Skip to content

main.jsのasync読み込みをやめてDOM参照処理の実行タイミングを保証する - #49

Open
sugawara-appleple wants to merge 2 commits into
developfrom
fix/vite-async-body-race
Open

main.jsのasync読み込みをやめてDOM参照処理の実行タイミングを保証する#49
sugawara-appleple wants to merge 2 commits into
developfrom
fix/vite-async-body-race

Conversation

@sugawara-appleple

Copy link
Copy Markdown
Contributor

変更内容の概要

vite() に渡していた scriptTagAttributes: { async: true } を削除し、あわせて externalLinksdomContentLoaded() で包みました。変更は2ファイルです。

ファイル変更
themes/develop/include/head/js.twigasync: true を削除(Touch_NotAdmin / Touch_Admin の2箇所)。理由をコメントで残す
themes/develop/src/js/lib/build-in.jsexternalLinks の中身を domContentLoaded() で包む

背景

vite() の出力は <script type="module"> で、仕様上すでに defer 相当(HTMLのパース完了後に実行)になります。async はその defer を打ち消し、モジュールの読み込みが終わった時点で即実行させるため、HTMLのパースが <body> に達しているかどうかが保証されません。

<body> の生成前に main.js が実行されると、DOMを参照する処理が対象0件で空振りします。

  • Alpine が Unable to initialize. Trying to load Alpine before <body> is available. を警告する(Alpine は警告後も MutationObserver で後から流れてくる要素を拾うため、x-data の動作自体には実害が出にくい)
  • externalLinks が空振りし、外部リンクに target="_blank" / rel="noopener noreferrer" が付かない。externalLinks は組み込みJSの中で唯一 domContentLoaded を待っていません。 文書末尾にあるフッターのリンクはどのページでも処理に間に合いません

async を外してもダウンロードは modulepreload が先行するため取得は遅くなりません。むしろ <head> のパース中に大きなモジュールを同期実行しなくなる分、初回描画には有利です。

このテーマを出発点にしたサイトでの実測

Chrome / Edge で再現し、Safari では実行が遅く再現しないことがあります。ビルドし直すと出て古いビルド生成物では出ないため、原因を掴みにくい症状です。

環境結果
ローカル(async あり)5回中5回、main.js 実行開始時に document.body なし(readyState=loading)。Alpine 警告あり。外部リンクの付与漏れあり
ステージング(async あり)初回アクセス4回中2回、フッターの外部リンクに target が付かない
同(async 削除後)警告0件、外部リンクは全件付与

確認したこと

  • npm --prefix themes/develop run lint
  • npm run package
  • themes/developnpm run build
  • ローカルの develop テーマ実機(修正後): Alpine 警告0件 / x-data 5/5 初期化 / x-cloak 残0 / 外部リンク 処理対象6件→6件付与。トップと /news/ で初回+キャッシュ済み各4回、回帰なし

実機では修正前の症状を再現できませんでした

develop テーマのローカル環境では、async あり(修正前)でも警告0件・外部リンク全件付与で正常でした。傍受なしの素の条件、CPU 8倍/20倍スロットル、キャッシュ有無、src/js/alpinejs/ のサンプルコンポーネント削除、pdfPreview 無効化によるモジュールグラフ縮小まで試しましたが再現せず、この環境で再現しない理由は特定できていません

そのため本PRは「現に壊れているから直す」ではなく、実行タイミングを保証しない指定を外し、DOMを参照する処理はDOMを待たせるという趣旨です。症状が出るかどうかはHTMLサイズ・JSサイズ・回線・キャッシュ・ブラウザで変わり、事前に予測できません。

関連Issue

なし

vite() の出力は <script type="module"> で、仕様上すでに defer 相当
(HTMLのパース完了後に実行)になる。async はその defer を打ち消し、
モジュールの読み込みが終わった時点で即実行させるため、HTMLのパースが
<body> に達する前に main.js が走ることがある。
その場合、DOMを参照する処理が対象0件で空振りする。
- Alpine が「Unable to initialize. Trying to load Alpine before <body>
is available.」を警告する。Alpine は警告後も MutationObserver で
後から流れてくる要素を拾うため、x-data の動作自体には実害が出にくい
- externalLinks(次のコミットで対応)が空振りし、外部リンクに
target="_blank" / rel="noopener noreferrer" が付かない。文書末尾に
あるフッターのリンクはどのページでも処理に間に合わない
このテーマを出発点にした複数のサイトで、main.js の実行開始時点で
document.body が存在しないこと(readyState=loading)と、それによる
フッターの外部リンクの付与漏れを実測で確認している。Chrome / Edge で
再現し、Safari では実行が遅く再現しないことがある。ビルドし直すと出て
古いビルド生成物では出ないため、原因を掴みにくい。
ダウンロードは modulepreload が先行するので、async を外しても取得は
遅くならない。むしろ <head> のパース中に大きなモジュールを同期実行
しなくなる分、初回描画には有利。
組み込みJSは他すべてが domContentLoaded() で包まれているのに
externalLinks だけ同期実行されており、DOM構築前に呼ばれると
querySelectorAll が0件を返して外部リンクに target/rel が付かないまま
終わる。
前のコミットの async 削除で実行タイミング側は解消しているが、この処理
自体はDOMを参照するため、他の組み込みJSと揃えて domContentLoaded を
待たせ、読み込み方法の変更に依存しないようにする。
htmx のスワップ後に window.dispatch(event.target) から呼ばれる経路では、
既にDOMに入った要素が context になるため domContentLoaded は即時発火し、
挙動は変わらない。
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@sugawara-appleple@uidev1116