ブラウザが画面に描画する工程を確認しよう

目次
  1. 最初はページのHTMLドキュメント
    1. フォントと画像
  2. HTMLの解析の流れ
    1. 上から順番に解析される
    2. リソースがあればダウンロードされる
    3. @font-face で読み込まれるフォントファイル(woff2)
    4. HTMLの解析をブロックするJavaScript
  3. 描画までの動きを確認しよう
    1. DOM(Document Object Model)の構築
    2. CSSOM(CSS Object Model)の構築
    3. スタイル計算(Style Calculation)
    4. レンダーツリーの構築
    5. レイアウト(Layout)
    6. ペイント(Paint)
    7. コンポジット(Compositing)
  4. 描画までの動きの中で各リソースはどのように関わっているか?
    1. CSS
    2. JavaScript
    3. 画像
    4. Webフォント
  5. 再描画の流れを確認しよう
    1. ダウンロードした画像やWebフォントの反映
    2. transform、opacityの変化
    3. 色やエフェクトの変化
    4. サイズの変化
    5. クラスの追加
  6. おわりに

最初はページのHTMLドキュメント

URLを入力してアクセスした際の最初の起点となるのは、対象ページのHTMLドキュメントです。例えば、以下の内容のHTMLだったとして、どのように読み込まれていくか確認していきましょう。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./assets/js/main.js"></script>
    <link
      href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@100..900&display=swap"
      rel="stylesheet"
    />
    <link rel="stylesheet" href="./assets/css/style.css" />
  </head>
  <body>
    <h1>Hello World</h1>
    <p>画像1</p>
    <img src="./assets/img/adam-kool-ndN00KmbJ1c-unsplash.jpg" alt="" />
    <p>画像2</p>
    <img src="./assets/img/urban-vintage-78A265wPiO4-unsplash.jpg" alt="" />
    <p>画像3</p>
    <img src="./assets/img/v2osk-1Z2niiBPg5A-unsplash.jpg" alt="" />
  </body>
</html>
body {
  margin: 0;
  padding: 20px;
  font-family: "Noto Sans JP", sans-serif;
}

img {
  max-width: 100%;
  height: auto;
}

実際にアクセスしてみると、Chromeのデベロッパーツールのネットワークを見ると、一番上にそのページのHTMLドキュメントが来ていることが分かるかと思います。
※ VS Codeの拡張機能「Live Server」でアクセスしています。

フォントと画像

Noto Sans Japanese – Google Fonts

https://unsplash.com/ja/%E5%86%99%E7%9C%9F/%E6%98%BC%E9%96%93%E3%81%AE%E9%9D%92%E7%A9%BA%E3%81%AE%E4%B8%8B%E3%81%A7%E3%83%AD%E3%83%83%E3%82%AD%E3%83%BC%E5%B1%B1%E8%84%88%E8%BF%91%E3%81%8F%E3%81%AE%E6%9C%A8%E3%80%85%E3%81%AE%E3%83%A9%E3%83%B3%E3%83%89%E3%83%9E%E3%83%BC%E3%82%AF%E5%86%99%E7%9C%9F-ndN00KmbJ1c

https://unsplash.com/ja/%E5%86%99%E7%9C%9F/%E5%A4%AA%E9%99%BD%E3%81%AE%E5%85%89%E3%81%8C%E5%BD%93%E3%81%9F%E3%82%8B%E5%B1%B1%E3%81%AE%E9%A2%A8%E6%99%AF%E5%86%99%E7%9C%9F-78A265wPiO4

https://unsplash.com/ja/%E5%86%99%E7%9C%9F/foggy-mountain-summit-1Z2niiBPg5A

HTMLの解析の流れ

HTMLドキュメントを受け取ったブラウザが、HTMLの中身をどのように解釈していくか確認しておきましょう。

上から順番に解析される

ブラウザがHTMLドキュメントを認識したら、上から順番に解析を始めます。先ほどの例だと、

  1. !DOCTYPEのタグ
  2. htmlの開始タグ
  3. headの開始タグ
  4. meta charset
  5. meta name="viewport"
  6. title

