React ソースコード読み - ReactDOM.render
はじめに
ReactとはUIを記述するためのjsライブラリです。
Reactは仮想DOMと呼ばれる内部オブジェクトによってアプリケーションの状態を保持します。
アプリケーションのデータが変更された時、Reactは仮想DOMを丸ごと作り直して、 必要な箇所のみ実際のDOMを更新します(この処理は差分検出処理と呼ばれています)。
そのため開発者は自分でDOMを操作することなく、宣言的にUIを記述することができます。
この差分検出処理などの内部処理がソースコード上でどう実装されているのか、気になって調べたものをブログにまとめていきます。
この記事では ReactDOM.render
の実装を追っていきます。
バージョン
この記事で扱うReactのバージョンは16.13.1です。
参考文献
この記事を書くにあたって、ソースコードの他に以下の資料を参考にしました。
- https://ja.reactjs.org/docs/getting-started.html
- https://github.com/acdlite/react-fiber-architecture
- https://blog.ag-grid.com/inside-fiber-an-in-depth-overview-of-the-new-reconciliation-algorithm-in-react/
- https://medium.com/react-in-depth/in-depth-explanation-of-state-and-props-update-in-react-51ab94563311
- https://blog.logrocket.com/deep-dive-into-react-fiber-internals/
なお、以下の内容は独学で調べたもので、またかなり端折っているので正確でない部分が多々あると思います。間違いなどありましたらご指摘いただければ幸いです。
概要
ReactDOM.render
は以下のように用いられ、初期表示処理のエントリーポイントとなります。
ReactDOM.render( <App />, document.getElementById('root') );
ReactDOM.render
は以下のように定義されます。
ReactDOM.render(element, container[, callback])
1個目の引数でコンポーネント、2個目の引数でDOMノードを受け取ります。
このメソッドが呼ばれると、Reactは以下のような流れでコンポーネントからDOMを生成します。
React element -> Fiber node -> DOM node
DOMノードの生成が終わると、それをcontainerに挿入します。
React element
React elementはコンポーネントを表す内部オブジェクトで、通常jsxから生成されます。
Reactでは普通jsxを使ってコンポーネントを記述しますが、jsxはコンパイル時に React.createElement
の呼び出しに変換されます。
React.createElement
は React element
を返す関数です。
React elementはプレーンなjsオブジェクトで、コンポーネントのタグ名やpropsを保持します。
{ $$typeof: REACT_ELEMENT_TYPE, type: type, key: key, ref: ref, props: props, _owner: owner, }
これによってReactは(DOMを触ることなく)アプリケーションを記述します。
Fiber
React elementはrender時にただちにDOMノードへと変換されるのではなく、 Fiberノードと呼ばれる別の内部オブジェクトに変換されます。
FiberノードはReact elementと同様にコンポーネントを表すjsオブジェクトですが、もっと多くの情報を含んでいます。
- 子・兄弟・親ノードへの参照
- すでにrenderされたprops
- これからrenderされるprops
- 後で実行する副作用
以下が Fiberノードの定義 です。
function FiberNode( tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode, ) { // Instance this.tag = tag; this.key = key; this.elementType = null; this.type = null; this.stateNode = null; // Fiber this.return = null; this.child = null; this.sibling = null; this.index = 0; this.ref = null; this.pendingProps = pendingProps; this.memoizedProps = null; this.updateQueue = null; this.memoizedState = null; this.dependencies = null; this.mode = mode; // Effects this.effectTag = NoEffect; this.nextEffect = null; this.firstEffect = null; this.lastEffect = null; this.expirationTime = NoWork; this.childExpirationTime = NoWork; this.alternate = null; // ... }
コンポーネントと同様に、Fiberノードは木を構成します(ここではFiberツリーと呼びます)。
Fiberの設計資料の中で、Fiberは「コンポーネントのインスタンス、また仮想的なスタックフレーム」に対応すると説明されています。
コンポーネントのインスタンスはともかく、スタックフレームというのはどこから出てきたのでしょうか?
ここからは推測混じりになってしまうのですが、Fiberによる実装を導入する前、Reactはコンポーネントのツリーを再帰的、かつ同期的に走査していたというのが背景にあると思います。
再帰的に関数を呼び出すと、スタックフレームがコールスタックに積まれていきます。jsはスタックが空でないとイベントキューを処理しないため、複雑なツリーを処理するとUIがブロックされてしまい、ユーザー体験がよくありませんでした。
一方、Fiberでは木はwhileループによって走査され、各コンポーネントのインスタンスはヒープに存在します。また、Fiberでは優先度の低い処理を中断して後回しにするという風に非同期的に処理を行うことができます。
このようなFiberと以前の実装の比較が念頭にあって、スタックにないけどスタックフレームと同様に処理の一単位になっている、という意味を込めて「仮想的なスタックフレーム」と言っているのだと考えています。
なお、この辺りの背景についてはこの記事がとても参考になります。
実装に移ると、Reactはrender時に二種類のFiberツリーを保持します。
一つ目がcurrentと呼ばれるFiberツリーで、現在のアプリケーションの状態を表します。
二つ目がworkInProgressと呼ばれるFiberツリーで、これから描画されるアプリケーションの状態を表します。
render時に、Reactは基本的に全てのコンポーネントに対してFiberノードを生成して、workInProgressを一から作り直します(この処理はDOMを作り直すのに比べて軽量だとされています)。
そして、currentとworkInProgressを比較して、差分がある(DOMを更新するという副作用が必要である)部分についてのみ、副作用を実行します。
render
上述したように、render時にReactはFiberツリーの生成とDOMの更新を行います。
この処理は以下の2ステップに分けて行われます。
- renderフェーズ
- 新しい状態を反映したFiberツリー(workInProgress)を生成します。
- commitフェーズ
- workInProgressの中で、副作用が必要なノードについてのみ、副作用を実行します。
ソースコード
サンプルアプリケーションを見ながら、ソースコード上の処理を追っていきます。
サンプルアプリケーション
以下のカウンターアプリケーションを例として処理を追っていきます。
import React, { useState } from 'react'; import ReactDOM from 'react-dom'; function App() { const [count, setCount] = useState(0); const handleClick = () => { setCount(count + 1); } return ( <div> <p>You clicked {count} times</p> <button onClick={handleClick}>Click me</button> </div> ); } ReactDOM.render( <App />, document.getElementById('root') );
App └──div ├──p └──button
ReactDOM.render
初期表示時の処理を追っていきます。
1. renderフェーズ
Fiberツリーを生成し、DOMノードに紐付けます(これをマウントと呼びます)。
legacyRenderSubtreeIntoContainer
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(...) // このfiberRootは後でperformSyncWorkOnRootに渡されます。 fiberRoot = root._internalRoot // ... updateContainer(children, fiberRoot, parentComponent, callback);
- サンプルアプリケーションの場合、containerはid=rootのDOMノードです。
なので、マウントされたツリーは以下のようにしてブラウザのコンソールから眺めることができます。
document.getElementById('root')._reactRootContainer
legacyCreateRootFromDOMContainer
Fiberツリーのルートノードを作ります。
const uninitializedFiber = createHostRootFiber(tag); // ... initializeUpdateQueue(uninitializedFiber);
initializeUpdateQueue
- updateQueueを初期化します。
- updateQueueはupdateオブジェクトのキューで、updateオブジェクトには後でrenderするReact elementが入ります。
- この時点では、updateQueueのフィールドはすべてnullになっています。
- updateQueueを初期化します。
-
const update = createUpdate(...) // renderに渡されたelementをupdate.payloadに代入します。 update.payload = {element}; // ... enqueueUpdate(...) scheduleWork(...)
-
// current.updateQueue.shared.pending.payloadの中に、renderに渡されたelementが入ります。 // これが後のprocessUpdateQueueで処理されます。 current.updateQueue.shared.pending = update
-
これは
updateContainer
内のscheduleWork
の実体です。// このrootはlegacyCreateRootFromDOMContainerで生成されたfiberRoot.currentです。 performSyncWorkOnRoot(root)
-
prepareFreshStack(...) // ... do { try { workLoopSync(); break; } catch (thrownValue) { handleError(root, thrownValue); } } while (true) // ... // このfinishedWorkが後のcommitフェーズでDOMに挿入されます。 // current.alternateはworkInProgressを参照しています。 root.finishedWork = (root.current.alternate: any);
-
workInProgressを生成します。
// workInProgressはモジュール内のトップレベルスコープに属します。 workInProgress = createWorkInProgress(...)
-
currentを元に、workInProgressを作成します。
workInProgress = createFiber(...) workInProgress.updateQueue = current.updateQueue // currentとworkInProgressはalternateによってお互いを参照します。 current.alternate = workInProgress workInProgress.alternate = current // ... return workInProgress
-
while (workInProgress !== null) { workInProgress = performUnitOfWork(workInProgress); }
- elementツリーの各ノードに対して繰り返しperformUnitOfWorkを実行します。
- performUnitOfWorkはworkInProgressの子を返します。
- このようにReactはwhileループによって木を走査しています。
- 概要のところでチラッと触れた部分です。
- elementツリーの各ノードに対して繰り返しperformUnitOfWorkを実行します。
-
// このnextはunitOfWorkの子です。 next = beginWork(current, unitOfWork, renderExpirationTime); // ... if (next === null) { next = completeUnitOfWork(unitOfWork); } // ... return next
- workInProgressに対してbeginWorkを実行します。
- beginWorkは、渡されたコンポーネントについて必要な処理を行った後、そのコンポーネントの子を返します。
葉まで到達した場合は、completeUnitOfWork(後述)を実行します。
beginWork
(root)- 最初にrootについてbeginWorkを実行します。
beginWorkの中には長いswitch文があって、workInProgressの種類ごとに異なるupdate関数を実行します。
ここで実行されるのはupdateHostRootです。
// ... return updateHostRoot(...)
-
processUpdateQueue(...) //... reconcileChildren(..., workInProgress, ...) return workInProgress.child
-
enqueueUpdate内で生成したupdateオブジェクトを処理していきます。
baseQueue = queue.shared.pending
baseQueue内のupdateを処理します。
- ただし、優先度の低いupdateはスキップします。
updateを元にして、stateを更新します。
newState = getStateFromUpdate(..., update, ...)
-
- ここでは、updateContainer内でpayloadに代入された
{element}
をそのまま返しています。
- ここでは、updateContainer内でpayloadに代入された
workInProgressに更新したstateを代入します。
workInProgress.memoizedState = newState
-
- React elementからfiberを生成し、workInProgress.childに代入します。
- このelementは、元々はupdateHostRootの中で代入されたworkInProgress.memoizedState.elementです。
この際にfiberのeffectTagに副作用をセットします。
- effectTagとは後で実行する副作用の種類を表します。
これによって、後のcommitフェーズでDOMに挿入される(副作用を持つ)ようになります。
workInProgress.child = reconcileChildFibers(workInProgress, ...)
-
-
-
// このpendingPropsが次のbeginWorkで処理される。 // elementは、もともとworkInProgress.memoizedState.elementだったもの。 pendingProps = element.props
-
-
// 後で実行する副作用のタグをセットします。 newFiber.effectTag = Placement;
-
- React elementからfiberを生成し、workInProgress.childに代入します。
beginWork
(App)rootのworkInProgress.childはAppなので、次はAppに対してbeginWorkが実行されます。
// ... // 引数にworkInProgress.typeを渡す(typeにはAppが入っている) return mountIndeterminateComponent(...)
-
クラスコンポーネントまたは関数コンポーネントからReact elementを生成し、workInProgress.childに代入します。
// このpendingPropsは前のbeginWorkのなかで設定されたもの。 const props = workInProgress.pendingProps; // ... // 引数に渡されたComponentとpropsからreact elementを作る。 value = renderWithHooks(..., Component, props, ...) // ... // react elementからfiberを生成し、workInProgress.childに代入する。 reconcileChildren(..., value, ...) // ... return workInProgress.child;
-
// childrenにはreact elementが入る children = Component(props, ...) ... return children
beginWork
(div)Appの次はdivに対して実行されます。
div以降のノードに対するbeginWorkは省略します。
// ... return updateHostComponent(...)
-
// 今までと同様に、workInProgress.childを更新する。 reconcileChildren(..., workInProgress, ...) return workInProgress.child
-
completeWork
もしすでにDOMノードがあれば、差分だけ更新します。
- 初期表示時なので、ここの処理は実行されません。
-
- 新旧のpropsを比較し、差分があればworkInProgressのupdateQueueに追加します。
これは導入で紹介した差分検出処理の一つだと思います。
const updatePayload = prepareUpdate(...); workInProgress.updateQueue = (updatePayload: any); // ... markUpdate(workInProgress);
-
diffProperties
- 新旧のpropsの差分を返す。
- oldPropsはもともとcurrent.memoizedPropsだったもの。
- newPropsはもともとworkInProgress.pendingPropsだったもの。
markUpdate
effectTagをセットします。
workInProgress.effectTag |= Update;
すでにDOMノードがなければ、新しく生成します。
初期表示時なので、こっちの処理が実行されます。
// DOMノードを生成 instance = createInstance(...) // ... // workInProgressに代入 workInProgress.stateNode = instance
-
- ブラウザ・ネイティブなどの実行環境(ホストと呼ばれます)によって異なる関数が渡されます。以下はブラウザ(ReactDOM)の実装です。
createElement
document.createElement
を実行する。- これは見慣れたDOMのAPIです。
2. commitフェーズ
renderフェーズで作られたworkInProgressを元に、副作用を実行していきます。
この時、FiberノードのnextEffectを辿ることで、副作用が必要なFiberノードのみを走査します。
finishSyncRender
commitRoot
-
// root.finishedWorkはrenderフェーズのperformSyncWorkOnRootで代入されたものです。 const finishedWork = root.finishedWork; // ... let firstEffect; firstEffect = finishedWork; // ... nextEffect = firstEffect;
-
- effectTagの種類ごとに異なる関数を呼び出します。
ここでは以下の関数が呼ばれます。
commitPlacement(nextEffect);
-
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
insertOrAppendPlacementNodeIntoContainer
const stateNode = isHost ? node.stateNode : node.stateNode.instance; appendChildToContainer(parent, stateNode);
-
ここではじめてDOMノードが挿入されます。
parentNode = container; // このchildはroot.finishedWork.stateNodeだったものです。 parentNode.appendChild(child);
state変更時
せっかくなので、ボタンをクリックしてstateが変更した時の処理も追っていきます(力尽きたので、流れだけ、、、)
カウンターのボタンをクリックすると、handleClickが実行されて、countがインクリメントされます。
const handleClick = () => { setCount(count + 1); }
更新されたcountは、 dispatchAction
にactionという引数として渡されます。
dispatchActionは渡されたactionを元にupdateオブジェクトを生成し、引数の queue.pending
に代入します。
var update = {...}; queue.pending = update;
なお、このqueueは最初に useState
を実行した際に、 mountState
の中でdispatchActionに対してbindされたものです。
dispatchActionの最後では scheduleUpdateOnFiber
が呼ばれて、いろいろあって最終的にperformSyncWorkOnLoopが呼ばれます。
この関数はReactDOM.render内で呼ばれた時と同じように、rootからworkInProgressを更新していきます。
rootを処理してAppの番が来ると、 updateFunctionComponent
が呼ばれて、Appコンポーネントの関数が実行されます。
Appコンポーネントの中では、まず useState
を実行します。
const [count, setCount] = useState(0);
useState
を実行すると、なんやかんやあって updateReducer
が呼ばれます。
// この中にはdispatchActionで生成されたupdateオブジェクトが入ります。 let pendingQueue = queue.pending;
この後、初期表示時のprocessUpdateQueueと同じ要領でupdateオブジェクトから新しいstateを作って更新します。
// newStateにはインクリメントされた値が入ります。 hook.memoizedState = newState; // ... // 以下がuseStateの返り値になります。 // hook.memoizedStateがcountに、dispatchがsetCountに代入されます。 return [hook.memoizedState, dispatch];
あとは、新しくなったcountを元にReact elementを返すだけです。
// 以下のcountはインクリメントされた値になっています const handleClick = () => { setCount(count + 1); } return ( <div> <p>You clicked {count} times</p> <button onClick={handleClick}>Click me</button> </div> );
以上でAppコンポーネントの処理は終わりで、残りの流れは初期表示の際と同じです。
それぞれのFiberノードに対してcompleteWorkが呼ばれます。
テキストの更新があったpタグについては、 updateHostText
が呼ばれます。
この関数は以下のようにテキストが等しいか判定し、差分がある場合 markUpdate
を呼びます。
if (oldText !== newText) { workInProgress.stateNode = createTextInstance(...); markUpdate(workInProgress); }
初期表示のところでみたように、 markUpdate
が呼ばれるとeffectTagがセットされます。
workInProgress.effectTag |= Update;
これによって、この後のcommitフェーズでDOMが更新されます。
『CPUの創りかた』読んだ
読んだ理由
低レイヤーの知識が全然なかったので読んだ
感想
- すごい面白かった。コンピュータって意外と単純だなといい意味でイメージが崩れた。
- 例えばメモリはスイッチをたくさん組み合わせて作ってて(メモリの1bitはスイッチのON/OFFで表現する)、そんな単純でいいんだという驚きがあった。
- レジスタとかの具体的なイメージが持ててよかった。
メモ
- プログラムの流れ
- メモリ
- 役割: プログラムの保持
- イメージ: スイッチの集まり
- 部品: DIPスイッチ
- アドレスデコーダ
- 役割: メモリの特定の部分を読む
- イメージ: スイッチ
- 部品: 74HC154
- レジスタ
- 役割: 計算のメモ
- イメージ: フリップフロップ(入力を一時的に記憶する)
- 部品: 74HC161
- ALU
- 役割: 算術演算を行う
- イメージ: 論理ゲートの組み合わせ
- 部品: 74HC283
- プログラムカウンタ
- 役割: 現在のアドレスを指し示す
- イメージ: クロックごとに変化するレジスタ
- 部品: 74HC161
- 命令デコーダ
- 役割: 機械語を各回路の入力に変換する
- イメージ: 論理ゲートの組み合わせ
- 部品: 74HC32, 74HC10
- データセレクタ
- 役割: ALUの入力に使うレジスタを決定する
- イメージ: スイッチ
- 部品: 74HC153
『Webページ速度改善ガイド』 4章
レンダリング処理の基礎知識
4.1 スムーズなUIとスムーズでないUIの違い
動きの滑らかさ
- スクロール操作・アニメーションが発生した時、レンダリング処理が発生する
- この処理に時間がかかると、スムーズでないUIになる
- RAILモデル(1.4節)によると、1フレームあたりの処理を10msec以内に収めるべき
UIの応答速度
- ユーザーのアクションに対する応答を早くする
- 応答の例: ボタンをクリック -> ボタンの見た目を変更
- RAILモデルによると、ユーザーアクションが発生してから100msec以内に応答するべき
- ユーザーのアクションに対する応答を早くする
4.2 レンダリング処理の基本
FPS(frames per second)
- 1秒間に何回フレームを更新したか
- レンダリング処理がスムーズかの基準
- Webでは60fpsを目標とする
- 一般的なディスプレイのリフレッシュレートが60Hzのため。
- 1回のフレーム更新にかけられる時間は16.7msec
1フレームの中の処理
レンダリング処理の最適化
レンダリング処理のパイプライン
- スクリプトの処理
- スタイルの処理
- レイアウトの処理
- 画面の各要素がどのような位置関係で配置されるかを決める
- offsetTop, offsetWidthなどのプロパティを参照すると、レイアウト処理を誘発して重くなる
- ペイントの実行
- 算出されたスタイル・レイアウトに基づいてディスプレイに表示する
Webアニメーション
- DOMアニメーション
- jsでDOM要素のstyleを連続的に更新
- CSS Transitions/Animations
- Web Animations
- DOMアニメーション
4.3 レンダリング処理の調査と計測
感想
- 2,3章のネットワーク処理とは異なり、普段意識しない部分だったので勉強になった
- Long Tasks APIは活用できそう
Webページ速度改善ガイド 3章
『超速! Webページ速度改善ガイド』のメモ
超速! Webページ速度改善ガイド ──使いやすさは「速さ」から始まる:書籍案内|技術評論社
3章 ネットワーク処理の調査と改善
3.1 サイズの大きいリソースの調査と改善
まずサイズが大きいリソースをブラウザの開発者ツールで特定する。サイズが大きいリソースは以下の理由により速度を悪化させる
- ダウンロードに時間がかかる
- (HTTP/1の場合)同時接続数の一つを占有するので、同じドメインの他のリソースのダウンロードを阻害する
リソースのサイズを小さくするには以下の方法がある
- テキストリソース(HTML, CSS, JS)の最小化
- uglify-esなどのツールを利用して、不要な空白を削ったり変数名を置き換えることでサイズを小さくする
- gulp, webpackなどのタスクランナーを使うと良い
- 配信時の圧縮
- 画像の最適化
- 適切な解像度で画像をダウンロードする
- 小さい領域に高解像度の画像を表示しても意味がない
- 詳しくは8章
- 適切な解像度で画像をダウンロードする
- JSのサイズ削減
- 不要なライブラリを削除する
- ページごとなどの粒度でファイルを分割する
3.2 待機時間が長いリクエストの調査と改善
リクエストを送信してからレスポンスが返ってくるまでの待機時間が長いリクエストを特定する。 開発者ツールでWaitingやTTFB(Time To First Byte)の時間を見れば良い。
改善方法は以下の通り。
- サーバサイドの最適化(APIの呼び出しを最適化するなど?)
- リソースの先読み
- PreLoad, Resource Hintsの利用(9章)
- キャッシュ
- サーバサイドのキャッシュ
- Service Worker, Cache APIの利用(9章)
- CDNの利用
3.3 リクエスト数の調査と改善
リクエスト数が多いと速度に悪影響がある。特にHTTP/1ではリクエストのオーバーヘッドが大きく、同時接続数の上限もある。
リクエスト数が多い場合、不要なリクエストを削る。以下が典型的なパターンである
また、静的リソースは結合することでリクエスト数を減らすことができる。 ただし、HTTP/2ではリソースを結合しすぎないほうが良い場合がある。リソースのサイズが小さいほうが早く評価を始められるため。
3.4 クリティカルレンダリングパスの調査と改善
クリティカルレンダリングパスを最適化するため、レンダーツリーの構築を早くする。 すなわちブラウザのDOMContentLoadedイベントを早くする。
前提
- ほとんどの場合(scriptタグがある場合)、ブラウザはCSSOMツリーの構築を待ってレンダーツリーを構築する。
- スクリプトを実行している間、DOMツリーの構築はブロックされる。
- スクリプトによりDOMが変えられる可能性があるため
- CSSを非同期でロードすると、スタイルが適用されていないコンテンツが表示される可能性がある。
以上の前提に基づく改善方法
- CSSは最も優先してダウンロードし、JSのロードは遅らせる。
- ATFに関わるCSSのみ
<link rel="stylesheet">
でロードし、それ以外は遅延ロードするのも良い
- ATFに関わるCSSのみ
- メインコンテンツに影響しないスクリプトは非同期で実行する
<script defer>
や<script async>
を利用する- 以下のようにjsからscriptタグを生成する方法もあるが、プリロードスキャンの対象にならないため
<script async>
の方が望ましい。
const script = document.createElement('script'); script.src = 'hoge.js'; document.head.appendChild(script);
3.5 Webフォントに関わるリソースの調査と改善
興味がないので省略
感想など
- HTMLのminifyはあまり一般的ではないと思う。今どれくらい行われているか調査したい
- CSSをATFとそれ以外で分けるのは運用が非常に難しそう。
Webページ速度改善ガイド 1-2章
『超速! Webページ速度改善ガイド』のメモ
超速! Webページ速度改善ガイド ──使いやすさは「速さ」から始まる:書籍案内|技術評論社
1章 Webページの速度
1.1 Webページの速度とは何か
- ページロード:ナビゲーション開始(URLの入力など)からページが表示されるまで
- ランタイム:(表示後の)インタラクションにかかる時間 例:スクロール、アニメーション
1.2 Webページの速度の重要性
速度改善は以下の観点で重要
- ビジネス:速度改善により収益増を見込める
- デバイス:モバイル環境はマシンスペック、回線ともにPCより不利 モバイルでも快適なページにする必要がある
- コンテンツ:高速化により、よりリッチなコンテンツを提供できる 例:音声・動画
1.3 Webフロントエンド高速化のポイント
ページ速度へのインパクトが大きいのはフロントエンド
フロントエンドの速度改善のポイントは以下の3つに分けられる
- ネットワーク:配信されるリソースをダウンロードする処理
- レンダリング:リソースを表示する処理
- スクリプト:javascriptを実行する処理
1.4 Webフロントエンド高速化の取り組み方
Measure, Don't Guess(推測するな、計測せよ)
やみくもに実装を変更するのではなく、計測 -> 分析 -> 改善 の順番で進める
普段の開発環境だけでなく、低スペックな環境などいろいろな環境でチェックする
応答時間の基準
- 認知機能の研究:ロードは1秒、ランタイムは0.1秒
- RAILモデル:Googleの人たちが提唱している応答時間の基準 RAIL モデルでパフォーマンスを計測する | Web | Google Developers
2章 ネットワーク処理の基礎知識
2.1 ページロードの速度を左右するネットワーク処理
ネットワーク処理の速度に影響を与える要素
- リソースの大きさ
- リクエストの回数
- 通信経路の距離
HTTP/2の特徴
- 通信の多重化:1つのTCPコネクション内で複数のリソースをリクエストできる
- ヘッダ圧縮:ヘッダがテキストでなく、HPACKというアルゴリズムで圧縮されている
- リソースの優先度制御:クライアントの制御する優先度によって配信を制御する
- サーバプッシュによる先読み:クライアントからのリクエストを待たずにリソースを送信する
2.2 ネットワーク処理の基本
ネットワーク処理の最適化のポイント
- 転送量を小さくする
- 転送回数を少なくする
- 転送距離を短くする
配信されるリソース
- テキスト
- HTML, CSS, JSなど
- 空白・改行を削るなどのいわゆるminify処理を行う
- 画像
- 圧縮する
- リソースの50%をしめるとゆわれる
- webフォント
- 日本語の場合重い
クリティカルレンダリングパス
- HTMLのダウンロードと評価
- サブリソースのダウンロードと評価
- レンダーツリーの構築とレンダリング
2が終わるとDOMツリー・CSSOMツリーが構築され、レンダーツリーが構築される
3以降は開発者が関与する部分はほとんどない、2までを速くする
2.3 ネットワーク処理の調査と計測
モニタリングには以下の2種類ある
- 合成(synthetic)モニタリング
- 計測用の環境を用意し、定期的に繰り返し計測する
- メリット
- 計測の揺らぎを抑えられる
- 詳細なレポートを得られる -> 改善の手がかりになる
- デメリット
- 実際のユーザーの環境とずれる
- リアルユーザーモニタリング
- クライアントサイドで時間を計測し、集計サーバに投げて収集する
- メリット、デメリットは合成モニタリングの逆
メリットデメリットがあるので両方モニタリングできると良い
モニタリングのサービス
- WebPagetest
- SpeedCurve
- New Relic
- Calibre
Timing API
ブラウザのいろいろな処理時間をjsのAPIから取得できるように策定が進められている
2.4 プロダクトに応じた指標作り
間接的な指標
- ブラウザイベント
- DOMContentLoaded: DOMツリーの構築完了
- Load: サブリソースのダウンロードと評価完了
- リクエスト数・ファイルサイズ
直接的な指標
- First Paint (Start Render): ページの何らかが表示され始めた時
- First Contentful Paint: コンテンツ(画像・テキストなど)が表示され始めた時
- First Paintと共に、Paint Timingで仕様が策定されている
- First Meaningful Paint: ユーザーにとって意味のある状態になった時
- アプリケーションによって異なる曖昧さがある
- Time To Interactive: ユーザーが操作可能になった時
- Speed Index: Above the fold(スクロールせずに閲覧可能な領域)における表示速度のスコア
感想など
- TCPコネクションの同時接続数どう決まってるか調べる
- 優先度の制御、どれくらい効果があるのか気になる
- すでにブラウザでかなり制御されてそう
- マイクロサービスだと先読みの制御が難しそう
macでchromiumをビルドしてxcodeでデバッグ
新しいmacbook proを買ってchromiumをビルドしたので作業を記録
※chromiumの開発者はubuntuでビルドすることが多いみたいです
環境
2.2 GHz core i7, メモリ 16GB
デバッグビルドするとソース・バイナリ合わせて100GBくらいになるので空き容量が必要
ソースコードの取得
公式ドキュメント通りにやればOK
Checking out and building Chromium for Mac
ソースコードの取得。数時間かかる
$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git $ export PATH="$PATH:/path/to/depot_tools" $ mkdir chromium && cd chromium $ git config --global core.precomposeUnicode true $ fetch chromium $ cd src
ビルド
chromiumはninjaというビルドツールが使われている
ここではビルドはninjaで行い、デバッグはxcodeで行う
gnはgoogleで使われているmakeみたいなツールらしい
$ gn gen out/gn --ide=xcode
設定ファイルを書く。
$ vim out/gn/args.gn $ cat out/gn/args.gn # Build arguments go here. # See "gn args <out_dir> --list" for available build arguments. dcheck_always_on = true is_component_build = true
ビルド。数時間かかる
$ autoninja -C out/gn chrome
エラー1
... ../../third_party/blink/renderer/platform/shared_buffer.h:38:10: fatal error: 'third_party/blink/renderer/platform/wtf/std_lib_extras.h' file not found #include "third_party/blink/renderer/platform/wtf/std_lib_extras.h" ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1 error generated. [27541/36317] CXX obj/third_party/blink/renderer/bindings/core/v8/bindings_core_impl/v8_custom_element_value_setter.o ninja: build stopped: subcommand failed.
対処
$ gclient sync
エラー2
... fatal error: cannot open file '/Users/knagashi/work/chromium/src/out/gn/../../third_party/skia/include/core/SkPostConfig.h': Too many open files 1 error generated.
対処
$ ulimit -n 10000
ビルドに成功すると実行ファイルができる
$ out/gn/Chromium.app/Contents/MacOS/Chromium
(aliasをchromiumに設定)
デバッグ
https://zhuanlan.zhihu.com/p/24911872
(この人のブログはすごく面白い。わざわざmacでビルドしようと思ったのはこの人に影響されたせい)
chromiumのファイル数はとても多いので、インデックスをしないように設定する
ちなみにインデックスすると2時間くらいかかる。
$ defaults write com.apple.dt.XCode IDEIndexDisable 1
xcodeを開く。このとき、"Automatically Create Schemes"を選択する
$ open out/gn/all.xcworkspace
ブレークポイントを設定する。
なんでもいいけど、DocumentLoader::DataReceivedとゆうメソッドに設定する。
このメソッドはHTTPレスポンスを処理し、HTMLのパーサーに渡す
chromiumをターミナルから起動する。
このとき、プロセスのPIDが表示される(以下の39954)ので、あとでxcodeからアタッチする
$ chromium --renderer-startup-dialog ... [39954:775:0108/232111.152967:ERROR:content_switches_internal.cc(110)] Renderer (39954) paused waiting for debugger to attach. Send SIGUSR1 to unpause. ...
xcodeの上のメニューから Debug > Attach to Processを選択し、さっき起動したプロセスを選ぶ
起動したchromiumで何かしらのサイトを開く。
twitter.comを開いた結果
dataとゆう引数にtwitterのHTMLが入っていることがわかる