React ソースコード読み - ReactDOM.render

はじめに

ReactとはUIを記述するためのjsライブラリです。

Reactは仮想DOMと呼ばれる内部オブジェクトによってアプリケーションの状態を保持します。

アプリケーションのデータが変更された時、Reactは仮想DOMを丸ごと作り直して、 必要な箇所のみ実際のDOMを更新します(この処理は差分検出処理と呼ばれています)。

そのため開発者は自分でDOMを操作することなく、宣言的にUIを記述することができます。

この差分検出処理などの内部処理がソースコード上でどう実装されているのか、気になって調べたものをブログにまとめていきます。

この記事では ReactDOM.render の実装を追っていきます。

バージョン

この記事で扱うReactのバージョンは16.13.1です。

参考文献

この記事を書くにあたって、ソースコードの他に以下の資料を参考にしました。

なお、以下の内容は独学で調べたもので、またかなり端折っているので正確でない部分が多々あると思います。間違いなどありましたらご指摘いただければ幸いです。

概要

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.createElementReact 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ステップに分けて行われます。

  1. renderフェーズ
    • 新しい状態を反映したFiberツリー(workInProgress)を生成します。
  2. 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コンポーネントは以下のような木構造になっています。

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);
      
    • createHostRootFiber

    • initializeUpdateQueue
      • updateQueueを初期化します。
        • updateQueueはupdateオブジェクトのキューで、updateオブジェクトには後でrenderするReact elementが入ります。
        • この時点では、updateQueueのフィールドはすべてnullになっています。
  • updateContainer

    const update = createUpdate(...)
    // renderに渡されたelementをupdate.payloadに代入します。
    update.payload = {element};
    // ...
    enqueueUpdate(...)
    scheduleWork(...)
    
  • enqueueUpdate

    // current.updateQueue.shared.pending.payloadの中に、renderに渡されたelementが入ります。
    // これが後のprocessUpdateQueueで処理されます。
    current.updateQueue.shared.pending = update
    
  • scheduleUpdateOnFiber

    • これは updateContainer 内の scheduleWork の実体です。

      // このrootはlegacyCreateRootFromDOMContainerで生成されたfiberRoot.currentです。
      performSyncWorkOnRoot(root)
      
  • performSyncWorkOnRoot

    prepareFreshStack(...)
    // ...
    do {
      try {
        workLoopSync();
        break;
      } catch (thrownValue) {
        handleError(root, thrownValue);
      }
    } while (true)
    // ...
    // このfinishedWorkが後のcommitフェーズでDOMに挿入されます。
    // current.alternateはworkInProgressを参照しています。
    root.finishedWork = (root.current.alternate: any);
    
  • prepareFreshStack

    • workInProgressを生成します。

      // workInProgressはモジュール内のトップレベルスコープに属します。
      workInProgress = createWorkInProgress(...)
      
    • createWorkInProgress

      • currentを元に、workInProgressを作成します。

        workInProgress = createFiber(...)
        workInProgress.updateQueue = current.updateQueue
        // currentとworkInProgressはalternateによってお互いを参照します。
        current.alternate = workInProgress
        workInProgress.alternate = current
        // ...
        return workInProgress
        
  • workLoopSync

    while (workInProgress !== null) {
      workInProgress = performUnitOfWork(workInProgress);
    }
    
    • elementツリーの各ノードに対して繰り返しperformUnitOfWorkを実行します。
      • performUnitOfWorkはworkInProgressの子を返します。
    • このようにReactはwhileループによって木を走査しています。
      • 概要のところでチラッと触れた部分です。
  • 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(...)
          
      • updateHostRoot

        processUpdateQueue(...)
        //...
        reconcileChildren(..., workInProgress, ...)
        return workInProgress.child
        
      • processUpdateQueue

        • enqueueUpdate内で生成したupdateオブジェクトを処理していきます。

          baseQueue = queue.shared.pending
          
        • baseQueue内のupdateを処理します。

          • ただし、優先度の低いupdateはスキップします。
        • updateを元にして、stateを更新します。

          newState = getStateFromUpdate(..., update, ...)
          
        • getStateFromUpdate

          • ここでは、updateContainer内でpayloadに代入された {element} をそのまま返しています。
        • workInProgressに更新したstateを代入します。

          workInProgress.memoizedState = newState
          
      • reconcileChildren

        • React elementからfiberを生成し、workInProgress.childに代入します。
          • このelementは、元々はupdateHostRootの中で代入されたworkInProgress.memoizedState.elementです。
        • この際にfiberのeffectTagに副作用をセットします。

          • effectTagとは後で実行する副作用の種類を表します。
          • これによって、後のcommitフェーズでDOMに挿入される(副作用を持つ)ようになります。

            workInProgress.child = reconcileChildFibers(workInProgress, ...)
            
        • reconcileChildFibers

          • reconcileSingleElement

            • createFiberFromElement

              // このpendingPropsが次のbeginWorkで処理される。
              // elementは、もともとworkInProgress.memoizedState.elementだったもの。
              pendingProps = element.props
              
          • placeSingleChild

            // 後で実行する副作用のタグをセットします。
            newFiber.effectTag = Placement;
            
    • beginWork (App)

      • rootのworkInProgress.childはAppなので、次はAppに対してbeginWorkが実行されます。

        // ...
        // 引数にworkInProgress.typeを渡す(typeにはAppが入っている)
        return mountIndeterminateComponent(...)
        
      • 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;
          
      • renderWithHooks

        // childrenにはreact elementが入る
        children = Component(props, ...)
        ...
        return children
        
    • beginWork (div)

      • Appの次はdivに対して実行されます。

        • div以降のノードに対するbeginWorkは省略します。

          // ...
          return updateHostComponent(...)
          
      • updateHostComponent

        // 今までと同様に、workInProgress.childを更新する。
        reconcileChildren(..., workInProgress, ...)
        return workInProgress.child
        
    • completeUnitOfWork

      • completeWork
        • もしすでにDOMノードがあれば、差分だけ更新します。

          • 初期表示時なので、ここの処理は実行されません。
          • updateHostComponent

            • 新旧のpropsを比較し、差分があればworkInProgressのupdateQueueに追加します。
            • これは導入で紹介した差分検出処理の一つだと思います。

              const updatePayload = prepareUpdate(...);
              workInProgress.updateQueue = (updatePayload: any);
              // ...
              markUpdate(workInProgress);
              
          • prepareUpdate

            • diffProperties
              • 新旧のpropsの差分を返す。
              • oldPropsはもともとcurrent.memoizedPropsだったもの。
              • newPropsはもともとworkInProgress.pendingPropsだったもの。
          • markUpdate
            • effectTagをセットします。

              workInProgress.effectTag |= Update;
              
        • すでにDOMノードがなければ、新しく生成します。

          • 初期表示時なので、こっちの処理が実行されます。

            // DOMノードを生成
            instance = createInstance(...)
            // ...
            // workInProgressに代入
            workInProgress.stateNode = instance
            
          • createInstance

            • ブラウザ・ネイティブなどの実行環境(ホストと呼ばれます)によって異なる関数が渡されます。以下はブラウザ(ReactDOM)の実装です。
            • createElement

