massa142's blog

くり返す このポリリズム

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

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

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

本書では高速化という課題に対し、きちんと対処できる知識と実力を身に付けます。基礎となるブラウザのレンダリングから、個別の問題に対する対応例、今後を見据えた設計の基礎などその場しのぎではない本質的な高速化を学びます。

この書籍のなかでも指摘されているけど、自分は巷で話題のベストプラクティスとかを調べて満足しちゃうタイプなので体系的に学べる本書はとても勉強になった。特にチューニングの話に入る前に、ネットワークとブラウザのレンダリングの仕組みについて解説されているのがめちゃくちゃ良い。

後半のところで書かれているテクニック(特にCSS関連のもの)は、否定していた『小さな最適化』のように感じられたが、全体として基礎をきちんと抑えることができる内容だったと思う。

また少し前に話題になったdev.toの速さの解説記事を合わせて読むことで、理解が深まって楽しめた。

フロントエンドのパフォーマンスについての興味が高まってるから、次にこの2つを読んでいきたい。

超速!  Webページ速度改善ガイド ── 使いやすさは「速さ」から始まる (WEB+DB PRESS plus)

超速! Webページ速度改善ガイド ── 使いやすさは「速さ」から始まる (WEB+DB PRESS plus)

パフォーマンス向上のためのデザイン設計

パフォーマンス向上のためのデザイン設計

読書メモ

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

第3章 チューニングの基礎

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

  • HTML/CSS/JavaSriptを最小化する
  • 適切な画像形式を選択する
    • dev.to は Cloudinaly を利用
    • Cloudinalyは、ブラウザごとに最適な拡張子(webp/jpeg)の選択をしてくれる
  • CSSのimportを避ける
  • JavaScriptの同期的な読み込みを避ける
    • script要素によるJSの読み込みは、ドキュメントのパースをブロック
    • defer属性
      • DOMツリーが構築されてから実行
      • 実行順序は宣言の順に従う
    • async属性
      • 実行タイミングと実行順を保証しない
      • ファイルが取得された時点で実行される
  • バイスピクセル比ごとに読み込む画像を切り替える
    • srcset属性
    • <picture>要素やsizes属性とも合わせて
  • CSSのメディアクエリを適切に指定する
    • FOUC(Flash of Unstyled Content)を回避するために、最初のレンダリングを行う前にCSSファイルの読み込みを待つ
    • link要素のmedia属性を適切に指定することで、CSSの読み込みがレンダリングをブロックしないようになる
<link href="style.css" rel="stylesheet" media="screen"> 
<link href="print.css" rel="stylesheet" media="print">
<link href="other.css" rel="stylesheet" media="(min-width: 920px)"> 
  • CSSスプライトを使って複数の画像をまとめる
    • HTTP/2以降では意味なし
  • リソースを事前読み込みしておく
    • 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="http://example.com">
  • Gzip圧縮を有効にする
  • CDNを用いてリソースを配信する
  • ドメインシャーディング
    • HTTP/2以降では意味なし
  • リダイレクトしない
    • URLの最後のスラッシュを付けるようにしておく
  • ブラウザのキャッシュを活用する
    • 強いキャッシュ(期限が切れるかキャッシュファイルが削除されるまで有効)
      • Expiresヘッダー
        • Expires: Mon, 02 Nov 2015 13:19:30 GMT
        • サーバーとクライアントの時間設定がずれてるとだめ
      • Cache-Controlヘッダー
        • Cache-Control:max-age=600
        • ExpiresとCache-Control両方が設定されていれば、Cache-Controlを優先
        • モダンなブラウザではCache-Controlに対応してるから、Cache-Control設定のみでOK
    • 弱いキャッシュ(条件付きGETリクエスト<=>304 Not Modified)
      • Last-Modifiedヘッダー
        • Last-Modified: Wed, 15 Nov 1995 04:58:08 GMT
        • If-Modified-Since: Wed, 15 Nov 1995 04:58:08 GMT
      • ETagヘッダー
        • ETag: "7797921b867ce392cad8415b729ffa97"
        • If-None-Match: "7797921b867ce392cad8415b729ffa97"
        • Last-ModifiedよりETagを優先
        • ETagは複数の結果をキャッシュできるのでより柔軟
        • 配信サーバーが複数の場合はETag値が異ならないように注意が必要
  • Service Workerの利用
  • HTTP/2の利用
    • バイナリ化したプロトコル
      • HTTP/1.1まではテキストベース
    • HTTP/1.1のセマンティクスの再利用
    • リクエストとレスポンスの多重化
      • 1つのTCP接続で複数のリクエストとレスポンスが並列で扱える
      • ブラウザの同時接続数の制限がなくなる
    • HTTPヘッダーの圧縮(HPACK)
      • 以前送信したものからの差分だけ送信できるように
    • TLSが事実上必須
  • QUICプロトコル
    • Googleが開発したネットワークプロトコル
    • TCPではなくUDP上に構築
    • 以前接続したことのあるサーバーとの通信であればゼロラウンドトリップ可能
    • 最初の接続もTCP+TLS相当のコネクション確立がラウンドトリップ1回で可能
    • Source Address Tokenをやりとりする
    • Head-of-Line Blockingがない(パケットは同時並行で送信される)
    • UDP上でありながら、TCP持つがパケットロス検出や誤り検出が可能
    • TLCが提供している盗聴・改ざん防止の機能も
    • Google Chromeのみ対応

