lasciva blog

開発して得た知見やwebビジネスのストック

「Webフロントエンド ハイパフォーマンス チューニング」を読んだ

Webフロントエンド ハイパフォーマンス チューニング

Webフロントエンド ハイパフォーマンス チューニング

感想

パフォーマンスチューニングというよりかは、ブラウザがHTMLをどのように処理をして描画されてるのかの理解を深めるために読んだが、めちゃくちゃ勉強になった。
この本は、原理を知って、計測して意味のあるチューニング(全体の1%だとインパクトが少ないから効果が薄いなど)をしようというスタンスで、実践的なところも良かった。
記事等で、パフォーマンスチューニングの話を見聞していたが、「何故か?」の部分までは踏み込めてなかったので、そのような意味でも良かった。
レンダリングエンジン周りはもう少し理解を深めたい。

目次

1. ウェブパフォーマンスとは何か

パフォーマンスの定義

ユーザの様々な振る舞いに対してウェブページが応答を返す速さ
即ち、初期読み込みのみでなく、jsなどによる操作も含む。

ハイブリッドアプリでは、jsが一度レンダリングエンジンに解釈された上で初めてコンパイルされるため、ネイティブアプリより一般に遅くなる。

2. ブラウザのレンダリングの仕組み

デスクトップブラウザのレンダリングエンジン

ブラウザ レンダリングエンジン JavaScriptエンジン
Google Chrome Blink V8
Mozilla Firefox Gecko SpiderMonkey
Safari WebKit Nitro
Internet Explorer Trident Chakra
Microsoft Edge EdgeHTML Chakra
Opera Blink V8
Vivaldi Blink V8(?)

モバイルブラウザのレンダリングエンジン

ブラウザ レンダリングエンジン
Google Chrome Blink
Safari WebKit
UC Browser U3(WebKitベース)
Opera 独自エンジン/Blink
Android Browser WebKit
IE Mobile Trident

上表のように、レンダリングエンジンはWebKit系が主流である。
そのため、パフォーマンスチューニングにおいては、WebKitを抑えるのが重要。

レンダリングの処理の流れは以下の通り。

  1. リソース読み込み(Loading)
  2. JavaScript実行(Scripting)
  3. レイアウトツリー構築(Rendering)
  4. レンダリング結果の描画(Painting)

1. リソース読み込み(Loading)

主な処理の流れは下記の通り。

  1. リソースのダウンロード
  2. リソースのパース
  3. HTMLからDOMツリーを構築
  4. DOMツリーに紐づくCSSや画像などのリソースの取得、読み込み

HTMLファイルの処理の流れ

  1. 字句解析によるトークンのリスト化
  2. 構文解析により構文木の構築
  3. 構文木内にあるJavaScriptを実行しつつDOMツリーの構築

CSSの読み込み

読み込まれたCSSは、レンダリングエンジンによってパースされて CSSOM(CSS Object Model)ツリーに変換される。

2. JavaScript実行(Scripting)

主な処理の流れは下記の通り。

  1. 字句解析
  2. 構文解析
  3. コンパイル
  4. 実行

3. レイアウトツリー構築(Rendering)

主な処理の流れは下記の通り。

  1. スタイルの計算
  2. CSSルールのマッチング処理
    • DOMの要素数 x CSSルールセットの数のマッチング処理を行う
  3. CSSセレクタのマッチング
  4. 適用されるCSSプロパティの算出
    • マッチングしたCSSルールの優先度を計算しながら、適用するルールを算出する
  5. レイアウト
    • 以下のようなものを決める、視覚的なレイアウト情報の計算を行うこと。
      • 要素の大きさ
      • 要素のマージン
      • 要素のパディング
      • 要素の位置
      • 要素のz軸の位置

4. レンダリング結果の描画(Painting)

  1. ペイント
    • 2Dグラフィックエンジン向けの命令を生成する
  2. ラスタライズ
    • 命令を元に、レイヤーごとにピクセルへと描画する
  3. レイヤーの合成

3. チューニングの基礎

「ハイパフォーマンスWebサイト-高速サイトを実現させる14のルール」のベストプラクティス

ベストプラクティスも前提条件を理解していないと、逆効果の可能性もある。

  1. HTTPリクエストを減らす
  2. CDNを使う
  3. Expiresヘッダを設定する
  4. コンポーネントgzipする
  5. スタイルシートは先頭に置く
  6. スクリプトは最後に置く
  7. CSS expressionの使用を控える
  8. JavaScriptCSSは外部ファイル化する
  9. DNSルックアップをへらす
  10. JavaScriptを縮小化する
  11. リダイレクトを避ける
  12. スクリプトを重複させない
  13. ETagの設定を変更する
  14. Ajaxをキャッシュ可能にする

ハイパフォーマンスWebサイト ―高速サイトを実現する14のルール

ハイパフォーマンスWebサイト ―高速サイトを実現する14のルール

目標設定

  1. パフォーマンス指標 RAIL

developers.google.com