2. commitフェーズ

renderフェーズで作られたworkInProgressを元に、副作用を実行していきます。

この時、FiberノードのnextEffectを辿ることで、副作用が必要なFiberノードのみを走査します。

  • finishSyncRender
  • commitRoot
  • commitRootImpl

    // root.finishedWorkはrenderフェーズのperformSyncWorkOnRootで代入されたものです。
    const finishedWork = root.finishedWork;
    // ...
    let firstEffect;
    firstEffect = finishedWork;
    // ...
    nextEffect = firstEffect;
    
  • commitMutationEffects

    • effectTagの種類ごとに異なる関数を呼び出します。
    • ここでは以下の関数が呼ばれます。

      commitPlacement(nextEffect);
      
  • commitPlacement

    insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
    
  • insertOrAppendPlacementNodeIntoContainer

    const stateNode = isHost ? node.stateNode : node.stateNode.instance;
    appendChildToContainer(parent, stateNode);
    
  • appendChildToContainer

    • ここではじめて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で表現する)、そんな単純でいいんだという驚きがあった。
  • レジスタとかの具体的なイメージが持ててよかった。

メモ

  • プログラムの流れ
    1. クロックが進む
    2. プログラムカウンタが進む
    3. プログラムカウンタが指すアドレスがアドレスデコーダからメモリに渡る
    4. メモリのアドレスに格納された命令(機械語)が命令デコーダに渡る
    5. 命令デコーダ機械語を各回路(データセレクタレジスタ)の入力に変換する
    6. データセレクタレジスタの値をALUに渡す
    7. ALUの算術演算の出力がレジスタに渡る
  • メモリ
    • 役割: プログラムの保持
    • イメージ: スイッチの集まり
    • 部品: DIPスイッチ
  • アドレスデコーダ
    • 役割: メモリの特定の部分を読む
    • イメージ: スイッチ
    • 部品: 74HC154
  • レジスタ
  • ALU
    • 役割: 算術演算を行う
    • イメージ: 論理ゲートの組み合わせ
    • 部品: 74HC283
  • プログラムカウンタ
    • 役割: 現在のアドレスを指し示す
    • イメージ: クロックごとに変化するレジスタ
    • 部品: 74HC161
  • 命令デコーダ
    • 役割: 機械語を各回路の入力に変換する
    • イメージ: 論理ゲートの組み合わせ
    • 部品: 74HC32, 74HC10
  • データセレクタ
    • 役割: ALUの入力に使うレジスタを決定する
    • イメージ: スイッチ
    • 部品: 74HC153

