Lexicalの初期化をrAFで遅延させてナビゲーションのラグを解消

basecamp/lexxy

Lexxyエディタの初期化処理を次のアニメーションフレーム(rAF)まで遅延させることで、Turbo morphと同時に発生していたナビゲーション時のラグスパイク(50〜100ms)を解消しました。

背景

LexxyをTurbo morphと組み合わせて使用した際、ナビゲーションに50〜100msの視覚的なハングが生じていました。根本原因はLexicalの持つ強制レイアウトフラッシュにあります。

LexicalはsetRootElementの中で選択ハンドラがSelection.anchorNodeを参照します。この参照がブラウザに即時レイアウトフラッシュを強制し、ペンディング中のミューテーションの確定、スタイルの無効化、再計算を一斉に引き起こします。Turbo morphがDOMを更新している最中にLexxyが初期化されると、このフラッシュがmorph由来の変更すべてを道連れにし、ペイントが遅延します。

遅延初期化により、ナビゲーションとDOMのペイントが先に完了した後でLexxyが初期化されるため、レイアウト計算の対象が大幅に減り、初期化自体も高速に完了します。

技術的な変更

connectedCallback内の初期化フローをrequestAnimationFrameでラップし、DOM確定後に実行されるよう変更しました。

変更前:

this.#initialize()
this.#scheduleEditorInitializedDispatch()
this.toggleAttribute("connected", true)
this.#handleAutofocus()
this.valueBeforeDisconnect = null

変更後:

this.#initialize()
this.toggleAttribute("connected", true)

requestAnimationFrame(() => {
  this.#mountRoot()
  this.#handleAutofocus()
  this.#dispatchInitialize()
})

getRootElement()はrAF前はnullを返し、rAF後にエディタのルート要素を返します。これはテストでも明示的に検証されています。

rAFへの移行に伴い、既存の#scheduleEditorInitializedDispatch / #cancelEditorInitializedDispatchの仕組みはフラグベースの管理に置き換えられました。具体的には#initializeEventDispatched#editorInitializedDispatched#valueLoadedの3つのboolean フラグが導入されています。disconnectedCallbackではこれらのフラグをリセットし、#valueLoadedtrueの場合のみvalueBeforeDisconnectを保存することで、rAF実行前に切断された場合の不正な値保存を防いでいます。

イベントリスナーの登録方法も合わせて変更されています。command_dispatcher.jsselection.jsでは、getRootElement()で取得した要素に直接リスナーを付けていた箇所がeditor.registerRootListenerベースの実装に移行しました。これにより、ルート要素がnullから実要素に変化するタイミングに合わせてリスナーが登録・解除されるようになり、rAF遅延との整合性が取れています。

// 変更前: getRootElement()で直接取得
const root = this.editor.getRootElement()
registerEventListener(root, "dragover", ...)

// 変更後: registerRootListenerでライフサイクルに追従
this.editor.registerRootListener((rootElement) => {
  if (rootElement) {
    const teardowns = [
      registerEventListener(rootElement, "dragover", ...),
      ...
    ]
    return () => teardowns.forEach((teardown) => teardown())
  }
})

設計判断

rAFコールバック内へのまとめ方が設計上の重要なポイントです。mountRoothandleAutofocusdispatchInitializeを1つのrAFコールバックにまとめることで、「ルート要素がマウントされる前にイベントが発火する」「オートフォーカスがレイアウト前に実行される」といった競合状態を一括して防いでいます。

ルート要素のライフサイクルをregisterRootListenerで管理する方針は、rAFによる遅延をイベントリスナー側に意識させない設計です。リスナーはルート要素が存在する間だけ有効であることが保証され、遅延の有無に関わらず正しく動作します。またselection.js#containEditorFocusでは、インラインのイベントハンドラが#handleArrowKeyOnLexicalCursorメソッドとして分離されており、リスナーの登録・解除とロジックの関心を分けています。

まとめ

requestAnimationFrameによる1フレームの遅延という小さな変更で、Turbo morphとの競合が引き起こす強制レイアウトフラッシュという複合的な問題を解消しています。registerRootListenerへの統一は、遅延初期化のための場当たり的な対処ではなく、ルート要素のライフサイクルをより堅牢に扱うための設計改善として機能しています。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
b8f1097c

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)、背景・技術詳細・設計判断(各論)、まとめ(結論)の3部構成が明確で、ガイドラインに完全に準拠しています。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```言語:ファイルパス)およびPR番号のリンク記法が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

「強制レイアウトフラッシュ」や「Turbo morph」などの専門用語を適切に使用しており、専門知識を持つエンジニアという対象読者に適合しています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論の構成になっており、各段落はトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られています。可読性が非常に高いです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内のすべてのコード引用は、提供されたDiff内容と正確に一致しています。コードの省略も技術的理解を妨げない範囲で適切に行われています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「強制レイアウトフラッシュ」「rAF」「ライフサイクル」「ルート要素」といった技術用語が、文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

ラグの根本原因から解決策の仕組みまで、PR DescriptionとDiffに基づいた技術的に正確で論理的な説明がなされています。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内の主張はすべてPR DescriptionやDiff内のコードで裏付けられており、ハルシネーション(創作)は見られません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#1044)やラグの時間(50〜100ms)などの数値・固有名詞は、PR情報と完全に一致しています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルは、PRの主題である「rAFによる初期化の遅延」とその結果である「ナビゲーションラグの解消」を的確に要約しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

PR情報に記載のないバージョン情報やサポート状況などの外部知識は含まれておらず、事実に基づいています。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「〜生じていました」「〜解消しました」といった時間表現は、PRの文脈と一致しており正確です。