項目 基準時間 備考
Response 100ms
Animation 16ms 60FPSを満たすのに必要。
JSの実行時間は6ms以下が望ましい。
Idle 50ms JSの処理とユーザからのアクションは同一のスレッドで行なわれるので、JSの実行時間が長いと良くない。
Load 1000ms

JavaScriptによる測定

Date.now()は精度が低いので、perfomance.now()を使いましょう

パフォーマンス診断ツール

  1. Audits
  2. PageSpeed Insights
  3. Lighthouse

4. リソース読み込みのチューニング

方針

  1. 読み込むリソースの大きさと数を減らす
  2. レンダリングをブロックする読み込みを減らす
  3. ブラウザとサーバ間の遅延を減らす
  4. ブラウザのキャッシュを活用する

JavaScriptの非同期読み込み

  • 同期で読み込むと、CSSファイルの読み込み等をブロックして、パフォーマンスが悪くなる
  • 外部のjsファイルを参照する際には、deferかasyncを指定して非同期に読み込む
    • DOMツリーが構築されてからJavaScriptが実行される
    • defer
      • ファイルの読み込み順が保証される
    • async
      • ファイルの読み込み順が保証されない

CSSの読み込み

  • ブラウザはレンダリング前に、CSSの取得と読み込みを待つ。
    • CSSの当たってないコンテンツが表示されると不快なため。
  • レンダリングのブロックを防ぐために、メディアクエリを設定する。
media属性 説明
screen PC,SPなどのディスプレイ
print 印刷時のみ
(min-width: 920px) ビューポートの横幅が920px以上

CSSスプライトを使って複数の画像をまとめる

f:id:hacking15dog:20181126140642p:plain

リソースの事前読み込み

// DNSプリフェッチ
<link rel="dns-prefetch" href="http://example.com">
// リソースの事前読み込み
<link rel="prefetch" href="./image.gif">
// ウェブページのプレレンダリング
<link rel="prerender" href="//example.com/prerender.html">
// 接続の投機的開始
<link rel="preconnect" href="//example.com">

その他

  1. Gzip圧縮
  2. CDN
  3. ドメインシャーディング
    • DNSの名前解決で逆に遅延になりうるので要注意
  4. ブラウザのキャッシュ
    1. Expiresヘッダー
    2. Cache-Controlヘッダー
    3. Last-Modifiedヘッダー
    4. ETagヘッダー
  5. ServiceWorkerの利用

5. JavaScript実行のチューニング

JavaScriptの実行モデル

  1. UIスレッド
    • JavaScriptは基本的には、UIスレッド上で実行され、複数のスクリプトが並列で動くわけではない
    • レイアウトの計算やレンダリング処理やDOMイベントの発火もこのスレッドで実行される
  2. イベントループと実行キュー
    • 実行キューを順に処理していく。
    • Ajaxなどの非同期処理での待ち時間はブロックせずに他の処理が実行される。
    • alert()関数などは例外でUIスレッドをブロックする

JavaScriptボトルネックを特定する

Chrome DevToolsでJavascriptの実行のプロファイルを取る方法

  1. Performanceパネルによる計測
    • レンダリングエンジンの行うほとんどの処理のプロファイルを取得できる
  2. Memoryパネルによる計測

メモリリークを防ぐ

  1. console.log()もメモリリークの原因
    1. いつでも表示できるように、特殊な参照が付くため。
  2. DOMリーク
    1. 親子などが参照を持つため、DOMツリー全体がメモリリークになる

WeakMapとWeakSet

  • オブジェクトを弱参照で持つことができる

Web Workersの利用

UIスレッドとは別のバックグラウンドのスレッドでJavaScriptのコードを実行できるようになる。

developer.mozilla.org

// 論理コア数を得られる
navigator.hardwareConcurrency

一部のブラウザでのみしかサポートされていないが、推測するライブラリなども公開されている。

github.com

Web Workersで動作するスクリプトでは、下記のオブジェクトが利用できない。

  • DOM要素
  • documentオブジェクト
  • windowオブジェクト
  • parentオブジェクト

  • scrollなどの高頻発に発生するイベントをハンドリングする場合は、一定時間毎に処理を行うようにする

  • モバイル端末でのclickイベント
    • 一部のブラウザでは、clickイベントはレンダリングエンジンによって擬似的に発生する
      • ダブルタップを検知するために、touchendから300ms後にclickイベントを発火する仕組みになっている
      • input要素やa要素のclickイベントも含む。
    • viewportの設定で回避する。

ページ表示状態を確認する

// ページが現在表示されているかどうか(他のタブを開いていないかどうか)
document.hidden

// 可視状態のevent
document.addEventListener('visibilitychnage', function(){})

document.visibilityState // visible, hidden, prerendering:

DocumentFragment

DOM要素に一括で処理できる仕組み。 大量にDOM要素を追加したりするときに、有効的に使える。

IntersectionObserver

あるDOM要素とそのDOM要素の親要素が視覚的に交差しているかどうかを監視できる。

