WebKitを探訪する
URLを開いてから、画面のボタンが動くまで。
一つの小さなページを手がかりに、エンジンの部品と仕事の流れを辿る。
01WebKitは、ページを動かすエンジン
リンクを開くと、文字や画像が表示される。 ボタンを押すと、文字が変わる。 その間に、コンピューターは何をしているのだろう。 ここでは、次の小さなページが動くまでを入口にして、WebKitの中を歩いていく。
<!doctype html>
<html lang="ja">
<meta charset="utf-8">
<title>読書メモ</title>
<style>
button { color: navy; }
</style>
<button id="read">読む</button>
<script>
const button = document.querySelector("#read");
button.addEventListener("click", () => {
button.textContent = "読んだ";
});
</script>
</html>
WebKitは、Webの文書やプログラムを処理し、表示や操作を支えるエンジンだ。 HTMLという文書の記述、CSSという見た目の規則、JavaScriptというプログラムを、それぞれの規則に従って扱う。 単にHTMLを画像に変換して終わるものではない。 クリックを受け、文書を変更し、通信し、再び画面を更新するところまでが続いている。
SafariはWebKitを使うアプリケーションの一つだ。 タブやアドレス欄などを持つブラウザー製品と、その中でWebの内容を処理するエンジンは、担当する範囲が違う。 ブラウザー以外のアプリもWebKitを組み込める。 Appleのプラットフォームでは、その入口の一つがWKWebViewというAPIだ。
図1の最下段にOSの機能があることも大切だ。 WebKitは、ネットワークやフォントや動画再生のすべてを、どのOSでも同じ独自コードだけで処理するわけではない。 Webの規則を実装する共通部分と、動作する環境の機能が協調している。 まずは「アプリがWebKitへ仕事を頼み、WebKitが必要な部品を使って処理する」と捉えると、次の分解を追いやすい。
この節の出典とコードの入口
- The WebKit Open Source ProjectProject Goals; What WebKit is Not
- Introduction to WebKitWhat is WebKit? のコンポーネント一覧
- WebKit2 — WebKit's Multi-Process ArchitectureOverview
02一つの上流から、複数の環境へ
このエンジンは、最初から現在の形で生まれたものではない。 WebKitにはKHTMLから継承したコードがあり、公式の回顧ではプロジェクトの開始を2001年6月25日としている。 2003年にはSafariが発表され、同年に1.0が公開された。 2005年のWWDCでのオープンソース公開は、開かれた開発の節目として記録されている。 これは、それ以前に関連コードが一切公開されていなかったという意味ではない。
- 2001WebKitプロジェクト開始。KHTMLを基に開発
- 2003Safariの発表と1.0の公開
- 2005開かれた開発体制の節目
- 2013ChromiumがBlinkへの派生を発表
図2の2013年には、ChromiumプロジェクトによるBlinkの発表がある。 BlinkはWebKitから派生したエンジンだが、現在のWebKitを調べるときに、Chromiumの内部構成をそのまま当てはめることはできない。 共通の出発点と、現在の実装が同じであることは別だからだ。
一方、現在のWebKit自体にも、複数の届け先がある。 共通のエンジンを特定の環境へ適応させる実装と統合をポートと呼ぶ。 Apple向けのポート、Linux向けのWebKitGTKとWPE WebKit、PlayStation向けのポートなどがあり、共通コードを共有しながら環境ごとの仕事を進めている。 公式資料は主要な保守主体としてApple、Igalia、Sonyを挙げるが、それだけで全貢献者や担当範囲を言い尽くせるわけではない。
この構造から、記事を読むときの確認事項が一つ決まる。 「WebKitのソースにある」は、「手元のSafariで使える」と同じではない。 各ポートはそれぞれリリースし、ビルド条件や設定も異なる。 本記事のコード説明は、2026年9月7日の上流コミットを固定して読んだ結果であり、特定の出荷版を実測した説明ではない。
この節の出典とコードの入口
- Celebrating 15 Years of WebKit2001年6月25日のプロジェクト開始と8月24日の最初のcheck-in
- 10 Years of Web InspectorSafari Unveiled; Safari 1.0 Released; WebKit Open Sourced
- Blink: A rendering engine for the Chromium project2013年4月3日のBlink発表
- What Are WebKit Ports?upstream ports一覧; リリースはポートごとに行うという説明
03コードの部品を見分ける
例のページを処理するには、文書を扱う部品、JavaScriptを動かす部品、アプリとの連絡を担う部品が必要になる。
ソースのSourceディレクトリを見ると、その役割が名前になって現れる。
コードは主にC++で書かれ、C、アセンブリー、AppleのAPIとの統合に使うObjective-Cなども含む。
WebCoreは、Webの文書と機能を実装する中心だ。 HTMLとCSSの解析、文書の構造、要素の振る舞い、位置や大きさの計算、描画、さまざまなWeb APIを扱う。 「読む」というボタンがどんな要素で、何という文字を持ち、どこに置かれるかは、この領域へつながる。
**JavaScriptCore(JSC)**は、JavaScriptの言語仕様であるECMAScriptを実装するエンジンだ。
例のスクリプトを解析して実行する仕事はこちらへつながる。
ただし、documentやボタンの操作までがJavaScriptという言語そのものに含まれるわけではない。
WebCoreが提供する機能とJSCの間には、あとで辿る接続部分がある。
レイアウト、描画、Web API
最適化、GC、WebAssembly
図3の上にあるSource/WebKitは、プロジェクト全体の名前と同じなので少し紛らわしい。
このディレクトリでは、アプリ向けAPIやプロセス間の協調など、WebCoreを製品から利用するための統合を扱う。
旧来の単一プロセス統合を扱うWebKitLegacyとは区別され、複数プロセスの仕組みはWebKit2とも呼ばれる。
「2」はCSSやJavaScriptのバージョンではない。
下段は共通の土台だ。
**WTF(Web Template Framework)は文字列、コンテナー、参照を扱う型などを提供する。
bmallocはメモリー領域の割り当てを担う。
WebCoreのplatformやPAL(Platform Abstraction Layer)**は、OS依存の機能との接点になる。
これらの名前を最初から覚える必要はないが、「どの部品が何を持っているのか」を確かめる地図として使える。
この節の出典とコードの入口
- Introduction to WebKitWhat is WebKit? のコンポーネント一覧
- Introduction.mdLicensingのKHTML由来の説明
04仕事を実行する場所を分ける
部品の地図ができたら、次は実行場所を見る。 プロセスは、OSが実行とメモリー空間を管理する単位だ。 スレッドは、そのプロセス内で処理を進める実行の流れを指す。 コードをどのライブラリーに置くかと、処理をどのプロセスやスレッドで動かすかは、別の分け方になる。 WebCoreという部品があるからといって、WebCore専用のプロセスが必ず一つ起動するわけではない。
複数プロセス構成では、アプリ側をUIプロセスと呼ぶ。
Webページの内容やコードを扱うのがWebContentプロセスで、ソースではWebProcessという名前も現れる。
Networkプロセスは通信だけでなく、HTTPキャッシュやWebサイトの保存領域の管理も担う。
さらに、構成に応じて描画やメディア処理を分離するGPUプロセスがある。
JavaScriptCoreによるページのコード実行
図4の境界を跨ぐときには、**IPC(プロセス間通信)**で依頼や応答を送る。 別プロセスのC++オブジェクトを、手元の普通のポインターでそのまま呼ぶ関係ではない。 WebKitには非同期の送受信だけでなく、同期の通信を扱うAPIもある。
具体的な窓口として、UI側にWebPageProxy、WebContent側にWebPageがある。
前者はページへの要求や通知を扱う代理で、後者はWebCoreのPageへつながる。
似た名前が並んでいても、所属するプロセスと責務を見ると区別できる。
また、タブとプロセスの数には固定の一対一対応がない。 一つのページには埋め込まれた別の文書がありうるし、隔離の設定によってその配置も変わる。 GPUプロセスも物理的なGPUチップを指す名前ではない。 図の箱を「実行するソフトウェアの区画」として読むことが、部品名との混同を防ぐ。
この節の出典とコードの入口
- WebKit2 — WebKit's Multi-Process ArchitectureOverview
- WebPageProxy.cppWebPageProxy::loadRequest; loadRequestWithNavigationShared; decidePolicyForNavigationAction
- WebPage.cppWebPage::create; Page::create; WebPage::loadRequest
- NetworkResourceLoader.cppstartRequest; canUseCache; retrieveCacheEntry; startNetworkLoad; didReceiveResponse
- GPUProcess.cppENABLE(GPU_PROCESS); createGPUConnectionToWebProcess
- IPC Connection.hsend; sendSync; sendWithAsyncReply
06HTMLから、文書の木を組み立てる
ネットワークから届くものは、最初はバイト列だ。
文字コードに従ってデコードすると文字列になり、HTMLパーサーへの入力になる。
WebCoreではDecodedDataDocumentParserがTextResourceDecoderを使う経路を確認できる。
HTMLの解析は、大まかには二つに分けられる。
まず開始タグや終了タグ、文字などのトークンへ分ける。
次に、そのトークンとHTMLの木構築規則を使って、文書のノードを組み立てる。
HTMLDocumentParserとHTMLTreeBuilderが、その処理を辿る入口になる。
- バイト列受信した文書のデータ
- 文字列文字コードに従ってデコード
- トークン開始タグ、終了タグ、文字など
- DOM木構築の規則でノードを組み立てる
- html
- head
- body
- button
- Text「読む」
- button
こうしてできる**DOM(Document Object Model)**は、文書をノードの木として表し、プログラムから扱うためのモデルだ。
図6では、文書全体のDocument、タグに対応するElement、文字内容のTextが別々のノードとして現れる。
ボタンと「読む」という文字は、同じ一個のノードではない。
冒頭のHTMLではheadとbodyのタグを明示していないが、DOMにはそれらの要素が作られる。
パーサーには省略を補う規則や、不正な入れ子から回復する規則がある。
したがってDOMは、ソースの字面をそのまま保存した木ではない。
HTMLソースを見ることと、開発者ツールで現在の要素を見ることに差が出る理由の一つだ。
文書と、それを収める場所も分けて考えよう。
iframeはページの中に別の文書を持てるため、一つのページに複数のDocumentが存在しうる。
その文書を収めるフレームの親子関係と、一つの文書内のDOMの親子関係は別の木だ。
LocalFrameは、同じプロセスにある文書、ローダー、表示を担うLocalFrameViewへの接点を持つ。
もう一つの木であるShadow DOMも、通常の子ノードの木と区別される。 ただし、それ自体が別プロセスや別オリジンの隔離境界になるわけではない。 「木が別」というだけで、実行場所や権限まで別だとは結論できない。
この節の出典とコードの入口
- HTML Standard — Parsing HTML documentsOverview of the parsing model; Tokenization; Tree construction
- DOM StandardTrees; Shadow trees; Dispatching events; Mutation observers
- DecodedDataDocumentParser.cppappendBytes; TextResourceDecoder::decode
- HTMLDocumentParser.cpppumpTokenizer; constructTreeFromHTMLToken; preload scanner呼び出し
- HTMLTreeBuilder.cppHTMLTreeBuilder::constructTree
- LocalFrame.hLocalFrame; document; loader; LocalFrameView
07CSSとDOMから、表示に必要な構造を作る
DOMにはボタンがあるが、それだけでは文字色も幅も決まらない。 CSSを読み、どの規則をどの要素に適用するかを調べる必要がある。
CSSにも解析の段階がある。
StyleSheetContents::parseStringや外部スタイルシートを扱う経路は、CSSの文字列をCSSParser::parseStyleSheetへ渡す。
そこで得たスタイルシートの規則と、各要素の情報を使って、Style::Resolverが要素のスタイルを求める。
CSSOMはCSSのスタイルシートなどをプログラムから扱うモデルで、CSSStyleSheetのcssRulesやinsertRuleもこの領域にある。
図7の合流点では、単に最後に書いた規則を選んでいるわけではない。
宣言の出所、重要度、カスケードレイヤー、セレクターの詳細度、継承などが値の決定に関わる。
「このボタンの文字はnavyにする」という結果と、「そのボタンをどの位置へ置くか」という結果も別だ。
参照コミットの描画オブジェクトでは、計算済みスタイルをStyle::ComputedStyleという型で扱っている。
表示のための構造もDOMとは別に作る。
WebCoreにはRenderObjectを基盤とする描画用の木があり、RenderTreeUpdaterがスタイル変更に応じてその作成、更新、破棄を行う。
違いを見るため、本文の例を少しだけ変えてみる。
<div style="display: contents">
<button>読む</button>
</div>
<p style="display: none">下書き</p>
- div(contents)
- button
- Text「読む」
- button
- p(none)
- Text「下書き」
- buttonのボックス
- 文字「読む」
div自身とpのボックスは作らない
- 役割:ボタン
- 名前:読む
- 操作:押す
不要なノードをそのまま複製しない
図8の左側では、非表示のpもDOMに残っている。
display: noneは、その要素と子孫の表示用ボックスを作らない指定であり、DOMから要素を削除する操作ではない。
一方、この例のdisplay: contentsはdiv自身のボックスを作らず、子のボタンにはボックスを作れる。
これを置換要素なども含めた全要素に無条件で一般化することはできないが、二つの木が違う理由は見える。
逆に、描画用の構造には、HTMLに要素として書いていない匿名のオブジェクトが必要になる場合もある。 「DOMのノードごとに描画の箱を一つ作る」では足りないのだ。 図8の右側に置いたアクセシビリティの構造も、あとで見るように、さらに別の目的で情報を組み立てる。
この節の出典とコードの入口
- StyleSheetContents.cppparseAuthorStyleSheet; parseString; CSSParser::parseStyleSheet
- StyleResolver.cppStyle::Resolver::unadjustedStyleForElement; initializeStateAndStyle; appendAuthorStyleSheets
- CSSStyleSheet.hCSSStyleSheet; cssRules; insertRule; deleteRule; replace
- CSS Cascading and Inheritance Level 5Value Processing; Cascading; Defaulting
- CSS Display Module Level 3Box Generation; displayのnoneとcontents
- RenderTreeUpdater.cppRenderTreeUpdater::updateElementRenderer
- RenderObject.hanonymous objectsのコメント; needsLayout; styleのStyle::ComputedStyle戻り値
08位置を決め、描いて、画面へ届ける
スタイルと表示用の構造が揃うと、次はレイアウトで位置や大きさを求める。
幅、高さ、周囲との関係、文字がどこで折り返すかなどを計算する仕事だ。
文字の幅にはフォントや字形が関わるので、単純な文字数だけでは決まらない。
WebCoreのFontCascadeには、フォントの選択や文字幅の測定、テキスト描画への接点がある。
位置が決まっても、まだ画素を描いたことにはならない。
paintでは、背景を塗る、文字を描く、画像を描くといった操作を、GraphicsContextなどを通して描画先へ渡す。
描画操作を画素へ変える仕事はラスタライズと呼ばれ、操作を組み立てる段階と実際の処理先が分かれる経路もある。
GraphicsContextなどを経て描画先へ
図9の中央に「合成レイヤーの構造と描画先」を置いたのは、合成がpaintの後だけに現れる仕事ではないためだ。
合成は、複数の描画内容をそれぞれの位置や重なりなどに従って組み合わせる処理だが、そのためのレイヤー構造は先に準備する必要がある。
RenderLayerCompositorは、スタイルやレイアウトの更新に伴って合成の要件やレイヤーの対応を更新する。
RenderLayerBackingがレイヤー内容のpaintに関わり、レイヤーを同期するときにpaintが起こる場合もある。
そのため、RenderLayerCompositorを「すべてのpaintが終わった後で、画面を完成させるだけの関数」と読むと役割を取り違える。
レイヤーの準備と、プラットフォーム側での最終的な合成や提示を分けておこう。
DOM要素、RenderObject、RenderLayer、GraphicsLayer、GPUのテクスチャにも、固定の一対一対応はない。
表示経路はポートや設定によって変わる。
GPUプロセスを利用する経路にはRemoteRenderingBackendなどがあり、描画を頼む場所と処理する場所を分離できる。
「WebKitの描画は全部GPUで行う」とも、「paintは常にWebContentの同じスレッドで完了する」とも一括りにはできない。
最後に、ここまでの処理は一回限りではない。
文字やスタイルが変われば、必要な計算と描画を繰り返す。
WebCoreはneedsLayoutなどの状態で更新の必要性を管理するため、どんな変更でも文書全体を最初から作り直すわけではない。
この節の出典とコードの入口
- LocalFrameView.cpplayoutContext().layout; paintContents; flushCompositingStateIncludingSubframes
- FontCascade.hdrawText; width; fontForCombiningCharacterSequence; FontCascadeFonts
- GraphicsContext.hfillRect; drawImage; 描画操作の抽象インターフェース
- RenderLayerCompositor.cppcompositing requirements; GraphicsLayer; flushPendingLayerChanges
- RenderLayerBacking.cpppaintContents; paintIntoLayer
- RemoteRenderingBackend.cppstartListeningForIPC; workQueueInitialize; createImageBuffer; RemoteDisplayListRecorder
09JavaScriptから文書を変更する
ここでボタンを押して、文字を「読んだ」へ変えてみよう。
イベントハンドラーの中で実行されるのは、button.textContent = "読んだ"という代入だ。
この一行は、JavaScriptという言語の実行と、Webの文書の操作をまたいでいる。
JSCは代入のような言語の処理を実行する。
一方、textContentがどのように文書の文字内容を変更するかは、DOMの機能だ。
WebCoreのC++オブジェクトと、JavaScriptから見えるオブジェクトをつなぐバインディングが、この境界を受け持つ。
図10で、JavaScriptから見えている側はラッパーと呼ばれる。 APIの形はWeb IDLという記述でも定義され、その定義からビルド時に生成したC++コードと手書きのコードがバインディングを構成する。 Web IDL、生成された接続コード、実際のDOMの実装という順に辿ると、「JavaScriptで書けること」がC++の仕事へ変わる場所を見つけられる。
ラッパーとC++オブジェクトの対応にも条件がある。
異なるDOMWrapperWorldが一つのC++オブジェクトに対して別々のラッパーを持つ場合があるので、常に一対一と決めつけない。
ただし、このボタンの例を理解するには、まず「言語のエンジンと文書の実装を接続している」と分かれば十分だ。
文字内容が変更された時点と、利用者が新しい文字を画面で見る時点には間がある。 DOMの更新から、必要なスタイルやレイアウト、paintへつながり、その内容が表示される。 APIを呼ぶたびに即座に画面を一枚描く仕組みではないことを、次の実行順序の話で確かめよう。
この節の出典とコードの入口
- JS Wrappers and IDL FilesOverview; JS Wrapper Lifecycle Management; Opaque Roots
10JavaScriptを、始めやすく速く動かす
その前に、JSCがスクリプトをどう実行するかを少し掘り下げる。 コードの文字列は、字句解析と構文解析を経て、実行用のバイトコードになる。 JSCには、それを解釈する方式と、実行時に機械語へ変換するJIT(Just-In-Time)コンパイルがある。
機械語にすれば、準備の費用が消えるわけではない。 変換や最適化にも時間とメモリーを使う。 一度しか動かない短い処理へ多くの最適化時間をかけても、その費用を取り戻せない場合がある。 繰り返し動く処理では、逆に費用をかけた最適化が役立つ。
- 解析 → バイトコード実行用の命令列を作る
- LLIntバイトコードを解釈して実行
- Baseline JIT機械語に変換し、実行情報も集める
- DFG JIT実行情報を使って最適化
- FTL JITさらにコンパイル費用をかけて最適化
JITが使えない構成もある。JavaScriptの意味を変えるための階段ではない。
図11の**LLInt(Low Level Interpreter)**はバイトコードを解釈して実行する。 Baseline JIT、DFG JIT、FTL JITは、実行状況に応じてより強い最適化を行う段階として登場する。 構成や実行情報によって使われ方は変わり、すべての関数がこの階段を最後まで上るわけではない。 JITを利用できない構成もある。
最適化では、「この値はこれまで数値だった」といった観測から仮定を置くことがある。 仮定が成立する場合には処理を簡略化できても、あとで違う値が来れば、そのままでは正しい意味を保てない。 そこでOSR exitなどにより、実行状態を引き継いで低い段階へ戻る道が用意されている。 この戻りは、それ自体がJavaScriptプログラムのエラーというわけではない。 速く動かしながら、言語の意味を保つための仕組みだ。
JSCにはWebAssemblyの実装もあるが、その実行方式をこのJavaScriptの四段階と同じものとして扱うことはできない。
関心がある場合はSource/JavaScriptCore/wasmが別の入口になる。
この節の出典とコードの入口
- JavaScriptCoreCore Engine; LLInt; Baseline; DFG; FTL
- Speculation in JavaScriptCoreTiering; OSR; Watchpoints and Invalidation
- JITCode.hJITType; nextTierJIT; ENABLE(JIT)
- WasmContext.hENABLE(WEBASSEMBLY); JSC::Wasm::Context
11処理の順番と、表示の機会をつなぐ
JavaScriptが動くことと、いつ動くかは別の問題だ。 クリック、タイマー、通信の応答などがある中で、ブラウザーは処理の順番を管理する。 その枠組みがイベントループだ。 HTMLのモデルでは、タスクの実行と、マイクロタスクのチェックポイントを区別する。
たとえばクリックのハンドラーを次のように変える。
button.addEventListener("click", () => {
button.textContent = "処理中";
Promise.resolve().then(() => {
button.textContent = "読んだ";
});
});
同期的な代入の後に、解決済みPromiseに登録した処理がマイクロタスクとして実行される。 ただし、DOMが一度「処理中」になったからといって、利用者がその文字を画面で見られるとは限らない。 画面への反映より先に「読んだ」へ変わるからだ。
- クリックのタスクイベントハンドラーを実行し、同期コードを終える
- マイクロタスクのチェックポイントこの例で予約したPromiseの反応を処理
- 描画機会がある場合の更新requestAnimationFrameのコールバックなど
- 必要なレイアウトや描画変更を表示用の内容へ反映
- 提示プラットフォームを通じて画面へ
図12の描画更新には、requestAnimationFrameで予約したコールバックも関わる。
これは次の表示へ向けて更新を行う入口であり、表示完了を知らせる通知ではない。
WebCoreのPage::updateRenderingでは、そのコールバックより前にも、必要に応じて後にもレイアウトを扱う処理がある。
「rAF、レイアウト、表示」という図だけで、内部のすべての順序を説明したことにはならない。
また、タスクが一つ終わるたびに必ず画面を一回更新する、という規則でもない。
描画機会に従って必要な更新が行われる。
WebCoreのEventLoop::runやperformMicrotaskCheckpointは、タスクとマイクロタスクの実装を辿る入口になる。
マイクロタスクは、別のOSスレッドの名前ではない。 Promiseを使うだけで重い計算が別の場所へ移るわけでもない。 長い処理やマイクロタスクの連続がページの仕事を占めれば、入力応答や描画の機会にも影響する。 そこで、計算の担当を分ける手段が次に登場する。
この節の出典とコードの入口
- HTML Standard — Event loopsProcessing model; Perform a microtask checkpoint; Update the rendering
- EventLoop.cppEventLoop::run; performMicrotaskCheckpoint
- Page.cppPage::updateRendering
- How Web Content Can Affect Power UsageJavaScript; Painting; Layout and Rendering timeline
12計算をWorkerへ分担する
ボタンを押した後に、大量の読書記録を集計したいとしよう。
その計算をページ側で長く実行すると、入力や表示の仕事と競合しうる。
Workerは、ページのWindowとは異なるグローバル環境とイベントループでスクリプトを実行する仕組みだ。
ここでは、new Worker("worker.js")で作る通常のDedicated Workerを考える。
利用者の操作を受ける
ページのdocumentは操作できない
図13では、ページが計算対象を送り、Workerが結果を返している。
postMessageでデータを受け渡すのであって、Workerがページのdocumentを直接操作するわけではない。
DOMを変更して表示へつなぐのは、結果を受け取ったページ側の仕事だ。
参照した実装では、通常のDedicated Workerを作るWorkerMessagingProxyがCreateNewThreadを指定する。
つまり、ここで使っている例は、ページの計算を別スレッドへ分ける経路として説明できる。
それだけで別プロセスに置かれるとは言えないが、Promiseのコールバックを予約することとは実行の分け方が違う。
共通のWorkerThreadクラスだけを見ると、メインスレッドで実行する分岐も見つかる。
ただし、その分岐をもって「通常のnew Workerも同じメインスレッドで動く」と判断してはいけない。
SWServerWorkerにはテスト指定や特定のページ識別子があるときにそのモードを選ぶ処理があり、呼び出し元の条件を辿る必要がある。
共通クラスに選択肢があることと、いま説明しているAPIがその選択肢を使うことは別だ。
Workerは計算を分ける道具であって、仕事そのものをなくす道具ではない。 何を送って、何を計算し、どれだけの結果を返すかまで含めて、分担を考えることになる。
この節の出典とコードの入口
- HTML Standard — Web workersScope; WorkerGlobalScope; The event loop; Communicating with a dedicated worker
- WorkerMessagingProxy.cppWorkerParametersのCreateNewThread; DedicatedWorkerThread::create
- WorkerThread.cppWorkerThread::createThread; createGlobalScope; evaluateScriptIfNecessary
- SWServerWorker.cppworkerThreadMode
13入力を受け、操作の意味を伝える
冒頭では自然に「ボタンを押す」と書いたが、座標だけを受け取っても、どの要素に届くべき入力なのかは分からない。
ポインター入力では、画面上の位置から対象を調べるヒットテストが関わる。
WebCoreのEventHandlerには、その結果を使って処理する経路がある。
対象が決まった後には、DOMのイベント配送がある。 祖先側から辿るキャプチャー、対象での処理、条件に応じたバブリングを区別する。 ヒットテストとイベントの伝播は、同じ一つの操作ではない。 また、キーボード操作など、座標の判定から始まらない入力もある。
図14の右側は、別の入口だ。
支援技術が必要とするのは、「ここに青い文字がある」という画素だけではない。
「これはボタンで、名前は読む、押す操作ができる」という意味だ。
アクセシビリティAPIを通して、役割、名前、状態などをプラットフォームへ公開する。
WebCoreのAXObjectCacheは、そのためのオブジェクトや更新を扱う。
この構造はDOMの全ノードのコピーではなく、公開する意味に応じて組み立てられる。 図8で三つ目の木を別にした理由もここにある。 プラットフォームのAPIや公開範囲にも差があるため、模式図を特定OSの実際のツリーとして扱うことはできない。
Webを書く側にも、この違いは関係する。
見た目をボタンらしくすることと、ボタンとして意味や操作を提供することは別の仕事だ。
例でbutton要素を使ったのは、文書の構造に操作の意味を持たせるためでもある。
なお、イベントの既定動作を止めるpreventDefault()と、伝播を止めるstopPropagation()も、それぞれ別の役割を持つ。
この節の出典とコードの入口
- EventHandler.cpphandleMousePressEvent; MouseEventWithHitTestResults; handleMousePressEventSingleClick
- DOM StandardTrees; Shadow trees; Dispatching events; Mutation observers
- Core Accessibility API Mappings 1.2Mapping WAI-ARIA to Accessibility APIs; Exposing attributes; State and property mapping
- AXObjectCache.cppgetOrCreate; Accessibility::initializeRoleMap; modalとaria-hiddenの処理
14応答、ページ状態、データを使い直す
一度読んだページをもう一度開くとき、何を使い直せるだろう。 「キャッシュ」という一語でまとめると、HTTPの応答と、動いていたページそのものが混ざりやすい。 まず、保存する対象で三つを分けよう。
HTTPキャッシュは、HTTPの規則に従ってレスポンスを再利用する仕組みだ。 Cache APIは、スクリプトからRequestとResponseの組を保存し、照合して取り出す仕組みだ。 BackForwardCacheは、戻る、進む操作で再利用するページ状態を保持する仕組みだ。 最後のものは、HTMLのレスポンスだけを保存するキャッシュとは異なる。
ブラウザーがHTTPの規則に従って再利用
スクリプトが保存と照合を扱う
戻る、進む操作で適格なら復元
図15の下にあるService Workerは、制御対象のページなどの取得処理に関与できる。
FetchEventに対して、Cache APIで見つけたレスポンスを返すことも、通信することも、自分でResponseを生成することもできる。
ただし、登録、スコープ、有効化、制御状態などの条件があり、すべての通信が必ずService Workerを通るわけではない。
名前にWorkerとあっても、前節の集計用Dedicated Workerとは利用目的が異なる。 Service Workerはイベントに応じて起動され、アイドル時に終了されうる。 永続的に動き続けるバックグラウンドプロセスが得られるAPIとして設計すると、寿命の見込みを誤る。
読書記録そのものを保存したければ、localStorageやIndexedDBなどの領域も関係する。
WebKitのNetworkプロセスにあるNetworkStorageManagerは、サイトの保存領域、容量制限、削除などを管理する。
sessionStorageには同じオリジンでもページごとに異なる領域を持ちうる性質があり、名前に「保存」と付く仕組みを同一視はできない。
保存したデータにも無期限の保持保証はない。 容量や削除方針などが関わり、BackForwardCacheにも利用できるかの判定がある。 戻る操作が速かった理由を調べるときは、応答を再利用したのか、ページ状態を復元したのかを分けると、観測の意味が明確になる。
この節の出典とコードの入口
- NetworkResourceLoader.cppstartRequest; canUseCache; retrieveCacheEntry; startNetworkLoad; didReceiveResponse
- Service WorkersLifetime; FetchEvent; Handle Fetch; Cache interface
- BackForwardCache.cppcanCacheLocalFrame; canCacheFrame; BackForwardCache::add; take
- NetworkStorageManager.cpporigin単位の管理; quota; eviction; IPCの受信処理
- StorageHierarchy; Notes
15文書と権限の境界を守る
アプリのコードと、ネットワークから来たページのコードには、与えてよい権限の違いがある。 WebKitではWebContentを制限されたサンドボックス内で動かし、ファイルやカメラなどへの操作には権限の仲介が関わる。 具体的な制限はOSやポートで変わるが、ページのコードへアプリと同じ自由を与える構成ではない。
さらに、Webの文書同士にも境界がある。 オリジンは、通常はスキーム、ホスト、ポートの組で決まる。 同じホストでもHTTPとHTTPSは異なるし、同じドメイン配下でもサブドメインが変われば異なるオリジンになる。 同じオリジンの範囲を基準に、別の文書のDOMなどへのアクセスを制限する。
https://shop.example.com:443スキーム + ホスト + ポートパスが違ってもオリジンは同じ
同じexample.com配下でもホストが違う
図16の上段と下段は、異なる境界を示している。 サイトとオリジンも同義ではなく、同じサイトに属するサブドメインが別オリジンになる場合がある。 サイトの正確な定義は使う仕様や文脈にも注意が必要だ。 そして、オリジンの違いがそのままOSプロセスの違いになるとは限らない。
Site Isolationの実装では、別プロセスに文書を置くフレームをRemoteFrameで表す構成がある。
参照コミットのSiteIsolationEnabledはunstableで、上書き可能な既定値がfalseと宣言されている。
図16の下段は有効な場合の考え方であり、利用中のSafariの実効設定を確認した図ではない。
ソースの構造、設定の宣言、動いている製品の状態を分けて読む必要がある。
ほかの制御にも、それぞれの仕事がある。 CORSはオリジンを跨ぐレスポンスの共有を制御する仕組みで、エラーになったからといって必ず要求自体が未送信とは限らない。 CSPは文書が読み込むものや実行するものなどに制約を与える。 サンドボックス、同一オリジンの境界、CORS、CSPを一つの壁としてまとめると、何を制限しているのかが分からなくなる。
プライバシーのためには、第三者の保存データやキャッシュを第一者サイトごとに分けるpartitioningなども使われる。 そのため、データをどの単位で保存し、どこから使えるかには、APIの基本機能だけでなく追跡防止の方針も関わる。 いずれの仕組みも、単独ですべての問題をなくすものとしては扱えない。
この節の出典とコードの入口
- WebKit2 — WebKit's Multi-Process ArchitectureOverview
- HTML Standard — BrowsersOrigins; Sites
- Fetch StandardHTTP fetch; CORS protocol; CORS-preflight fetch
- Content Security Policy Level 3Introduction; Fetch Directives; script-src
- Site Isolation有効・無効の場合のframe図; RemoteFrame; BrowsingContextGroup
- UnifiedWebPreferences.yamlSiteIsolationEnabled; UseGPUProcessForCanvasRenderingEnabled; UseGPUProcessForDOMRenderingEnabled
- Tracking Prevention in WebKitTerminology; Partitioned Third-Party Storage; Intelligent Tracking Prevention
16動画とCanvasも、ページに参加する
ページに動画を一つ置くと、四角形と文字だけの話では足りなくなる。
videoはDOMの要素であり、ページ内で場所を持つ。
一方、メディアを読み込んで再生する機能は、別の部品へつながる。
WebCoreのHTMLMediaElementはMediaPlayerを作り、MediaPlayerは再生バックエンドを選んで読み込みを進める。
バックエンドとは、共通の窓口の後ろで環境に応じた実処理を担当する実装だ。
Apple向けにはAVFoundationの実装があり、WebKitGTKとWPEではGStreamerを利用する。
すべての環境で両方を同時に使うという意味ではない。
図17の分岐が、再生とページの描画が出会う場所だ。
通常のpaint経路では、RenderVideo::paintReplacedから動画要素のpaintを経て、MediaPlayerの描画へつながる。
一方、加速合成などの条件が揃うとソフトウェアpaintを省く分岐があり、プレイヤーのプラットフォームレイヤーへつなぐ接点もある。
したがって「動画の全フレームを、毎回ほかのDOMと同じpaintで描く」とは限らない。
ポスター表示、フルスクリーン、スナップショットでも条件が変わる。
Canvasも、要素の配置と、その中身の描き方を分けて考えられる。 Canvas 2Dでは、スクリプトから矩形、パス、文字、画像などを描く。 描いた図形一つひとつがHTML要素としてDOMに追加されるわけではない。 WebGLやWebGPUは、さらに別の描画やGPU利用のAPIを提供する。
音声にはAudioContext、カメラなどの取得にはMediaDevicesという入口もある。
ここでも、ソースにAPIの定義があること、実行するページにAPIが公開されること、利用者の許可を得ること、実際に処理が成功することは別の段階だ。
WebGPUやMediaDevicesには安全な文脈であることなどの条件があり、コーデックやハードウェアも環境によって異なる。
GPUプロセスにメディア関連の実装があることからも、その名前を「3D描画専用」と読むべきでないことが分かる。
この節の出典とコードの入口
- HTMLMediaElement.cppMediaPlayer::create; platformLayer
- MediaPlayer.cpploadWithNextMediaEngine
- RenderVideo.cpppaintReplaced; accelerated renderingの条件分岐
- HTMLVideoElement.cppHTMLVideoElement::paint
- MediaPlayerPrivateAVFoundation.hENABLE(VIDEO) && USE(AVFOUNDATION); MediaPlayerPrivateAVFoundation
- Multimedia — WebKitGTK and WPE WebKit冒頭のGStreamerの用途
- CanvasRenderingContext2D.idlCanvasRenderingContext2D; CanvasRect; CanvasText; CanvasDrawImage
- GPU.idlEnabledBySetting=WebGPUEnabled; SecureContext; requestAdapter
- AudioContext.idlAudioContext; suspend; resume; createMediaElementSource; createMediaStreamSource
- MediaDevices.idlSecureContext; enumerateDevices; getUserMedia; getDisplayMedia
- WebGLRenderingContext.idlConditional=WEBGL; EnabledBySetting=WebGLEnabled; WebGLRenderingContextBase
17不要になったオブジェクトを片づける
読み込み、文書、スクリプト、描画を動かす間には、多数のオブジェクトが作られる。 必要がなくなったものをいつ片づけるかも、エンジンの仕事だ。 ただし、WebKit全体を一種類の回収方式で管理しているわけではない。
JSCには**GC(ガベージコレクション)**があり、JavaScriptのオブジェクトを管理する。 回収の判断では、出発点となるルートから参照を辿れるかが中心になる。 画面に表示されているかどうかだけでは決まらない。
const savedButton = document.querySelector("button");
savedButton.remove();
// 文書から外した後も、この変数から要素へアクセスできる。
図18の例では、文書からボタンを取り外しても、それを指す変数が残っている。 「DOMから消した」と「もう使えない」は同じではないし、「すぐにメモリーを回収した」とも限らない。 実際の寿命にはJavaScriptラッパーとC++オブジェクトの関係も含まれる。
WebCoreの多くのC++オブジェクトでは参照カウントを使う。
所有する参照を数え、その規則に従って寿命を管理する方式だ。
コードにはRef、RefPtr、unique_ptr、弱参照などが現れ、所有の有無やnullを取りうるかなどを区別している。
すべてのオブジェクトが同じ型で管理されるわけではない。
この二つの世界をつなぐバインディングには、ラッパーや参照先の寿命を調整する仕組みがある。 GCと参照カウントを別々に説明するだけで、その接続まで説明し終えたことにはならない。 さらにbmallocなどのアロケーターが担うのは、使うメモリー領域の確保であり、「何が不要か」を判定するGCそのものとは違う。 部品の名前へ戻ると、それぞれが異なる問いを引き受けていたことが見えてくる。
この節の出典とコードの入口
- Understanding Garbage Collection in JavaScriptCore From Scratch導入; Memory Allocation in JSC; GCの段階的説明
- Memory ManagementOverview; Reference counting; Weak Pointers; Reference Counting of DOM Nodes
- JS Wrappers and IDL FilesOverview; JS Wrapper Lifecycle Management; Opaque Roots
18観測からコードとテストへ進む
最初のボタンへ戻ろう。 URLを開く依頼はアプリ側から始まり、通信で得た入力が文書になる。 CSSと文書から表示に必要な構造を作り、位置を計算して描く。 クリックは対象の要素へ届き、JSCで実行した処理がバインディングを通ってDOMを変更する。 必要な更新が行われ、その結果が画面へ現れる。 ここまでに登場した名前は、この往復のどこを担当するかで結びつけられる。
自分で確かめるときは、まずWeb Inspectorで観測する対象を一つ決めるとよい。 要素とスタイルを調べるなら、HTMLソースと現在のDOMを比べる。 通信を調べるなら、どんなリソースを取得し、どこで待ったかを見る。 動作の重さを調べるなら、タイムラインでJavaScript、レイアウト、描画のどこに時間がかかったかを分ける。 そのページと端末で得た結果は、WebKit全体の性能順位を決める材料とは別だ。
↓
WebCoreのDOM / style / rendering
↓
JSC / loader / NetworkProcess
↓
LayoutTests / JSCテスト / APIテスト
- 課題と修正再現条件とテストを用意
- レビューとEWS変更内容、ビルド、テストを確認
- 上流へ取り込み共有するソースが更新される
- ポートごとのリリース製品に搭載して利用者へ届ける
図19のように観測からコードへ進むと、巨大なソースにも入口ができる。
たとえば、文字内容の変更ならDOMとバインディング、ボックスの変化ならstyleとrendering、応答データならloaderやNetworkProcessへ進める。
関数を見つけた後は、呼び出し元と条件分岐を確認する。
Workerで見たように、共通クラスに分岐があるだけでは、説明したい場面でその分岐を通るとは分からない。
変更を確かめるテストにも種類がある。
LayoutTestsは名前に反してレイアウト専用ではなく、DOMなどのWeb機能も扱う。
JavaScriptCore向けのテスト、Tools/TestWebKitAPIのAPIテスト、共有されたWeb Platform Testsもある。
通常の貢献では、課題と修正、テスト、レビュー、EWSによる確認を経て上流へ取り込む流れになる。
セキュリティ問題の報告には別の非公開の方針がある。
それでも、上流への取り込みが手元の製品への搭載完了を意味するわけではない。 ポートごとの統合とリリースが、その先にある。 WebKitを調べるときは、「どの部品か」「どこで動くか」「何を受け渡すか」「どの条件の話か」を揃えると、名前の一覧が、追いかけられる処理の流れになる。
この節の出典とコードの入口
- Web Inspector ReferenceElements; Console; Network; Sources; Timelines; Storage
- How Web Content Can Affect Power UsageJavaScript; Painting; Layout and Rendering timeline
- TestingLayout Tests; API tests; JavaScript tests
- Getting Started ContributingSubmitting a pull request; Addressing review feedback; Landing Changes
- Security Policy非公開での脆弱性報告と修正・開示方針
- What Are WebKit Ports?upstream ports一覧; リリースはポートごとに行うという説明
- Web Platform Tests IntegrationWPTのimportとexport