のような順番で、最後の</html>に到達するまで続きます。まずは、上から順番に解析されていくという基本の部分を押さえておいてください。

リソースがあればダウンロードされる

上から順番に解析していく中で、リソースに出会うとダウンロード対象となり、ダウンロードが始まります。

リソースとは、主には以下の4つです。

  • CSS
  • JavaScript
  • 画像
  • フォント

今回の場合は、

  • JavaScript:./assets/js/main.js
  • CSS:https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@100..900&display=swap
  • CSS:./assets/css/style.css
  • 画像:./assets/img/adam-kool-ndN00KmbJ1c-unsplash.jpg
  • 画像:./assets/img/urban-vintage-78A265wPiO4-unsplash.jpg
  • 画像:./assets/img/v2osk-1Z2niiBPg5A-unsplash.jpg

がHTML内に直接記載されています。これが本当にダウンロードされているかChromeのデベロッパーツールから確認してみましょう。

index.htmlから始まり、v2osk-1Z2niiBPg5A-unsplash.jpgまでダウンロードされていることが分かります。v2osk-1Z2niiBPg5A-unsplash.jpg以降に並んでいるのはフォントですが、これはGoogle FontsのCSSから読み込まれています。

@font-face で読み込まれるフォントファイル(woff2)

HTMLで上から順番の流れとは少しそれますが、Webフォントは主にはCSSファイル内の@font-faceを頼りに読み込まれます。具体的にどのタイミングかは、後で紹介している「CSSOM(CSS Object Model)の構築」のタイミングになるのですが、ひとまず今回読み込まれているファイルを見ていきましょう。

Google FontsでNoto Sans JPを使う場合に生成されるURL(https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@100..900&display=swap)がHTMLで読み込まれていますが、こちらにアクセスすると以下のようにたくさんの@font-faceの定義が確認できます。

たくさんありますが、全てが読み込まれているわけではなく、画面で使われている文字と合致するunicode-rangeの範囲のフォントファイル(woff2)だけをダウンロードしています。

ネットワークを見ると、今回は3つのフォントファイル(woff2)がダウンロードされているわけですが、これは画面に表示されている「H」「e」「l」「o」「 」「W」「o」「r」「l」「d」「画」「像」「1」「2」「3」の文字が含まれる範囲でダウンロードされています。

「H」「e」「l」「o」「 」「W」「o」「r」「l」「d」のアルファベットと「1」「2」「3」の半角数字は以下。

@font-face {
  font-family: 'Noto Sans JP';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/notosansjp/v55/-F62fjtqLzI2JPCgQBnw7HFYwQgP-FVthw.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

「画」は以下。

@font-face {
  font-family: 'Noto Sans JP';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/notosansjp/v55/-F62fjtqLzI2JPCgQBnw7HFowwII2lcnk-AFfrgQrvWXpdFg3KXxAMsKMbdN.117.woff2) format('woff2');
  unicode-range: U+4e, U+a0, U+3000, U+300c-300d, U+4e00, U+4e0a, U+4e2d, U+4e8b, U+4eba, U+4f1a, U+5165, U+5168, U+5185, U+51fa, U+5206, U+5229, U+524d, U+52d5, U+5408, U+554f, U+5831, U+5834, U+5927, U+5b9a, U+5e74, U+5f0f, U+60c5, U+65b0, U+65b9, U+6642, U+6700, U+672c, U+682a, U+6b63, U+6c17, U+7121, U+751f, U+7528, U+753b, U+76ee, U+793e, U+884c, U+898b, U+8a18, U+9593, U+95a2, U+ff01, U+ff08-ff09;
}

「像」は以下。

