目次
目次
はじめに
Customizable Select Element Ep.16からは、<selectedcontent>
が、どうして仕様に入ることになったのか、どういった技術的背景があるのかをお話ししています。
2024/12/9時点でのselectの各パーツの定義
Ep.16では、UAによるLight DOMへのNodeクローン実装について、CSSWGとの合意を得た詳細についてお話ししました。
今回からは、Light DOMでの実装の中でも肝となる、「どのタイミングでクローンするのか」の議論を具体的に見ていきます。
Timing of cloning for the <selectedoption>
element
前回までで、Light DOMへのNodeクローンはCSSWGとの合意を得ましたが、その実装については未確定でした。
👍🏻 この時点で固まっている仕様
- 選択された
<option>
で、cloneNode()
をCallする - 選択された
<option>
の、<option>
を除く<option>
内の全てのDOMをクローンする <selectedcontent>
を用いて、宣言的な方法で、クローンされたDOMを<selectedcontent>
のLight DOM内に追加する- 選択された
<option>
が変更されるたびに、<selectedcontent>
内のDOMを更新する
具体的には、前回のCSSWGとOpen UIの会合の中で、「<option>
の子Nodeを”どのタイミングで”クローンして、<selectedoption>
に反映するのか」という議論が発散してしまいました。
そのため、今回以下のIssueがWHATWGに切り出されます。
まず、クローンするタイミングの候補として、以下が挙げられました。
- 同期的にクローンする
- マイクロタスク実行時にクローンする
- Custom Element Reaction実行時にクローンする
Jarharが行ったBlinkの初期実装では、マイクロタスクを使用したかったことから、そのタイミングで発火するMutationObserverを使用していました。
では、そもそもどうしてマイクロタスクで実行したかったのでしょうか?
JS実行タイミングとブラウザレンダリングの仕組みの関係を理解する
マイクロタスクは、「マイクロタスクを呼び出す関数が実行された後、コールスタックが空になった後にのみ実行される短い関数」のことですが、詳細には、「コールバックキュー内のタスクキューが空になった後にのみ実行される短いタスク」と説明することがきます。
「コールバックキュー」とは、非同期処理の結果をキューイングするためのキューで、コールバックキュー内のタスクを実行するためのキューが、「タスクキュー」です。
「タスクキュー」とは、文字通り、タスクのキューです。そのキューを構成するのが「タスク」で、MDNでは以下のように説明されています。
A task is any JavaScript scheduled to be run by the standard mechanisms such as initially starting to execute a program, an event triggering a callback, and so forth. Other than by using events, you can enqueue a task by using setTimeout() or setInterval(). https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide/In_depth
つまり、タスクは「プログラムの実行開始やイベントが、コールバックをトリガーするなどの標準的なメカニズムにより実行されるよう、スケジューリングされた JavaScript のこと」です。
具体的なタスクの例としては、以下のようなものがあります。
- イベントリスナの、コールバック関数
setTimeout()
やsetInterval()
で登録されたコールバック関数
そして、本題の「マイクロタスク」は「コールバックキュー内のタスクキューが空になった後にのみ実行される短いタスク」と説明できました。
例えば、DevtoolsのPerformanceタブでJavaScriptの実行順序を観察してみると、dispatchEvent
というコールバックキュー内のタスクが実行された後に、「Run microtasks」でマイクロタスクが実行されていることがわかります。
JSでのマイクロタスクの実行タイミング
そして、このマイクロタスクの実行タイミングが、ブラウザレンダリングの過程でどこに当たるのかを図で表すと以下のようになります。
JS実行タイミングとレンダリングの相関図
同期的な処理はそのまま実行されますが、非同期な処理は一旦コールバックキューに入れ、タスクキューのタスクが終わったあとに順次処理されます。コールバックキューは、「タスクキュー」と「マイクロタスクキュー」に分けられ、タスクキューから優先的に消化され、最後にマイクロタスクキューに入っているマイクロタスクが処理されます。
マイクロタスクを使うメリット
つまり、マイクロタスクキューは、JS一連の実行タイミングの中でも最後に非同期実行されるキューです。
マイクロタスクは、タスクキューのタスクが完了するたびに実行され、短期間で多くの小さな非同期処理を効率的に処理する特性を持っています。これにより、例えば、複数のDOM変更を一度にまとめて処理することで、レンダリングのオーバーヘッドを減少させることができます。そのため、パフォーマンスの点で優れた実装が見込めます。
それをすでにある仕様で実現できるのがMutationObserverであっため、Blinkでの初期実装はMutationObserverでクローンタイミングを制御していました。
同期的なMutationObserver: CEReactions MutationObserverの提案
これに対して、Custom Elementの文脈で利用されるattributeChangedCallback
やconnectedCallback
などの「CEReactionタイミング」での変更を検知する、CEReactions MutationObserverの実装を提案する意見もありました。
CEReactionsは、Custom Elementのライフサイクルに関連するタイミングで発火するコールバック関数のことです。このタイミングを利用することで、Custom Elementのライフサイクルに合わせて同期的にクローンを行うことができます。
Ep.16でも述べたように、非同期的に変更検知を行うMutationObserverでは、Layout Flash時に同期的に変更を検知することができません。つまり、非同期的な処理では、Layout Treeとの整合性が保たれない恐れがあります。これは、マイクロタスクタイミングを使った非同期処理のデメリットでもあり、CEReactionsタイミングを使って解決することができる問題でもあります。
CEReactionsを使った手法を主張するMozillaのsmaugは、以下のように述べています。
The over-cloning would happen only if one mutates the content of the selected option, no? The normal case is that user selects one option and the contents get cloned once. So CEReaction or even more synchronous cloning might not be too bad in this case.
ほとんどのケースでは、ユーザーは1つの
<option>
を選択し、コンテンツが1回だけクローンされる。したがって、CEReactionタイミングで同期的にクローンすればいいのでは?Microtasks were designed for MutationObserver, and the reason was to improve performance in cases when one does lots of DOM mutation all over the place. That is not quite the case here.
マイクロタスクはほぼMutationObserverのために設計されたと言っても過言ではなく、目的は、あちこちでたくさんのDOM変更を行う場合にパフォーマンスを向上させることにある。ここではそいうケースじゃないだろうから使わなくていいのでは?
comment on 2255746553
MutationObserverは、キューイングによるパフォーマンス向上が利点でしたが、CEReactionsは同期的な処理で、Layout Treeとの整合性を保つことができます。
最終的に、クローンタイミング実装の初期勘案では、主に2つの方法が挙げられましたが、Jarharは、最終的にマイクロタスクを使ったMutationObserverを使う方向を示します。
I think we should go with microtasks instead of CEReactions for the following reasons:
- MutationObservers already use microtasks, so trying to create an alternate type of CEReactions MutationObserver would be harder to spec and harder to implement.
- -> MutationObserverはすでにマイクロタスクを使っているので、別のCEReactions MutationObserverを作成しようとすると、仕様を作成する必要があり、実装するのも難しい。
- Performance will be better when imperatively building or modifying option elements due to fewer calls to clone all of the options contents into selectedoption elements.
- → 選択された
<option>
の内容をクローンする回数を減らせるため、<option>
を命令的に構築または変更する際のパフォーマンス向上が期待できる。- As @dandclark said in the call, it will be easier to understand how this works because it matches the author defined API of MutationObserver. I think this also increases the likelihood that this element is polyfillable.
- → MutationObserverのAuthor定義APIと一致するため、どう機能するか理解しやすい。これにより、この要素がポリフィル可能である可能性も高まると思う。
comment on 2265868320
以下が、これまでの議論結果を含めたBlinkでの再実装です。
ここまでが、議論の1/3程度の内容でした。
それでは、また明日⛄
See you tomorrow!
Appendix
- select: Should
<selectedoption>
respond to mutations in the selected<option>
· Issue #825 · openui/open-ui - Add
<selectedcontent>
element by josepharhar · Pull Request #528 · w3c/html-aria - Define the
<selectedcontent>
element by josepharhar · Pull Request #10633 · whatwg/html - [html-aam] Addition: selectedoption element by scottaohara · Pull Request #2344 · w3c/aria
- 5370555: Implement for StylableSelect
- JS Visualizer 9000
- Accessibility Object Model | aom
- HTML Standard - Custom Element Reaction
- In depth: Microtasks and the JavaScript runtime environment - Web APIs | MDN
Standard Positions