SSRガードの追加と `with-*` 属性によるスロット検出の改善
Lit SSRを利用する際にブラウザ専用APIへのアクセスでクラッシュしていた複数のコンポーネントに対し、isServer ガードと with-* 属性パターンを体系的に適用し、SSR互換性を大幅に向上させた。
背景
Web AwesomeではLit SSRによるサーバーサイドレンダリングをサポートしているが、複数のコンポーネントがSSR環境で動作しない問題が報告されていた。具体的には以下の3件のIssueが起点となっている。
-
#1858:
wa-page・wa-button・wa-drawerがSSRで動作しない -
#2174:
wa-scrollerがResizeObserver is not definedエラーでクラッシュする -
#2178:
HasSlotControllerがquerySelectorの非存在によりundefined !== nullとなり、サーバーとクライアントで描画結果が異なるハイドレーションミスマッチが発生する
Lit SSRは constructor() と connectedCallback() を呼び出すが、firstUpdated()・updated()・イベントハンドラは呼び出さない。そのため、connectedCallback() やクラスフィールドの初期化子で ResizeObserver・MutationObserver・document.* などのブラウザ専用APIを直接使用しているコンポーネントがすべてSSRで問題を引き起こしていた。
技術的な変更
本PRの変更は大きく2種類に分類される。1つ目は isServer ガードによるブラウザ専用APIの保護、2つ目は hasUpdated ternaryパターンによるスロット検出のSSRフォールバックだ。
isServer ガードによるブラウザ専用APIの保護
ResizeObserver・MutationObserver・document.createComment など、ブラウザ環境にしか存在しないAPIを使用している箇所に isServer ガードが追加された。影響を受けたコンポーネントは以下の通り。
-
wa-scroller: クラスフィールド初期化子でのResizeObserverインスタンス化をconnectedCallback()内に移動し、isServerガードで保護 -
wa-resize-observer:connectedCallback()内のResizeObserver生成をisServerガードで保護 -
wa-split-panel:connectedCallback()内のResizeObserver関連コードをisServerガードで保護 -
wa-tab-group:connectedCallback()の先頭にif (isServer) return;を追加 -
wa-tree:MutationObserverの生成とsetAttribute呼び出しをisServerガードで保護 -
wa-carousel:setAttribute呼び出しをisServerガードで保護 -
wa-zoomable-frame: クラスフィールドのMutationObserverインスタンス化を!isServer ? new MutationObserver(...) : nullの条件式で保護 -
WebAwesomeElement(基底クラス):document.createCommentによるコメントノード挿入をisServerガードで保護
wa-scroller での変更を例に示す。
変更前:
private resizeObserver = new ResizeObserver(() => this.updateScroll());
connectedCallback() {
super.connectedCallback();
this.resizeObserver.observe(this);
}
disconnectedCallback() {
super.disconnectedCallback();
this.resizeObserver.disconnect();
}
変更後:
private resizeObserver: ResizeObserver | null = null;
connectedCallback() {
super.connectedCallback();
// SSR guard: ResizeObserver is not available during server-side rendering
if (!isServer) {
this.resizeObserver = new ResizeObserver(() => this.updateScroll());
this.resizeObserver.observe(this);
}
}
disconnectedCallback() {
super.disconnectedCallback();
this.resizeObserver?.disconnect();
}
フィールドの型を ResizeObserver | null に変更し、disconnectedCallback() でのアクセスをオプショナルチェーン(?.)に統一している。
hasUpdated ternaryパターンによるスロット検出のSSRフォールバック
SSR環境では HasSlotController が内部で呼ぶ querySelector が存在しないため、undefined !== null が true になるというバグ(#2178)が存在した。これに対して、render() メソッド内のスロット検出に hasUpdated ternaryパターンが導入された。
// SSR時(hasUpdated === false)はwith-*プロパティを使用
// ハイドレーション後(hasUpdated === true)はHasSlotControllerに委譲
const hasFooter = this.hasUpdated ? this.hasSlotController.test('footer') : this.withFooter;
このパターンに対応するため、wa-dialog・wa-drawer・wa-button・wa-slider などに新しい with-* プロパティが追加された。例えば wa-button では withStart と withEnd が追加されている。
@property({ attribute: 'with-start', type: Boolean }) withStart = false;
@property({ attribute: 'with-end', type: Boolean }) withEnd = false;
// render() 内
'has-start': this.hasUpdated ? this.hasSlotController.test('start') : this.withStart,
'has-end': this.hasUpdated ? this.hasSlotController.test('end') : this.withEnd,
既存コンポーネント(wa-input・wa-textarea・wa-select・wa-radio-group 等)の with-label・with-hint プロパティについては、JSDocコメントを「SSR専用である」ことが明確になるよう統一的な文言に改訂した。
wa-textarea のパフォーマンス改善
wa-textarea では、connectedCallback() で無条件に ResizeObserver を生成していたコードを削除し、代わりに updateResizeObserver() メソッドを導入して必要な場合のみ生成する遅延初期化に変更した。disconnectedCallback() でのクリーンアップも unobserve() から disconnect() + undefined への代入に整理されている。
設計判断
isServer ガードを各コンポーネントに直接記述する方式が採用された。globalThis へのポリフィルシムを提供する回避策は、コントリビューティングガイドで明示的に禁止されている。これはポリフィルが隠れた依存関係を生み出し、将来的な保守コストを高めるためだと読み取れる。
hasUpdated ternaryパターンは、HasSlotController 自体を改修してSSR対応させるのではなく、render() メソッドの呼び出し側で分岐する設計だ。PRの説明では HasSlotController の変更は「より議論を要する可能性がある」として後続PRに委ねると明記されており、本PRでは安全で影響範囲の小さい方法が優先された。
with-* 属性のデフォルト値を false にしていることも重要な判断だ。開発者はSSRを使用する場合にのみこれらの属性を明示的に指定する必要があり、クライアントサイドのみのアプリケーションに影響しない。ハイドレーション後は hasUpdated が true になることで HasSlotController が自動的に引き継ぐため、with-* 属性を指定したまま残しても二重描画などの問題は発生しない。
まとめ
本PRは、個別のバグ修正を積み重ねるのではなく、isServer ガードと hasUpdated ternaryパターンという2つの共通パターンをコントリビューティングガイドとともに確立した点に意義がある。今後コンポーネントを追加・修正する際の規範が明文化されたことで、SSR互換性の維持がより体系的に行えるようになる。