@font-face {
  font-family: 'Noto Sans JP';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/notosansjp/v55/-F62fjtqLzI2JPCgQBnw7HFowwII2lcnk-AFfrgQrvWXpdFg3KXxAMsKMbdN.114.woff2) format('woff2');
  unicode-range: U+3e, U+3005, U+4e0d, U+4e88, U+4ecb, U+4ee3, U+4ef6, U+4fdd, U+4fe1, U+500b, U+50cf, U+5186, U+5316, U+53d7, U+540c, U+544a, U+54e1, U+5728, U+58f2, U+5973, U+5b89, U+5c71, U+5e02, U+5e97, U+5f15, U+5fc3, U+5fdc, U+601d, U+611b, U+611f, U+671f, U+6728, U+6765, U+683c, U+6b21, U+6ce8, U+6d3b, U+6d77, U+7530, U+7740, U+7acb, U+7d50, U+826f, U+8f09, U+8fbc, U+9001, U+9053, U+91ce, U+9762, U+98df;
}

という具合で、今回は使っているテキストが少ないので、3つのフォントファイル(woff2)が使われているということになります。

HTMLの解析をブロックするJavaScript

CSS、画像、フォントは、HTMLの解析と並行してダウンロードが進みますが、JavaScript(scriptタグ)に出会うとJavaScriptのダウンロードと実行が終わるまでHTML解析が止まります。

試しに重たい処理を入れてみましょう。時間がかかる処理はChatGPT先生に考えていただきました。実行が終わるまで3秒かかる処理です。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./assets/js/main.js"></script>
    <link
      href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@100..900&display=swap"
      rel="stylesheet"
    />
    <link rel="stylesheet" href="./assets/css/style.css" />
  </head>
  <body>
    <h1>Hello World</h1>
    <p>画像1</p>
    <img src="./assets/img/adam-kool-ndN00KmbJ1c-unsplash.jpg" alt="" />
    <p>画像2</p>
    <img src="./assets/img/urban-vintage-78A265wPiO4-unsplash.jpg" alt="" />
    <p>画像3</p>
    <img src="./assets/img/v2osk-1Z2niiBPg5A-unsplash.jpg" alt="" />
  </body>
</html>
console.log("JavaScript の読み込み開始");

const start = performance.now();

// HTMLパースをブロックして 3秒間 JavaScript を実行し続ける
const endTime = Date.now() + 3000;
while (Date.now() < endTime) {
  // CPUを占有してHTMLパースを止める
}

console.log("JavaScript 実行完了(パースが再開されます)");
console.log(`実行時間: ${(performance.now() - start).toFixed(2)} ms`);

アクセスした様子です。表示されるまでに、3秒以上かかっていることが確認できます。

このように、JavaScript(scriptタグ)は、HTMLの解析をブロックするということを押さえておいてください。

ちなみに、ネットワーク上を確認すると、main.jsのscriptタグでHTMLの解析が止まるはずなのに以降のリソースをブラウザが認識しているように見えますが、「Speculative Parser(投機的解析)」という仕組みのようです。

メインスレッドから分離された投機的解析に対応しています。これは、スクリプトがダウンロードされて実行されている間、先に解析を進めます。 HTML パーサーは、ストリーム内で先に見つかるスクリプト、スタイルシート、画像の投機的な読み込みを開始し、 HTML ツリー構築アルゴリズムを投機的に実行します。

https://developer.mozilla.org/ja/docs/Glossary/Speculative_parsing

「Speculative Parser(投機的解析)」はサブのスレッドのようなイメージで、メインのスレッドはmain.jsの実行で止まっているという動きになります。

描画までの動きを確認しよう

ここまでで、「ブラウザはHTMLドキュメントの上から順番に解析していること」と「リソースが登場したらダウンロード対象としてダウンロードすること」を確認しました。

箇条書きにすると以下の通りです。

  1. HTMLドキュメントのダウンロード
  2. HTMLの解析
    • CSSのダウンロード
    • JavaScriptのダウンロード
    • 画像のダウンロード
    • Webフォントのダウンロード

