実務でフロントエンドのパフォーマンス改善タスクに少し携わったので、これを機にパフォーマンスチューニングについて学んだので書き記しておく。
書籍「Webフロントエンド ハイパフォーマンス チューニング」で読んだことをベースにまとめていく。
パフォーマンスチューニングを行う前に
意識すること
- 速すぎる最適化は諸悪の根源である
- 推測するな、計測せよ
- 目指すべき指標を設定する
- 小さな最適化で満足しない
パフォーマンスとは
- パフォーマンスとはユーザーがコンテンツを閲覧するのに体感する速度
- 速度が遅いのが仕様になってしまわないように
- 重い処理はある程度はカバーできるが、ボトルネックとなりうる
- ユーザーの操作によるブラウザの応答:インタラクション
- このインタラクションをどれだけスムーズに行うことができるか
レンダリングエンジン
- webブラウザがhtml, css, jsを解釈し、レンダリング(描画)して画面に表示する役割
- コンポーネントの一種
- Blink, Webkitなど
- ブラウザそのものではなく、あくまでレンダリングするだけのコンポーネント
JavaScriptエンジン
- jsのコードを実行する
- コードを実行する前に、ソースコードをトークンに分解
- 構文解析を行って、抽象構文木(AST)を生成
- ASTをバイトコードなどの形式に変換し、実行
- コードの実行速度を高めるために、コンパイル時に最適化を行う
- V8, Chakra, Nitroなど
- JavaScriptの実行環境
ブラウザレンダリングの流れ
Loading
- LoadingではURLバーに打ち込まれたURLからHTMLを読み込み、レンダリングに必要な付属するリソースを読み込む
- URLに含まれるホスト名の解決
- HTTPによる取得
- TCP接続の確立
- TLS接続の確立
- HTTPリクエストの送信とHTTPレスポンスの受け取り
リソースのダウンロード
- webページ自身を含むリソースのダウンロード
リソースのパース
- ダウンロードしたリソースをレンダリングエンジンの内部表現に変換
- DOMツリーやCSSツリーに変換される
ブラウザがリソース取得の際に利用するネットワークプロトコルについて
- IP
- ネットワークのノード間のパケットのやり取りを中継する
- パケットとはノード間のデータ通信に利用される最小単位のデータ
- TCPやUDPなどIP上で利用される上位のプロトコルでは全てのデータをパケットを用いて通信する
- IPv4とIPv6がある
- HTTPでネットワーク越しにリソースを取得する場合、リソースの取得の速度はIPのパケットの届く速さに依存する
- パケットの特徴
- 内容が壊れることがある
- 送信したパケットが消失することがある
- 送信順が入れ替わることがある
- 複製されて届くことがある
- この欠点を補うために使用されるのがTCP
- ネットワークのノード間のパケットのやり取りを中継する
- TCP
- TCPの特徴
- 相手先に確実にデータが届いているかどうかを確認する
- データの欠損や破損を検知して再送する
- データの送信順を保証する
- TCPはコネクションという概念をもち、相手先とコネクションを確立し接続が終了するまで信頼性のあるデータ転送(パケットの特徴を補った)ができる
- HTTPでデータを取得する際には必ずTCP上で行われる
- TCPの特徴
- TLS
- URLがhttpsの場合TCPとHTTP間でTLSを利用する
- TLSはSSLとも呼ばれる
- TLSの特徴
- クライアントとサーバーの認証機能
- 通信データの改竄
- 通信データの暗号化
- UDP
- TCP同様IPとポート番号を指定してメッセージを送る
- IPに対してデータ欠損の通知や送信順の保証などを行わない
- ハンドシェイクがない
- HTTP
- ブラウザとwebサーバーでHTMLなどのWebページのコンテンツを送受信する
- keepAliveがデフォルトで有効になっている
- 1つのTCP接続を破棄せずにその中で何度もリクエストとレスポンスのやり取りをする
- そうすることでハンドシェイクなどTCP接続のオーバーヘッドを減らせる
- 1つのTCP接続を破棄せずにその中で何度もリクエストとレスポンスのやり取りをする
- DNS
- IPアドレスを解決するためのシステム
- TCP接続をするのに相手方のIPアドレスが必要なため、URLのホスト名から変換する
HTMLの読み込み(パース)
- ネットワークプロトコルを使用してHTMLを取得し、それらをDOMツリーやCSSOMツリーに変換する
- DOMツリーへの変換
- 字句解析によるトークンのリスト化
- 構文解析による構文木の構築
- 構文木内にあるJavaScriptを実行しつつDOMツリーの構築
Scripting
- 下記の順番で実行する
- 字句解析
- 構文解析
- コンパイル
- 実行
字句解析・構文解析
- JavaScriptのコードをトークンの列に変換する
- トークンとは
. ();
などの記号ごとに分割した値-
console, . , log, (, "hello world" , ), ;
となる
-
- トークンとは
- 分割したトークンを構文解析を行い抽象構文木に変換する
-
console, log, “hello world”
が残る
-
コンパイル
- 処理系内部の仮想マシン用のコードへの変換
- JITコンパイルによる機械語への変換
- 一旦中間コードを生成しておいて、実行する際に機械語に変換することで高速に実行できる
Rendering
- レイアウトツリーの構築を行う
- スタイルの計算とレイアウト
スタイルの計算
- ドキュメント内の全てのDOM要素に対してどのようなCSSプロパティが当たるかを計算する
- CSSOMツリー内を全て参照してCSSセレクタのマッチング処理を行う
- 総当たり形式で実行される
- DOM要素100 * CSSルールセット50であれば5000回のマッチングが行われる
レイアウト
- 視覚的なレイアウト情報の計算を行う
- 要素の大きさ、マージン、パディング、位置、z-indexなど
Painting
- ペイント・ラスタライズ・レイヤーの合成からなる
ペイント
- 2Dグラフィック向けの命令を生成する
ラスタライズ
- ピクセルに対してレイヤーという単位ごとに描画される
レイヤーの合成
- 最終的なレンダリング結果の生成
再レンダリング
- 全ての処理をやり直すのではなく、なるべく再利用される
チューニングの基礎
パフォーマンスチューニングを行う前に
- 速すぎる最適化は諸悪の根源である
- 推測するな、計測せよ
- 目指すべき指標を設定する
- 小さな最適化で満足しない
パフォーマンス指標 RAIL
-
Responce: 100ms
- ユーザーのアクションに対してWebページ上に何らかの変化をもたらすまでにかかる時間
- 例えばボタンクリック後、モーダルを表示するまでに300msかかると一瞬フリーズしたように見えてしまう
-
Animation: 16ms
- アニメーションの1フレームの時間
- フレームとはScriptingからRendering, Paintingが終わるまでにかかる時間のこと
- システムやOSやGPUドライバや描画の合成の固定的な時間が4msかかるため、実質jsの処理時間は12ms以下となる
- 安定的に60FPSを保つためにはその半分の6ms以下が望ましいとされる
- FPS:1秒間に60回フレームを表示すること
- アニメーションの1フレームの時間
-
Idle: 50ms
- アイドル状態に実行されるjsの処理時間のこと
- アイドル状態:システムが何も処理を実行していない状態のこと
- jsはメインスレッドで処理が行われるため、Pendingで待つケースがあるので注意
- アイドル状態に実行されるjsの処理時間のこと
-
Load: 1000ms
- webページのコンテンツ読み込みにかかる時間
- 1000ms以内に別ページが開かれると早いと感じる
- キャッシュを使用したり、初期読み込み時はローディングを表示したり、遅延ロードを行い待ち時間を極力減らす
- 遅延ロード
- 画像や動画などのリソースを必要に応じて読み込む技術
- ページの読み込み時間を短縮しユーザーのストレスや不快感を軽減することが目的
- レイジロード
- ユーザーが操作している最中にページの一部分が遅延して表示される現象
- ユーザーにとってストレスや不快感を与えることがあるため、ページのパフォーマンスにおいて問題となっている
- 遅延ロード
リソース読み込みのチューニング
HTML/CSS/JSを最小化する
- 改行やタブなどが含まれているとそれだけで無駄な文字数になってしまうため、ツールを用いて取り除く
- そうすることにより、リソースファイルのサイズやダウンロード時間を減らすことができる
CSSのimportを避ける
- cssファイルから@importを書いた場合、cssファイルからcssファイルを呼び出していると思いきや、HTTPリクエストでインポートされているファイルを取得している
- 並列ではなく直列で実行されるので遅延につながる
- キャッシュを有効にすることができない
- linkタグを使用すると並列でcssファイルを取得することができる
デバイスピクセル比ごとに読み込む画像を切り替える
- サイズの合っていない画像は引き伸ばされて表示されてしまう
- 大きな画像を小さくする分には問題ないので、大きめの画像を用意する
キャッシュの使用
- 強いキャッシュ
- 期限内に有効なキャッシュが存在する場合には必ずそれを使用し、新しいリクエストを送信することがない
- 弱いキャッシュ
- 期限内でもキャッシュが更新される可能性があるキャッシュ
- ブラウザキャッシュ
- ブラウザ内に保存されるキャッシュ。HTTPヘッダーに基づいて、リソースの再取得を制御する。
- サーバーキャッシュ
- サーバー側に保存されるキャッシュ。リクエストを処理した結果をキャッシュし、同じリクエストが来た場合はキャッシュからレスポンスを返す。
- CDNキャッシュ
- CDNサービスが提供するキャッシュ。静的ファイルなどをキャッシュして、高速に配信する。
Service Workersの使用
- メインスレッドとは別のスレッドで動作することができる
- キャッシュから取得するかサーバーから取得するか処理で分岐できる
- WebWorkersはjsをメインスレッドとは別のバックグラウンドスレッドで実行することができる
JavaScript実行のチューニング
UIスレッド
- ブラウザのタブ1つに対して1つ存在するスレッド
- ブラウザのUIに関連する処理を行う
- レイアウトの計算やレンダリング処理、DOMイベントの発火など
- レイアウトの計算が遅くなればレイアウトの処理が遅くなっていく
- DOMイベント発火もこのスレッドなので、処理中にイベントを発火させても処理終了まで待たなければならない
- alertなど特定の関数では実行された際にUIスレッドをブロックし続け、その後の処理を実行しないものもあるので注意
console.log()によるメモリリーク
- console.log()にオブジェクトを渡すとコンソールにいつでも値を表示できるように特殊な参照が付く
- そのためGCの対象から外れる
- コンソールをクリアしない限りこの参照は消えない
- プロダクション環境ではconsole.logを入れたままにしない
WeakMapとWeakSet
- jsではオブジェクトを変数として参照していると、GCによって解放されない
- WeakMapやWeakSetは弱参照と呼ばれ、GCによって参照されていないとみなされる
- キーにオブジェクトしか使用できず、イテレーションや反復処理ができないため値を取得するには直接キーを指定しなければならないので使い勝手が悪い
Web Workersの利用
- UIスレッドとは別のスレッドで処理を行うことができる
- メッセージパッシングと呼ばれる手法を用いてWebWorkersスレッドとUIスレッドで情報のやり取りを行う
- 複数のスレッド間で同一のデータを操作したり読み出したりできないようになっている
- バグの温床となるため
- WebWokersではデータをコピーして渡すことでスレッド間で同じデータに対してアクセスできないようになっている
- オーバーヘッドが発生してしまう可能性がある
モバイル端末でClickイベントについて
- モバイル端末でaddEventListener(’click’, () ⇒ {})を行うと、ダブルタップかどうか判別するために300ms待ってからclickイベントが発火するようになっている
- input要素やa要素でも同様
- viewportに設定を加えることで回避することが可能
レイアウトツリー構築のチューニング
CSSのマッチング
- セレクタのマッチングは右から左に行われる
- ネストしまくるとそれだけ時間がかかるのでできるだけネストは避ける
- 子孫セレクタ、間接セレクタ、全称セレクタ(*)も時間がかかるので避ける
- 基本的はセレクタ1つが望ましいため、BEMのような設計原則を用いるとパフォーマンス改善につながる
- jsで動的に書き換える場合はclassを動的に追加するよりもstyleをjsから書き換えた方が早い
- ただ保守性は下がるのでclassを動的に追加・削除した方が基本的には良いとされている
- メディアクエリを使用することでマッチング対象を減らすことができる
- 表示しない要素はdisplay: noneを設定する
再レンダリング
- 前回のレンダリングで使用したスタイルをできる限り再利用しようとする
- 再レンダリングでは再計算しなければならない箇所のみが計算し直される
レンダリング結果の描画のチューニング
- DOM操作や要素の座標が更新されるとLayoutの更新が行われる
- Layoutが実施されると後続のPaintingは必ず行われる
- colorの変更など、DOM要素のレイアウトは変更されないが再描画は行われる場合にLayoutを挟まずpaintingが行われる
- レイヤーごとに処理が行われる
- z-indexが同じであればスタッキングコンテキスト(レイヤーをまとめる概念)に含まれる
- そのほかpositionなども関係してくる
- z-indexが同じであればスタッキングコンテキスト(レイヤーをまとめる概念)に含まれる
認知的チューニング
- パフォーマンスチューニングをやり切っても時間がかかっている部分があるのであれば、認知的チューニングを行う
- ローディング画面を表示する
- 1000ms以上であれば表示したほうがよい
- 仮のコンテンツを表示し、そこにデータが入ってくるようなUIにする
- 無限スクロールを用いる
まとめ
フロントエンドを2年ほどやってきたが知らないことが思いの外多かった。
プラクティスを学ぶとそれを適用できる箇所を見つけようとしてしまうので、常にボトルネックを改善する意識を持って計測・改善に取り組む。
またリソースの取得のところでネットワークプロトコルが登場し、ネットワークに関しては全くの無知なのでこれを機に勉強していきたい。