設問とコンテキスト
ドキュメントリーダーは、リッチテキスト内の検索一致箇所、注釈、現在選択されているヒットを表示します。すべての一致箇所をspanでラップすると、テキスト選択、編集、レイアウトが崩れてしまいます。Rangeのライフサイクル、レジストリの更新、カスケード、アクセシブルな状態、フォールバック、パフォーマンスをカバーするCSS Custom Highlight APIソリューションを設計してください。これはテキスト描画とプログレッシブエンハンスメントに関するfrontendの質問です。
面接官が見ているポイント
- JavaScriptのRange、Highlightオブジェクト、
::highlight()の違いを明確に理解しているか。 - テキスト置換や編集後に古いRangeを再構築できるか。
- ハイライトがDOMを変更せず、
::selectionとカスケード可能であることを理解しているか。 - コントラスト、現在の一致箇所、フォーカス、キーボード操作のセマンティクスを設計できるか。
- 安全なフォールバックを提供し、そのコストを定量化できるか。
尋ねるべき確認事項
- 対象ブラウザやWebViewはそのAPIをサポートしていますか?
- ドキュメントは編集可能ですか、仮想化されていますか、頻繁に再レンダリングされますか?
- 一致箇所はいくつ存在し、一致箇所が複数のテキストノードをまたぐことはありますか?
- ダークモード、ハイコントラスト(forced colors)、スクリーンリーダーのサポートは必須ですか?
- 非対応環境において、DOMにマークをつけるフォールバックは許容されますか?
30秒で答える要約
「各一致箇所をRangeとして表現し、それらのRangeをHighlightにまとめ、::highlight(name)によるスタイリングのためにCSS.highlightsに登録します。テキストが変更された際は、切り離されたノードを保持し続けるのではなく、古い登録を削除し、テキストのバージョンとオフセットから再構築します。検索、注釈、現在の一致箇所のレイヤーを分離し、色だけに頼らず件数やキーボード状態を提示します。サポート状況を検知して安全にマークされたテキストへフォールバックし、一致件数、更新p95、描画フレーム、メモリをベンチマークします。」
詳細解説
ステップ 1: RangeとHighlightの作成
このAPIを使用すると、JavaScriptで任意のテキストRangeを作成し、それらをHighlightに結合して、そのHighlightをHighlightRegistryに登録できます。CSSはラッパー要素を追加することなく、::highlight(name)でRangeのスタイルを設定します。
const range = new Range();
range.setStart(textNode, start);
range.setEnd(textNode, end);
const hit = new Highlight(range);
CSS.highlights.set("search", hit);プロダクションコードでは、ノードをまたぐRangeやノードのライフタイムも適切に処理する必要があります。
ステップ 2: テキスト更新の調整
各一致情報とともに、ドキュメントのバージョン、テキストノードのパス、文字オフセットを保持します。再レンダリングや編集によってノードがDOMツリーから切り離され、そのRangeが無効(stale)になることがあります。古いレジストリエントリを削除し、DOM参照のみを保持するのではなく、新しいテキストインデックスから再構築します。
ステップ 3: レイヤーの管理
検索、注釈、現在の一致箇所、スペルチェックの候補には個別の名前を使用します。::selectionや他のユーザーエージェントのハイライトとの意図したカスケードを定義します。1つの現在ヒット用オブジェクトを更新する際に、すべての一致箇所の再構築が強制されないようにします。
ステップ 4: アクセシブルなフィードバックの提供
色を唯一のシグナルにしてはいけません。現在のヒット件数、フォーカス可能なナビゲーション、可視フォーカスインジケーター、ハイコントラスト(forced colors)向けの代替手段を提供します。ハイライト自体がスクリーンリーダーによって読み上げられると想定せず、結果リストでセマンティックなステータスを明示する必要があります。
ステップ 5: パフォーマンスの制御
入力をデバウンスし、レジストリ更新をバッチ処理し、可能な場合は可視ウィンドウ内の処理に限定します。テキスト走査、更新p95、スタイル計算、スクロールフレームレート、メモリを測定します。キー入力ごとのレジストリ変更は避けます。
ステップ 6: プログレッシブフォールバック
機能を検知し、Custom Highlight、マーク付きDOM、またはプレーンな結果リストを選択します。DOMフォールバックでは、テキストのエスケープ、リンクの保持、重複したネストラッパーの回避が必要です。クライアント側のエンハンスメントが行われる前でも、サーバー出力は読める状態を保つ必要があります。
ステップ 7: テストと受け入れ基準
複数ノードにまたがる一致、再レンダリング、編集、ダークモード、強制カラー、コピー、仮想化、RTL、非対応ブラウザをテストします。結果テキスト、キーボード順序、初回ペイント、スクロールFPS、更新p95、メモリを比較し、フォールバック時にも一致箇所が欠落しないようにします。
模範回答
「ドキュメントのDOMを変更しない方針をとります。一致箇所をRangeとして表現し、それらをHighlightにグループ化してCSS.highlightsに名前を登録し、::highlight()でスタイルを適用します。各結果にはテキストバージョンとオフセットを保持させ、再レンダリング時には古い登録を削除して再構築します。検索、注釈、現在のヒットには別々のレイヤーを設け、件数表示、フォーカス、セマンティックステータスにより現在のヒットをアクセシブルにします。
機能検知により、Custom Highlightか安全なマーク付きDOM/結果リストフォールバックかを選択します。デバウンス、バッチ更新、可視ウィンドウ制限により負荷を制御します。受け入れテストでは、ノードをまたぐRange、コピー、編集、ダークモード、強制カラー、仮想化、非対応ブラウザを網羅し、p95、FPS、メモリ、結果の整合性を比較・検証します。」
よくある間違い
- すべてのヒットをspanで囲む → テキスト構造や選択状態を壊す → Range/Highlightを優先する。
- 古いRangeを保持し続ける → 再レンダリングでノードが切り離される → テキストバージョンに基づいて再構築する。
- 色だけに頼る → ハイコントラストモードや色覚多様性のあるユーザーが状態を認識できない → セマンティックなステータスとフォーカスを追加する。
- キー入力ごとに状態を変更する → スタイル計算の多発(churn)を引き起こす → デバウンスとバッチ処理を行う。
- フォールバックHTMLを文字列結合で作成する → インジェクションやネストの崩れのリスクが生じる → エスケープ処理を行い、マークする範囲を制限する。
フォローアップ質問と回答
フォローアップ 1: なぜspanを使用してはいけないのですか?
Custom HighlightはDOMを変更しないため、選択、編集、レイアウトへの干渉を減らすことができます。spanはエスケープとネスト制御を備えた互換性用フォールバックとして残します。
フォローアップ 2: 編集後にRangeはどうなりますか?
そのノードが置き換えられたり切り離されたりする可能性があります。古い参照を使い続けるのではなく、新しいテキストバージョンとオフセットから再構築します。
フォローアップ 3: スクリーンリーダーはハイライトを読み上げますか?
読み上げられると想定してはいけません。検索結果の件数、現在の状態、キーボードナビゲーションをセマンティックなテキストとして明示してください。
フォローアップ 4: リグレッションがないことをどのように証明しますか?
固定されたドキュメントと一致件数を用いて、更新p95、スタイル計算コスト、スクロールFPS、メモリ、コピー動作、仮想化処理を比較・測定します。