ここから、さらに進んで、描画までの動きを確認していきましょう。
全体としては、以下のような流れになります。
※ Webフォントの位置も調整しました。
※ CSSでダウンロードされる画像も追加しました。

  1. HTMLドキュメントのダウンロード
  2. HTMLの解析
    • CSSのダウンロード
    • JavaScriptのダウンロード
    • 画像のダウンロード
  3. DOM(Document Object Model)の構築
  4. CSSOM(CSS Object Model)の構築
    • Webフォントのダウンロード
    • 画像のダウンロード
  5. スタイル計算(Style Calculation)
  6. レンダーツリーの構築
  7. レイアウト(Layout)
  8. ペイント(Paint)
  9. コンポジット(Compositing)

流れとしては、基本的には前の工程が終わるのを待って次という流れです。ただし、CSSOMの構築のキッカケとなるのはDOMの構築ではなく「CSSのダウンロードが終わったら」というタイミングになります。

また、スタイル計算は、DOMとCSSOMが揃うことが条件となります。

この辺りの前提条件だけ整理しておきましょう!

DOM(Document Object Model)の構築

HTMLの解析が終わると、HTMLタグの入れ子関係をもとに要素ノードのツリー構造(DOMツリー) が作られます。DOMは、ブラウザが「何がページに存在するか」を理解するための構造です。

HTMLの解析が終わってから、DOMの構築が始まります。

CSSOM(CSS Object Model)の構築

次に、ダウンロードしたCSSファイルを解析し、セレクターとプロパティ・値の対応関係をツリー構造として整理します。

これが CSSOMツリー(CSS Object Model)です。DOMが「構造」、CSSOMが「見た目のルール」に相当します。

CSSファイルがダウンロードされてから、CSSOMの解析が始まります。

スタイル計算(Style Calculation)

DOMとCSSOMが揃うと、ブラウザは両者を突き合わせて各要素に最終的に適用されるスタイルを決定します。

この段階で「詳細度」や「継承」、「初期値」などを考慮して、すべての要素に具体的な色・サイズ・フォントなどが確定します。

レンダーツリーの構築

スタイル計算が終わったら、実際に描画対象となるノードだけを抽出し、DOMツリーとCSSOMツリーを組み合わせて レンダーツリー を構築します。

描画する対象だけで構築されるため、たとえばdisplay: noneの要素などはここで除外されます。

レイアウト(Layout)

レンダーツリーをもとに、各要素の位置やサイズ(ボックスモデル)を計算します。

ここでブラウザはページ全体の配置を決定します。ちなみに、レイアウトを再構築することを「リフロー(Reflow)」と呼びます。

ペイント(Paint)

レイアウト結果に基づき、背景・テキスト・ボーダー・シャドウなどのピクセル情報を描画します。実際に「見えるもの」が生成されるのがこの工程です。

コンポジット(Compositing)

最後に、複数のレイヤーに分けて描画された要素を、GPUで合成(コンポジット)し、最終的な画面として表示します。コンポジットの表現になるCSSプロパティは概ね決まっていて「transform」「opacity」「filter」などが対象となります。

描画までの動きの中で各リソースはどのように関わっているか?

HTMLの解析の中で見つかったリソースは、JavaScript以外は並行してダウンロードされていきます。先ほどの流れの中で、CSSはCSSOMの構築の元の情報になるという紹介をしました。CSSも含めて他のJavaScriptや画像、フォントがどのような関係になるか確認しておきましょう。

CSS

CSSはHTMLの解析と並行してダウンロードされますが、CSSOM(CSS Object Model)の構築のタイミングで使います。順番としてはCSSのダウンロードを待って、CSSOMの構築になり、CSSOMの構築が終わったらスタイルの計算に移ります。

つまり、CSSのダウンロードが遅くなると、以降の工程の開始が遅くなるということを押さえておいて下さい。

JavaScript

JavaScriptは、単にscriptタグで読み込んでいるだけだと、「HTMLの解析をブロックするJavaScript」で紹介した通りHTMLの解析をブロックして実行する動きとなります。もう少し正確にいうと、JavaScriptファイルをダウンロード時間も含めてブロックする動きとなってしまいます。

つまり何も指定しないと、JavaScriptファイルのダウンロード時間 + 実行時間の間で描画の処理が止まるということになります。