第5章 JavaScript実行のチューニング

  • メモリリークを防ぐ
    • メモリ使用量が増えるとメモリスワッピングを引き起こす
      • メモリをハードディスクなどに退避させてメモリを解放する
    • console.log()によるメモリリーク
      • いつでもコンソールに表示できるようにするために特殊な参照が付いてGCの対象から外れる
    • WeakMapとWeakSet
  • Web Workersの利用
    • Web WorkersのスレッドとUIスレッド間の通信には、非同期でデータを通信しあうメッセージパッシングで行う
      • データは共有されずに、コピーしたものを送信する
      • Transferableインターフェースを持つオブジェクトは、所有権を扱える
        • データコピーする必要がなくなって、そのオーバーヘッドを回避できる
    • モバイル端末でも4コアな機種が多い
    • DOM要素・document/window/parentオブジェクトはWeb Workersからは使えない
  • asm.jsによるJavaScript高速化
    • Mozillaが設計したJavaScriptのサブセット
      • 計算はすべて静的型付け
      • 計算で利用できる方はintとdoubleとfloatの3つのみ
      • asm.jsモジュール内部では仮想的にGCが利用不可
    • EmscriptenC/C++のコードをasm.jsにトランスパイル
    • WebAssemblyに置き換えられる流れ
      • asm.jsはあくまでJavaScriptのサブセット
      • WebAssemblyブラウザ上で実行可能なバイナリフォーマット
  • SIMD.jsの利用
    • 複数の値の計算を一度に行うことができるSIMD(Single Instrauction Multiple Data)を扱えるAPI
    • WebAssemblyに置き換えられていくのが予想され、正式な仕様にはならない見通し
  • 高頻度で発火するイベントの抑制
  • モバイル端末でのclickイベントの遅延をなくす
    • touchendイベント後300ms待ってからclickイベントが発火
      • ダブルタップかどうかを判断するため
    • meta要素によるviewportを適切に設定すればOK
      • <meta name="viewport" content="width=device-width, user-scalable=no">
  • Passive Event Listenerでスクロールのパフォーマンスを改善する
    • イベントリスナがスクロールをブロックするScroll Jank問題
    • {passive: true}オプション
      • イベントリスナないでevent.preventDefault()が無効になることを保証
  • 無駄なForced Synchronous Layoutを減らす
    • DOM操作した後にレイアウト情報を参照するとレンダリングエンジンが最新の正しいレイアウト情報を返すために引き起こされる
      • DOM操作する前にレイアウト情報を参照しておく
      • requestAnimationFrame()を使用する
      • requestAnimationFrame()に渡したコールバックが呼び出されるタイミングは、DOM操作が終わり再レンダリングが終わってから

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

  • レイアウトツリー構築の流れ
    • CSSセレクタは右から左に向けて処理される
    • セレクタの記述を増やせば増やすほどマッチング処理に時間がかかる
    • レンダリングの際は、すべてのDOM要素のスタイルが再計算されるのではなく、新たに計算し直さないといけないところのみ
  • 高速なCSSセレクタの記述
  • BEMを用いる
    • Block・Element・Modifier
    • 基本的に1つのクラスセレクタのみを用いるのでパフォーマンスに優れる
  • レイアウトを減らす非表示
    • visibility: hiddenはペイント処理は行われないが、レイアウトツリーには含まれたまま
    • display: noneはレイアウトの対象からも外すことができる
  • img要素のサイズを固定する

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

  • 再描画
    • Composite Layersのみ引き起こされる場合
      • opacityプロパティの値が更新
      • transformプロパティの値が更新
  • translateZハック
    • 描画結果そのものは変えずにGPUによるレイヤー合成を行う
    • GPUへの転送時間やGPUのVRAMの容量などもあるので、必要なときだけ用いる
.target {
    transform: translateZ(0);
}

第8章 高度なチューニング

.foobar {
    contain: content;
}                   

第9章 認知的チューニング

  • インジケータを用いる
    • 1秒を超えるようであれば適用するのが望ましい
  • インターフェースプレビューを用いる
  • 処理が終わったように振る舞う
    • 楽観的UI(Optimistic UI)と呼ばれる
  • 無限スクロールを用いる
  • 投機的なリソースの先読み
    • Ajaxでデータを取得してアプリケーションレベルのキャッシュに保持する
      • IndexDBやLocalStorageなどのストレージor単なるJavaScriptの変数で保持
    • Resource Hintsを用いる