book.mynavi.jp

『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フレームの中の処理

    • スクリプト、スタイル、レイアウト、ペイント、画像のデコード
    • これらの処理のほとんどがブラウザのメインスレッドで行われる
    • 負荷の高い処理があると、レンダリング処理が遅れてFPSが低下する
  • レンダリング処理の最適化

    1. 1フレーム内の処理の軽減
      • 不要な処理の削除
    2. ブラウザの内部処理による最適化を活かす
  • レンダリング処理のパイプライン

    1. スクリプトの処理
      • 例: HTMLテンプレートの処理、イベント発生後のDOM更新
      • スクリプトはメインスレッドを占有するため、重い処理はレンダリングを遅らせる
      • 以下のような処理は避ける
        • 計算量の大きい処理
        • scrollやmousemoveのような高頻度で発生するイベントで、DOMの更新を行う
    2. スタイルの処理
      • CSSのスタイル情報を適用する
      • どのようなセレクタで指定するかよりも、どのようなプロパティを指定しているかがボトルネックとなる
        • CSS3以降のグラデーション、角丸などのプロパティは重い
    3. レイアウトの処理
      • 画面の各要素がどのような位置関係で配置されるかを決める
      • offsetTop, offsetWidthなどのプロパティを参照すると、レイアウト処理を誘発して重くなる
    4. ペイントの実行
      • 算出されたスタイル・レイアウトに基づいてディスプレイに表示する
  • Webアニメーション

    1. DOMアニメーション
      • jsでDOM要素のstyleを連続的に更新
    2. CSS Transitions/Animations
      • CSSセレクタで捕捉可能な要素の変化をトリガとしてアニメーションを行う
        • CSS Transitions: 変化前・変化後の2点間をアニメーション
        • CSS Animations: 最初と最後だけでなく、中間の状態も定義する
      • スクリプト処理がないので、DOMアニメーションより高性能
    3. Web Animations
      • アニメーションを抽象化したjsのAPI
      • CSS Animationsと同じようなもの
        • CSSで簡単に表現できるなら無理に使う必要はない
        • 複雑な制御が必要ならCSSより良い
  • CompositingによるGPUアクセラレーション

    • Compositing
      • ある要素をGPUの管理下に置き(合成レイヤ)、GPUの中で高速に処理する
    • 有効化
      • will-changeプロパティ
        • 変形・更新する可能性がある別のプロパティ名を指定する
      • CSSハック
        • translateZ(0)など
        • will-changeプロパティの方が良い
    • 副作用
      • compositingにも処理コストがあるので、アニメーションする可能性がある要素にのみ適用する

4.3 レンダリング処理の調査と計測

  • Chrome DevToolsを使い、FPSや各種の処理にかかった時間がわかる

  • Long Tasks API

    • ページのレンダリングに伴うフレームの中で、50msecを超えるフレームに関する情報を取得するAPI
    • リアルユーザーモニタリングの時、エンドユーザーの手元で時間がかかっている処理が把握できる

感想

  • 2,3章のネットワーク処理とは異なり、普段意識しない部分だったので勉強になった
  • Long Tasks APIは活用できそう

Webページ速度改善ガイド 3章

『超速! Webページ速度改善ガイド』のメモ

超速! Webページ速度改善ガイド ──使いやすさは「速さ」から始まる:書籍案内|技術評論社

3章 ネットワーク処理の調査と改善

3.1 サイズの大きいリソースの調査と改善

まずサイズが大きいリソースをブラウザの開発者ツールで特定する。サイズが大きいリソースは以下の理由により速度を悪化させる

  1. ダウンロードに時間がかかる
  2. (HTTP/1の場合)同時接続数の一つを占有するので、同じドメインの他のリソースのダウンロードを阻害する

リソースのサイズを小さくするには以下の方法がある

  • テキストリソース(HTML, CSS, JS)の最小化
    • uglify-esなどのツールを利用して、不要な空白を削ったり変数名を置き換えることでサイズを小さくする
    • gulp, webpackなどのタスクランナーを使うと良い
  • 配信時の圧縮
    • 配信サーバで動的にgzip形式に圧縮して配信する
    • gzip以外にも、圧縮率を高めたzopfli, brotliなどの圧縮アルゴリズムがある
    • brotliは新しく圧縮効率が高い。IE, Android Browser以外の最新ブラウザは対応している
    • css, jsなどの静的コンテンツは、あらかじめ圧縮してサーバに配置すると良い
  • 画像の最適化
    • 適切な解像度で画像をダウンロードする
      • 小さい領域に高解像度の画像を表示しても意味がない
    • 詳しくは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ではリクエストのオーバーヘッドが大きく、同時接続数の上限もある。