このブロックを防ぐために、scriptタグには「async」と「defer」の属性が用意されています。「async」と「defer」を指定すると、実行時に描画の処理を止めることは変わりませんが、JavaScriptファイルのダウンロード時間はHTMLの解析と並行して進んでくれるようになります。

それぞれの動きも確認しておきましょう。

async

書き方としては、以下のような書き方となります。asyncがある場合は、scriptタグが出てきた時点でHTMLの解析を止めることはなく、HTMLの解析と並行してダウンロードが進み、ダウンロード完了後に即実行されるという動きになります。ダウンロードの時間はブロックされないため、ブロックされる期間が大幅に減ります。

<script src="./assets/js/main.js" async></script>

ダウンロード後に即実行されるので、asyncを指定したscriptタグが複数ある場合は、上から順番にJavaScriptが実行されず、ダウンロードされた順番に実行されるため、HTMLで書いた順番は保証されません。

defer

書き方としては、以下のような書き方となります。deferがある場合も、scriptタグが出てきた時点でHTMLの解析を止めることはなく、HTMLの解析と並行してダウンロードが進みます。そして、実行タイミングがダウンロード後に即実行ではなく、DOMの構築が完了した後に実行されます。

<script src="./assets/js/main.js" defer></script>

deferが複数ある場合は、deferで指定したscript間での順番は保証されます。

また、deferは、DOMの構築後の実行が決まっています。一方でasyncは実行タイミングの保証がありません。例えば、asyncの実行タイミングが描画直前になり、DOMの追加が発生すると、DOMの構築から工程が戻ることになるので、asyncで実行する内容に工程の手戻りが発生する処理がないか注意すると良いかもしれません。

画像

画像はブラウザが画面に描画するまでの各工程と並行してダウンロードされます。どこで組み合わさるかというと、「ペイント」のタイミングです。

ネットワークが重たかったりすると、テキストやレイアウトは表示されているけど、画像が少しずつ表示される画面を見たことある方もいると思いますが、これはダウンロードできたタイミングで画像が後から表示されているからですね。

このように、配置する枠のサイズが決まる「レイアウト」のタイミングでも画像の情報はブラウザに伝わっていないため、後から読み込まれた画像によって画面の幅や高さがガクッと動く場合があります。

画像は、タイミングと優先順位を決めることが可能です。

画像のダウンロードのタイミングを決められる

imgタグにはloading属性があります。これは、画像のダウンロードのタイミングを決めることができる属性です。

  • <img src="" loading="eager" ・・・見つかり次第すぐにダウンロード開始
  • <img src="" loading="lazy" ・・・見つかってもダウンロードは保留。ブラウザの画面上で表示が近づけばダウンロード開始

ただし、LCP対象(メインビジュアルなど)の画像には loading="lazy" を使わないようにしましょう。

画像のダウンロードのタイミングを早くする

画像のダウンロードは、基本はHTMLを解析する中で「見つかった順番」にダウンロード対象となる動きですが、早くダウンロードしてほしい場合は「プリロード」を使うことによって可能です。

「プリロード」は以下のようにlinkタグを使ってheadタグの中に書きます。

<head>
  <!-- 中略 -->
  <link rel="preload" as="image" href="./assets/img/adam-kool-ndN00KmbJ1c-unsplash.jpg">
  <!-- 中略 -->
</head>

こうすることで、ブラウザがheadタグを解析する中でlink rel="preload" as="image"を見つけたら、そのタイミングで先にダウンロード対象にしてくれるようになります。

画像のダウンロードの優先順位を決められる

画像のダウンロードは「優先順位」を決めることができます。優先的に取得して欲しい画像を明示するようなイメージです。

ネットワークのリソース(帯域幅)は限られています。優先順位を上げることで、限られた帯域幅の中から優先的にリソースを使ってダウンロードしてくれるようになります。

優先順位は、重要度を表すfetchpriorityという属性によって指定が可能です。画像のダウンロードに関係する、imgタグやプリロードで利用することができます。値としては、highとlowが指定可能で、先ほどのrel="preload" as="image"と組み合わせることができます。

