SSR とハイドレーション不整合の検出と修正を自動化
SSR が有効な状態で、コンポーネントのハイドレーション時に lit-hydration-error が発生してビルドが失敗するケースが報告されていました。本 PR はその検出を自動化し、SSR の実装を安定させるためのコードと CI 変更をまとめています。
背景
SSR が有効な開発サーバーでドキュメントページをビルドすると、<wa-accordion> などのコンポーネントが子要素の更新を待たずに DOM 操作を行い、ハイドレーションミスマッチ が起きていました。これによりローカル環境でも CI でもテストが不安定になる問題がありました。
開発者は手動でページを確認してエラーを特定していたため、再現性のある自動テストが求められていました。PR の説明では「Playwright スクリプトでページをクロールし、エラーイベントをキャッチする」ことが解決策として提示されています。
技術的な変更
Playwright ハイドレーションチェックスクリプトの追加
packages/webawesome/scripts/check-hydration.js が新規追加され、SSR モードで全ドキュメントページを走査し lit-hydration-error が発火したらプロセスが非ゼロで終了します。主要な設定は環境変数 BASE_URL、CONCURRENCY、RELOAD_ATTEMPTS で調整可能です。
import { globby } from 'globby';
import { chromium } from 'playwright';
// ...(省略)
const BASE_URL = (process.env.BASE_URL || 'http://localhost:4000').replace(/\/$/, '');
スクリプトはサーバー起動待ち (waitForServer) とページ読み込み後のカスタムイベント監視 (page.evaluate) を行い、エラーがあれば process.exit(1) で失敗させます。
CI ワークフローへの統合
.github/workflows/hydration_tests.yml が新規作成され、npm start & で SSR 開発サーバーをバックグラウンド起動し、続いて npm run test:hydration を実行します。node_modules に playwright がインストールされるステップも追加されています。
- name: Start dev server
run: npm start &
working-directory: ./packages/webawesome
- name: Check docs pages for hydration errors
run: npm run test:hydration
working-directory: ./packages/webawesome
SSR 時のコンポーネントロジック修正
packages/webawesome/src/components/accordion/accordion.ts の handleSlotChange に、SSR 後の子要素の updateComplete を待機するロジックが追加されました。didSSR が真の場合、子要素がハイドレーション完了するまで Promise.allSettled で待ち、完了後に再帰的に handleSlotChange を呼び出します。
if (this.didSSR) {
const promises: Promise<boolean>[] = [];
this.getAllItems().forEach(item => {
if (item.didSSR && !item.hasUpdated) {
promises.push(item.updateComplete);
}
});
if (promises.length > 0) {
Promise.allSettled(promises).then(() => this.handleSlotChange());
return;
}
}
同様に docs/.eleventy.js では SSR が有効なときだけ globalThis.litSsrCallConnectedCallback = true を設定し、SimulateWebAwesomeApp へ ssr フラグを渡すように変更されています。
if (!isDev && !serverBuild) {
const ssr = process.env.SSR === 'true';
if (ssr) {
globalThis.litSsrCallConnectedCallback = true;
}
eleventyConfig.addTransform('simulate-webawesome-app', function (content) {
return SimulateWebAwesomeApp(content, { isDev: isDev, ssr });
});
}
ビルドスクリプトへのハイドレーションテスト呼び出し
packages/webawesome/scripts/build.js に cookie-parser ミドルウェアの登録と、SSR ビルド時に initLitSsr() を呼び出す処理が挿入され、check-hydration.js が CI でも実行可能になる基盤が整備されました。
import cookieParser from 'cookie-parser';
// ...
middleware: [
cookieParser(),
function simulateWebawesomeApp(req, res, next) { /* ... */ }
]
テストランナー設定の拡張
web-test-runner.config.js でブラウザリストが動的に構成され、CI では Chromium と Firefox、ローカルでは WebKit も含めてマルチブラウザでハイドレーションテストが走ります。
const browsers = [
playwrightLauncher({ product: 'chromium', concurrency }),
playwrightLauncher({ product: 'firefox', concurrency }),
];
if (process.env.CI !== 'true') {
browsers.push(playwrightLauncher({ product: 'webkit', concurrency }));
}
設計判断
エラーディテクションをコード側と CI 側の二層で検出
SSR のハイドレーションエラーは コンポーネント側での非同期更新待ちと CI での自動クロールの二段階で捕捉できる設計となっています。コンポーネント修正は子要素の完了待ちロジックだけで済み、既存 API への侵害はありません。
後方互換性と最小侵入性
handleSlotChange の追加は SSR 時だけ条件分岐し、CSR では従来通りの同期ロジックが走ります。また globalThis.litSsrCallConnectedCallback の設定は SSR が有効なときに限り有効化するため、開発サーバーのデフォルト挙動を変えません。
設定とテストの分離
Playwright スクリプトは環境変数で対象 URL・並列度・リロード回数を制御でき、ローカルと CI の負荷に合わせて柔軟に調整可能です。CI ワークフローは独立したジョブとして追加され、他のテストパイプラインに影響を与えない構成になっています。
まとめ
この PR では SSR 時に発生したハイドレーションミスマッチを 自動検出スクリプト と コンポーネント側の待機ロジック で解決し、CI にハイドレーションテストを組み込むことでリグレッションを防止します。既存コードへの影響は最小限に抑えつつ、開発フロー全体の信頼性が大きく向上しました。