リクエスト数が多い場合、不要なリクエストを削る。以下が典型的なパターンである

  • 全てのページで使っているわけではないCSS, JS
  • すでに使ってないソーシャルプラグイン
  • スクロールしないと見えない位置の画像ファイル
    • 遅延ロードする

また、静的リソースは結合することでリクエスト数を減らすことができる。 ただし、HTTP/2ではリソースを結合しすぎないほうが良い場合がある。リソースのサイズが小さいほうが早く評価を始められるため。

3.4 クリティカルレンダリングパスの調査と改善

クリティカルレンダリングパスを最適化するため、レンダーツリーの構築を早くする。 すなわちブラウザのDOMContentLoadedイベントを早くする。

前提

  • ほとんどの場合(scriptタグがある場合)、ブラウザはCSSOMツリーの構築を待ってレンダーツリーを構築する。
  • スクリプトを実行している間、DOMツリーの構築はブロックされる。
  • CSSを非同期でロードすると、スタイルが適用されていないコンテンツが表示される可能性がある。
    • FOUC(Flash of Unstyled Content)とゆう。
    • Webフォントのロードを伴う場合や、body要素でcssを読み込む場合などに起きる

以上の前提に基づく改善方法

  • CSSは最も優先してダウンロードし、JSのロードは遅らせる。
    • ATFに関わるCSSのみ<link rel="stylesheet">でロードし、それ以外は遅延ロードするのも良い
  • メインコンテンツに影響しないスクリプトは非同期で実行する
    • <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つに分けられる

  1. ネットワーク:配信されるリソースをダウンロードする処理
  2. レンダリング:リソースを表示する処理
  3. スクリプトjavascriptを実行する処理

1.4 Webフロントエンド高速化の取り組み方

Measure, Don't Guess(推測するな、計測せよ)

やみくもに実装を変更するのではなく、計測 -> 分析 -> 改善 の順番で進める

普段の開発環境だけでなく、低スペックな環境などいろいろな環境でチェックする

応答時間の基準

2章 ネットワーク処理の基礎知識

2.1 ページロードの速度を左右するネットワーク処理

ネットワーク処理の速度に影響を与える要素

  • リソースの大きさ
  • リクエストの回数
  • 通信経路の距離

HTTP/2の特徴

  1. 通信の多重化:1つのTCPコネクション内で複数のリソースをリクエストできる
  2. ヘッダ圧縮:ヘッダがテキストでなく、HPACKというアルゴリズムで圧縮されている
  3. リソースの優先度制御:クライアントの制御する優先度によって配信を制御する
  4. サーバプッシュによる先読み:クライアントからのリクエストを待たずにリソースを送信する

2.2 ネットワーク処理の基本

ネットワーク処理の最適化のポイント

  1. 転送量を小さくする
  2. 転送回数を少なくする
  3. 転送距離を短くする

配信されるリソース

  • テキスト
    • HTML, CSS, JSなど
    • 空白・改行を削るなどのいわゆるminify処理を行う
  • 画像
    • 圧縮する
    • リソースの50%をしめるとゆわれる
  • webフォント
    • 日本語の場合重い

クリティカルレンダリングパス

  1. HTMLのダウンロードと評価
  2. サブリソースのダウンロードと評価
  3. レンダーツリーの構築とレンダリング

2が終わるとDOMツリー・CSSOMツリーが構築され、レンダーツリーが構築される

3以降は開発者が関与する部分はほとんどない、2までを速くする

2.3 ネットワーク処理の調査と計測

モニタリングには以下の2種類ある

  1. 合成(synthetic)モニタリング
    • 計測用の環境を用意し、定期的に繰り返し計測する
    • メリット
      • 計測の揺らぎを抑えられる
      • 詳細なレポートを得られる -> 改善の手がかりになる
    • デメリット
      • 実際のユーザーの環境とずれる
  2. リアルユーザーモニタリング
    • クライアントサイドで時間を計測し、集計サーバに投げて収集する
    • メリット、デメリットは合成モニタリングの逆

メリットデメリットがあるので両方モニタリングできると良い

モニタリングのサービス

  • 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に設定)

デバッグ

以下のブログを参考にxcodeデバッグする

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のパーサーに渡す

https://chromium.googlesource.com/chromium/src/+/4bbb3621b0854a3a3792f87c24e2c9283e9c7a88/third_party/blink/renderer/core/loader/document_loader.cc#830

f:id:nagashimashi:20190108230426p:plain

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を開いた結果

f:id:nagashimashi:20190108232548p:plain

dataとゆう引数にtwitterのHTMLが入っていることがわかる