<head>
  <!-- 中略 -->
  <link rel="preload" as="image" href="./assets/img/adam-kool-ndN00KmbJ1c-unsplash.jpg" fetchpriority="high">
  <!-- 中略 -->
</head>
<body>
  <!-- 中略 -->
  <img src="./assets/img/adam-kool-ndN00KmbJ1c-unsplash.jpg" loading="eager" fetchpriority="high">
  <!-- 中略 -->
</body>

こうすることで、「adam-kool-ndN00KmbJ1c-unsplash.jpg」を優先的に多くのネットワークリソースを使ってダウンロードしてくれるようになります。

上記の例では、imgタグにもfetchpriorityが設定できるということで指定していますが、同じ画像でpreloadの方にfetchpriorityを指定している場合はimgタグにはなくても良いかもしれません。

Webフォント

Webフォントも画像と似ていて、ブラウザが画面に描画するまでの各工程と並行してダウンロードされます。主にはCSSに記述された、@font-faceにフォントファイルへのパスが指定してあることが多いです。

Google Fontsだと例えば以下のような指定です。

@font-face {
  font-family: 'Noto Sans JP';
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url(https://fonts.gstatic.com/s/notosansjp/v55/-F62fjtqLzI2JPCgQBnw7HFYwQgP-FVthw.woff2) format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

つまり、フォントファイルのパスに辿り着く順番としては、

  • CSSファイル
  • フォントファイル

の順番となります。

描画の中の工程でいうと、CSSOMを構築する中で「@font-faceルール」と「Web フォントの指定」が見つかり、実際のダウンロードはDOMとCSSOMが組み合わさったレンダーツリーが構築されるタイミングです。

実際にWebフォントを利用していることが分かったタイミングで、対象の「太さ」と「種類」などからフォントのダウンロード開始します。ダウンロードは他の描画の工程と並行して動きます。

初回のペイントまでにフォントのダウンロードが間に合えば初回のペイントで描画しますが、間に合わないフォントは再度ペイントという形で描画されます。

初回のペイントに間に合わない場合の代替フォント

Webフォントのダウンロードが初回のペイントに間に合わない場合ですが、4つのパターンがあります。これは@font-faceのfont-displayに指定する値です。

  • swap・・・初回はデバイスフォントで文字表示 → 準備ができたらWebフォントに置き換え
  • block・・・初回は文字を表示しない → 準備ができたらWebフォントに置き換え
  • fallback・・・初回は少し待ってWebフォントがまだならデバイスフォントで文字表示 → 準備ができたらWebフォントに置き換え
  • optional・・・初回に間に合わない場合は表示しない

Google Fontsではswapがデフォルトの指定ですが、変更することは可能です。

再描画の流れを確認しよう

ブラウザが画面に描画する流れを確認してきました。これは初回の描画の流れになります。

  1. HTMLドキュメントのダウンロード
  2. HTMLの解析
    • CSSのダウンロード
    • JavaScriptのダウンロード
    • 画像のダウンロード
  3. DOM(Document Object Model)の構築
  4. CSSOM(CSS Object Model)の構築
    • Webフォントのダウンロード
    • 画像のダウンロード
  5. スタイル計算(Style Calculation)
  6. レンダーツリーの構築
  7. レイアウト(Layout)
  8. ペイント(Paint)
  9. コンポジット(Compositing)

では、2回目以降はどうなるか、というところを確認していきましょう。何を描画するか、あるいは、何を変更するかによって「どこの工程から再開するか」が変わってきます。

そして、描画の流れは一方通行です。例えば、HTML要素を追加した場合に、「DOM(Document Object Model)の構築」だけが行われるということではなく、「スタイル計算」「レンダーツリーの構築」「レイアウト」「ペイント」「コンポジット」といった、以降の工程もすべて実施されるということを押さえておいて下さい。

ダウンロードした画像やWebフォントの反映

先ほども確認したように、画像やフォントは描画の工程に並行する形でダウンロードが進みます。そしてダウンロードされたタイミングで画面に反映されるという流れです。

この画面に反映される際に影響がある工程は以下の2つです。

  • ペイント(Paint)
  • コンポジット(Compositing)

フォントや画像は「レイアウトが既に確定している要素の中で、視覚的に置き換わる」処理です。
そのため、位置計算(Layout)は不要で、描画(Paint)と合成(Composite)のみが再実行されます。

transform、opacityの変化

CSSプロパティでいうところの、transformやopacityといった表現を変化させる場合は、コンポジットからの工程が再度実施されます。

  • コンポジット(Compositing)

色やエフェクトの変化

CSSプロパティでいうところの、background-color、color、background-image、box-shadow、text-shadowといった表現を変化させる場合は、ペイントからの工程が再度実施されます。

  • ペイント(Paint)
  • コンポジット(Compositing)

「見た目を塗り直す必要がある」変更(background, color, box-shadow など)は Paint が再実行されます。Layout までは影響しないため、配置は維持したまま、見た目だけを再描画します。

サイズの変化

CSSプロパティでいうところの、width, height, margin, padding, border-width, display, font-size、top、right、bottom、leftといった表現を変化させる場合は、レイアウトからの工程が再度実施されます。

  • レイアウト(Layout)
  • ペイント(Paint)
  • コンポジット(Compositing)

サイズが変わると再度枠を決めるためのレイアウトの計算が必要になります。この再度レイアウトを行うことを「リフロー(Reflow)」と呼びます。リフローは、「要素の位置・大きさの再計算」が必要なときに起こります。

強制リフローとは?

どの再描画する際も、実施するタイミングはブラウザが他の処理との兼ね合いも考えて「良きタイミング」で実施しています。

ただ、強制リフローが発生すると、ブラウザがどういった処理を行っているか関係なく、強制的にレイアウトの再計算処理に移ります。

どうやったら起こるかというと、レイアウトに関する変更処理からレイアウトの計算が終わるまでの間に、JavaScriptでの「現在の位置・サイズ・スタイル」を読み取る操作をした場合です。具体的には、offsetWidth、offsetHeight、offsetTop、clientWidth、scrollTop、getComputedStyle()など。

実際の例としては、以下のように、書き換えた後にすぐにレイアウトを確認しにいくと強制リフローとなります。

box.style.width = "300px";

// すぐに offsetWidth を読む → 強制リフロー発生!
console.log(box.offsetWidth);

すぐに確認しにいくのではなく、ブラウザの良きタイミングで読みにいくようにすると強制リフローは防ぐことができます。

box.style.width = "300px";

// レイアウト再計算をまとめる
requestAnimationFrame(() => {
  console.log(box.offsetWidth); // レイアウト確定後に読む
});

クラスの追加

JavaScriptで動的にクラスをつけ外しする実装があるかと思います。ボタンをクリックしたら、button class="is-active"になり、もう一度タップするとbutton class=""が外れるような場合のイメージです。

この時に、初回に構築されたDOMとCSSOMが「更新」されて、「スタイル計算」から再度実施されます。

  • スタイル計算(Style Calculation)
  • レンダーツリーの「更新」
  • レイアウト(Layout)
  • ペイント(Paint)
  • コンポジット(Compositing)

レンダーツリーは必要な部分だけ「更新」され、レイアウト、ペイント、コンポジットは変化する内容に応じて実施されます。

ちなみに、スタイルを再計算することを、「Style Recalculation(リカリキュレーション)」と呼ばれたりします。

おわりに

ブラウザが画面に描画する工程を確認してきました。次は、その工程を踏まえてページの読み込みスピードを改善するための方法を確認していきましょう!

PageSpeed Insightsの点数が悪かった時の考え方と改善方法

WordPressでお悩みの方はお気軽にご相談ください。

ちょっとしたお困り事からサイト全体のリニューアルのようなご依頼まで、お悩みに応じて柔軟に対応いたします。

この記事を書いた人

はにわまん

HPcode代表。300件以上のWordPress案件を対応してきました。ちょっとしたWordPressのカスタマイズからブロックテーマでのリニューアルまで。更新できるWordPressサイトが好きです。