🤗

DevToolを使って画面が描画されるまでを追ってみた

2023/02/05に公開

今回は、実際に記述したコードが画面に描画されるまでの過程をDevToolを使って追ってみたいと思います。
今まで、知識としてParseやRenderingなどがあることは知っていても、書いたコードが実際にどんな過程を経て描画されているのか視覚的に見たことはありませんでした。(「やりたいなー」と思っていながらズルズル時間が流れていってしまっていた。。。)
CSS-in-JSとCSS Moduleのperformanceも比較したいなと思ったのですが、まずは一番シンプルなコードで試してみます。
では、早速やってみましょう。

この記事に書いていないこと

  • Performance向上のためのtips
  • DevToolの詳しい使い方
  • CSS-in-JSとCSS Moduleのperformanceの比較
  • ブラウザに画面が描画されるまでの説明
    気になる方はこちらから(少し古いけど)
    How browsers work

シンプルなhtmlを表示させてみる

cssもJavaScriptもない本当にシンプルなhtmlです

html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div>html</div>
  </body>
</html>

Performanceタブから計測を行います。

結果はこちら。Parse HTML0.11msを要しています。

ちなみに、Parse HTMLの後に2つScriptがあるのですが、これは前方からreadystatechangeDOMContentLoadedです。

続いて、Renderingが始まります。

Process Time
Recalculate Style 0.12ms
Event:readystatechange 3μs
Event:load 5μs
Event:pageshow 2μs
Layout 0.22ms
Pre-Paint 47μs

Pre-Paintが初めてみる言葉だったので調べてみました。
https://developer.chrome.com/articles/renderingng-architecture/

Pre-paint: compute property trees and invalidate any existing display lists and GPU texture tiles as appropriate.

既存のDisplay list(実際にピクセルへと描画する時に使用されるもの。Render Treeが元になっている)とGPUで持ってるtextureをいい感じにinvalidateするらしい。
詳しいことは後日調べてみます🙇‍♂️

DevtoolのPerformance panelがRenderingNGのステップに追いついていないためPre-Paintを追加するためのやりとりがありました。
UpdateLayerTreeがPre-Paintになったみたいです。
https://groups.google.com/a/chromium.org/g/devtools-reviews/c/uWrBTSajW7Q

Renderingが終わるとPaintingが始まります。

Process Time
Paint 41μs
Composite Layers 0.18ms

これで一連の流れが終了しました。

ProcessとTimeのまとめ

Process Time
Parse HTML 0.11ms
Event:readystatechange 3μs
Event:DOMContentLoaded 1μs
Recalculate Style 0.12ms
Event:readystatechange 3μs
Event:load 5μs
Event:pageshow 2μs
Layout 0.22ms
Pre-Paint 47μs
Paint 41μs
Composite Layers 0.18ms

次はStyleをあててみましょう。

Styleをあてて表示させてみる

htmlはStyle sheetの読み込みとclass付与以外は変わりません。

html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <div class="test">html</div>
  </body>
</html>
css
.test {
  background: red;
  width: 100px;
  height: 100px;
}

Performanceを測ってみるとEvent:DOMContentLoadedの後にParse Stylesheetが追加されていました。

Process Time
Parse Stylesheet 0.26ms

正にこの部分ですね。
CSS parsing

Rendering以降を見てみるとcssがないときに比べて全体的に時間がかかっていることがわかります。(Event系は省いています。)

Process Time(css有り) Time(css無し)
Recalculate Style 0.30ms 0.12ms
Layout 0.13ms 0.22ms
Pre-Paint 50μs 47μs
Paint 56μs 41μs
Composite Layers 0.18ms 0.18ms

ちなみに、cssは例外を除き左からではなく右から解析されるためセレクタの指定方法によっては遅くなることがあります。以下のコードで試してみます。

html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <div class="test">
      html
      <div class="test-2">htmlの子</div>
    </div>
    <div>シンプルなhtml</div> // これが99個
  </body>
</html>
css
/* 速い */
.test .test-2 {
  background: blue;
  width: 100px;
  height: 100px;
}

/* 遅い */
.test div {
  background: blue;
  width: 100px;
  height: 100px;
}

1.5倍くらい遅くなっていることがわかります。
これは、全てのdivタグを走査し.testを親にもつDOMを探す計算をしているためです。

Process Time(速い) Time(遅い)
Recalculate Style 0.26ms 0.40ms
Layout 0.69ms 1.00ms
Pre-Paint 0.11ms 0.17ms
Paint 0.38ms 0.65ms
Composite Layers 0.24ms 0.45ms

JavaScriptを実行してみる

最後にjavascriptを実行してみます。

html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./index.js"></script>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <div>html</div>
  </body>
</html>
JavaScript
const main = async () => {
  console.log("main");
};

main();

結果はこちら。Evaluate Scriptが表示されました。
赤枠の部分は、実行してるmain関数です。

Process Time
Evaluate Script 0.26ms
main() 16μs

ちなみに、Compile ScriptCompile Codeの違いはこちらです。
Compile Script・・・ブラウザがスクリプトファイルのコードをコンパイル
Compile Code・・・ブラウザが関数のコードをコンパイル
なので、下記のようなファイルだった場合Compile Codeは2回走ります。

function hoge() {
  console.log("hoge");
}

function foo() {
  console.log("foo");
}

hoge();
foo();

次は意図的にParse blockを引き起こしてみます。問題を顕著にしたいためCPUを6x slowdownにし、著しく性能を落とした上で実行してみます。

html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./index.js"></script>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <div>html</div>
  </body>
</html>
JavaScript
const main = () => {
  for (const i in [...Array(1000)]) {
    console.log(i);
  }
};

main();

結果はこちら。
Parse HTMLの中でEvaluate Scriptが実行されていることがわかります。

画像からも分かる通り、Self Time2.50msなのに対し、Total Timeでは58.25msの時間を要しています。

このような事象を回避する方法として「JavaScriptはbodyの最後に追加する」なんてことが解決方法として挙げられます。
どのような結果になるか試してみます。

html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <div>html</div>
    <script src="./index.js"></script>
  </body>
</html>

Parse HTMLの中でのEvaluate Scriptの実行はなくなりました。
しかし、Paintingが終了した後にEvaluate Scriptが実行されているため、JavaScriptのコードによってはParse HTML -> Rendering -> Paintingを再度引き起こすかもしれません。やってみましょう。

document.writeを使ってDOM構造を変化させてみます。

JavaScript
const main = () => {
  for (const i in [...Array(1000)]) {
    console.log(i);
  }
  document.write("<div>main</div>");
};

main();

Evaluate Scriptの中でParse HTMLが実行されRendering -> Paintingが引き起こされました。

document.writeは極端な例ですが、昨今のリッチなUIではanimationも不可欠な要素になってきており、animationの処理によっては同じようにRendering -> Paintingが引き起こされfpsの低下につながり画面のカクつきを起こしてしまうことがあります。

まとめ

今回は、ブラウザに表示されるまでの間にどんな事が起きているのかを実際にDevToolを使って追ってみました。
今まで、テキストの知識としてしか理解できていなかったParseやRenderingなどの各種のフェーズが、実際に記述したコードによってどのように変化するのかを知る良い機会になり解像度が上がりました。
また、このような文献は圧倒的に英語の記事が多いため「英語の勉強しなきゃな・・・」と痛感させられました。。。
次は、実際にCSS-in-JSとCSS Moduleを使ってPerformanceの比較を行ってみたいと思います。
ありがとうございました。

参考文献

https://web.dev/howbrowserswork/#render-tree-construction

https://developer.chrome.com/docs/devtools/performance/

Discussion