requestAnimationFrame

  • setIntervalなどで指定した場合は、シングルスレッドのために実行時間が保証されない。

WebGL

6. レイアウトツリー構築のチューニング

レイアウトツリー構築の流れ

  1. CSSのマッチング処理
    • 右から左に評価してマッチングするか処理する
  2. DOM要素の位置情報を計算する
  3. レンダリング

レイアウトツリー構築におけるパフォーマンスの計測

高速なCSSセレクタの記述

BEMを用いる

  1. BEM
  2. SMACSS
  3. AMCSS
  4. SUIT CSS

CSSセレクタのマッチング処理を避ける

styleを動的に変えるには、2つ方法があるが、それぞれ用途に応じて使い分ける。

  • styleを直接変更する
    • 保守性は低い
    • CSSセレクタのマッチングが増加しないので、高速なパフォーマンスが求められる際には望ましい。
  • DOMにcssのクラスを付与する
    • 保守性は高い
    • CSSセレクタのマッチングが増えるので、高速なパフォーマンスが求められる際には望ましくない。

その他のマッチング処理を避ける方法

  1. 利用していないCSSルールセットを減らす
    • マッチング処理数 ≒ DOM数 x CSSルールセット数
    • UNCSS等を利用して進めると便利。
      • 適用されていないCSSルールセットを検出、削除できる
  2. メディアクエリを指定する

レイアウトを避ける

Layoutを引き起こす原因は主に3つ。

  1. DOM要素の差表や大きさの変化
  2. DOMツリーの構造の変化
  3. DOM要素のコンテンツの変化
    • テキスト量が変わり、DOMの高さが変わるなど

その他

  • DOMツリーから切り離して処理する
    • 全体に影響を及ぼさないように、DOMツリー外で操作してから、DOMツリー内に戻す
  • レイアウトを減らす非表示
    • visibilityCSSをhiddenに設定する(Layoutそのものは行われる)
    • displayCSSをnoneに設定する
  • img要素のサイズを固定する
    • 画像読み込み前ではサイズがわからないので、読み込み後にLayoutが走る

7. レンダリング結果の描画のチューニング

レンダリング結果の描画の流れ

  1. ペイント(Paint)
    • Display List(描画命令列)をレイヤーごとに生成
  2. ラスタライズ(Rasterize)
    • Display Listを実行して、実際にピクセル化を行い、ビットマップを生成
  3. レイヤーの合成(Composite Layers)
    • 各レイヤーを一枚に合成する

再描画

再描画が引き起こされる原因は3つ。

  1. 前段階に当たるRenderingのLayoutが呼び出される
  2. Paintingだけが呼び出されるとき
    • JavaScriptでstyleプロパティのみが変更されたときなど
  3. Composite Layersのみが呼び出される
    • opacityCSSプロパティが別の値に更新された場合
    • transformCSSプロパティが別の値に設定された場合、もしくは新たに設定された場合

CSSプロパティの変更がどのような作用を起こすかは、CSS Triggersで確認できる。

レイヤーの生成条件

GPUによって合成されるレイヤー

  1. 3D変形を指定したtransformCSSはプロパティを持つ要素
  2. WebGLを用いたcanvas要素
  3. ハードウェアアクセラレーションを有効にしている状態での2Dコンテキストを使用したcanvas要素
  4. CSS Filterを使用した要素

translateZハック

transformCSSプロパティにtransformZ(0)を指定して、表示を変更せずにレイヤーを生成し、GPUで合成させるテクニック。

8. 高度なチューニング

1. 大量のDOM要素をあつかうバーチャルレンダリング

  1. バーチャルレンダリングのコンセプト
    • UITableViewやListViewみたいなイメージ。
  2. アルゴリズムの概要
    • 高さのある空のscroll領域を生成する。
    • ユーザに見える部分の要素だけ、DOMツリーに追加して表示する
  3. 事例とライブラリ

2. なめらかなアニメーション

  1. 指標と基本的な考え方
    • RAILの指標に従って、評価する
    • レンダリング時の不要なフェーズを減らす
  2. スタイルの計算をスキップする
    • DOMツリーに変更を加えないこと
    • styleを変更したいときは、直接指定すること
  3. レイアウトをスキップする
    • transformを使う
  4. ペイントをスキップする
    • opacityやtransformを使う
  5. レイヤーの合成を最適化する
    • GPU上での処理を使い回せるように、translateZハックを使う

3. CSS Containmentで再レンダリングを最適化する

will-changeCSSプロパティと同様に、パフォーマンス最適化のために利用されるCSSプロパティ。
DOM等が変更されて再レンダリングされる場合には、document全体が対象になるケースが多いが、これを局所的に抑えるためのプロパティ。
現在は、ChromeOperaのみサポートされてる。

CSS Containment Module Level 1

9. 認知的チューニング

indicatorを表示するなどして、体感速度を良くしようと言う話。

  • IndexedDBブラウザでのキャッシュ方法の一つ。