JavaScript の実行を最適化する

JavaScript は多くの場合、視覚変化をトリガーします。視覚変化はスタイル操作を通じて直接行われることもあれば、データの検索やソートのように、計算が最終的に視覚変化につながることもあります。タイミングの悪い JavaScript や長時間実行される JavaScript はパフォーマンス低下の原因になることが多いため、可能な限り JavaScript の影響を最小限に抑える必要があります。

JavaScript は多くの場合、視覚変化をトリガーします。視覚変化はスタイル操作を通じて直接行われることもあれば、データの検索やソートのように、計算が最終的に視覚変化につながることもあります。タイミングの悪い JavaScript や長時間実行される JavaScript はパフォーマンス低下の原因になることが多いため、可能な限り JavaScript の影響を最小限に抑える必要があります。

ユーザーが書く JavaScript は実際に実行されるコードとは異なるため、JavaScript パフォーマンス プロファイリングはちょっとした便利な技術と言えます。最近のブラウザは JIT コンパイラおよび多様な最適化とトリックを使用して、可能な限り高速の実行を実現しようとします。これによって、コードの動態が大きく変わります。

ただし、そうは言っても、アプリで JavaScript を的確に実行するための工夫がいくつかあります。

概要

  • 視覚的な更新のために setTimeout または setInterval を使用するのを避けて、常に requestAnimationFrame を使用するようにします。
  • 長時間実行される JavaScript をメインスレッドから Web Worker に移動します。
  • マイクロタスクを使用して、複数のフレームにわたって DOM 変更を行います。
  • Chrome DevTools の Timeline と JavaScript プロファイラを使用して、JavaScript の影響を評価します。

視覚的な変更に requestAnimationFrame を使用する

画面上で視覚変化が発生しているとき、ブラウザにとって適切なタイミングで(つまり、フレームの開始時に)処理を実行する必要があります。フレームの開始時に JavaScript が実行されることを保証する唯一の方法は、requestAnimationFrame を使用することです。

/**
    * If run as a requestAnimationFrame callback, this
    * will be run at the start of the frame.
    */
function updateScreen(time) {
    // Make visual updates here.
}

requestAnimationFrame(updateScreen);

フレームワークまたはサンプルでは、アニメーションのような視覚変化を実現するために setTimeout または setInterval を使用する場合がありますが、これの問題はコールバックがフレームの任意の位置で(おそらくフレームの最後で)実行されることです。そのため、1 つのフレームが見落とされ、ジャンクを発生させることが頻繁にあります。

setTimeout が原因でブラウザでフレームの見落としが発生する。

実際、jQuery は animate 動作に setTimeout を使用していました。バージョン 3 では、requestAnimationFrame を使用するように変更されました。古いバージョンの jQuery を使用している場合は、requestAnimationFrame を使用するようにパッチを適用できます(強く推奨)。

複雑性の軽減または Web Worker の使用

JavaScript は、スタイル計算、レイアウト、そして多くの場合はペイントとともに、ブラウザのメインスレッドで実行されます。JavaScript が長時間実行される場合は、これらの他のタスクがブロックされ、フレームが見落とされることになる可能性があります。

JavaScript を実行するタイミングと実行時間は十分に考慮する必要があります。たとえば、スクロール操作のようなアニメーションでは、JavaScript の実行時間を 3 ~ 4 ミリ秒に抑えることが理想的です。そうしないと、実行時間が長くなりすぎる危険があります。アイドル期間であれば、実行時間をあまり気にする必要はありません。

多くの場合、たとえば、DOM アクセスが必要でない場合は、純粋な計算処理を Web Worker に移動することができます。ロード処理やモデル生成と同様に、検索やソートなどのデータ操作またはトラバーサルは、通常、この手法に適しています。

var dataSortWorker = new Worker("sort-worker.js");
dataSortWorker.postMesssage(dataToSort);

// The main thread is now free to continue working on other things...

dataSortWorker.addEventListener('message', function(evt) {
    var sortedData = evt.data;
    // Update data on screen...
});

すべての処理がこの手法に適しているとは限りません。Web Worker は DOM アクセスを行いません。メインスレッドで処理を実行する必要がある場合は、バッチ手法を検討してください。この手法では、大きいタスクを複数のマイクロタスクに分割します。各マイクロタスクの実行時間は数ミリ秒以内であり、各フレームにわたり requestAnimationFrame ハンドラの内部で実行されます。

この手法により UX と UI に影響があります。進捗インジケータまたはアクティビティ インジケータを使用して、タスクが処理されていることをユーザーに知らせる必要があります。どのような場合も、この手法ではアプリのメインスレッドが空けられるため、ユーザーの操作に迅速に応答することができます。

JavaScript の「フレーム コスト」の認識

フレームワーク、ライブラリ、または独自のコードを評価するときは、JavaScript コードをフレーム単位で実行するコストを評価することが重要です。遷移やスクロールのようなパフォーマンス重視型アニメーション処理を実行するとき、これは特に重要です。

JavaScript のコストを測定する最適な方法は、Chrome DevTools の [パフォーマンス] パネルを使用することです。通常は次のような低レベルの記録が得られます。

Chrome DevTools でのパフォーマンス レコーディング

[Main] セクションには、JavaScript 呼び出しのフレームチャートが表示されます。これにより、呼び出された関数と各関数にかかった所要時間を正確に分析できます。

この情報を活用して、アプリケーションに対する JavaScript のパフォーマンス影響を評価し、関数の実行に時間がかかりすぎているホットスポットの検出と修正を開始することができます。前述のとおり、長時間実行される JavaScript は削除するか、それが不可能な場合は、他のタスクを続行するために JavaScript を Web Worker に移動してメインスレッドを解放する必要があります。

[パフォーマンス] パネルの使用方法については、ランタイム パフォーマンスの分析を始めるをご覧ください。

JavaScript の細かい最適化の回避

ブラウザがあるバージョンの処理を別のバージョンの処理よりも 100 倍速く実行できることを知るのは面白いかもしれませんが、要素の offsetTop をリクエストする方が getBoundingClientRect() を計算するよりも速いなど、ほとんどの場合、このような関数はフレームごとに数回しか呼び出されないことがほとんどです。そのため、JavaScript のパフォーマンスのこの側面に焦点を当てても、通常は労力の無駄になります。通常、短縮できる時間はミリ秒未満にすぎません。

ゲームや計算負荷の高いアプリケーションを開発している場合は、通常、大量の計算を単一のフレームに埋め込みます。その場合はあらゆることが役立つため、このアドバイスは当てはまりません。

簡単に言えば、通常、細かい最適化は作成中のアプリケーションに適しないため、細かい最適化には非常に慎重になる必要があります。