JavaScript アプリを高速化するための一般的なアドバイスには、「メインスレッドをブロックしない」や「長いタスクを分割する」などがあります。このページでは、このアドバイスの意味と、JavaScript でタスクを最適化することが重要な理由について説明します。
タスクとは
タスクとは、ブラウザが実行する個々の作業のことです。これには、レンダリング、HTML と CSS の解析、作成した JavaScript コードの実行など、ユーザーが直接制御できない処理が含まれます。ページの JavaScript は ブラウザタスクの主要なソースです
タスクはいくつかの方法でパフォーマンスに影響します。たとえば、ブラウザが起動時に JavaScript ファイルをダウンロードすると、その JavaScript を実行できるように、その JavaScript を解析してコンパイルするタスクをキューに入れます。ページのライフサイクルの後半では、JavaScript が機能したときに、イベント ハンドラによるインタラクションの促進、JavaScript ドリブン アニメーション、アナリティクス コレクションなどのバックグラウンド アクティビティなどのタスクが開始されます。ウェブワーカーや同様の API を除き、これらはすべてメインスレッドで実行されます。
メインスレッドは何か?
メインスレッドでは、ほとんどのタスクがブラウザで実行されます。また、ここで作成するほぼすべての JavaScript が実行されます。
メインスレッドは一度に 1 つのタスクしか処理できません。50 ミリ秒を超えるタスクは「長いタスク」としてカウントされます。長時間のタスクやレンダリングの更新中にユーザーがページを操作しようとすると、ブラウザはその操作を処理するまで待機する必要があり、レイテンシが発生します。
これを防ぐには、長いタスクを複数の小さなタスクに分割し、それぞれの実行時間を短縮します。これは、長いタスクを分割と呼ばれます。
タスクを分割することで、ブラウザが他のタスク間のユーザー操作など、優先度の高い作業に応答する機会を増やすことができます。これにより、インタラクションがはるかに速くなります。ブラウザが長いタスクの終了を待つ間に遅延に気づくかもしれません。
タスク管理の戦略
JavaScript は、タスク実行の実行から完了モデルを使用するため、各関数を単一のタスクとして扱います。つまり、次の例のように、他の複数の関数を呼び出す関数は、呼び出された関数がすべて完了するまで実行しなければならず、それによってブラウザの速度が低下します。
function saveSettings () { //This is a long task.
validateForm();
showSpinner();
saveToDatabase();
updateUI();
sendAnalytics();
}
コードに複数のメソッドを呼び出す関数が含まれている場合は、複数の関数に分割します。これにより、ブラウザが操作に応答する機会が増えるだけでなく、コードの読み取り、保守、テストの作成が容易になります。以降のセクションでは、長い関数を分割し、関数を構成するタスクに優先順位を付けるための戦略について説明します。
コードの実行を手動で延期する
一部のタスクの実行を延期するには、関連する関数を setTimeout()
に渡します。これは、タイムアウトを 0
に指定しても機能します。
function saveSettings () {
// Do critical work that is user-visible:
validateForm();
showSpinner();
updateUI();
// Defer work that isn't user-visible to a separate task:
setTimeout(() => {
saveToDatabase();
sendAnalytics();
}, 0);
}
これは、順番に実行する必要がある一連の関数に最適です。コードの構成が異なる場合は、別のアプローチが必要になります。次の例は、ループを使用して大量のデータを処理する関数です。データセットが大きければ大きいほど、この処理には時間がかかります。また、必ずしもループ内で setTimeout()
を配置するのに適した場所がなくなります。
function processData () {
for (const item of largeDataArray) {
// Process the individual item here.
}
}
幸いなことに、コードの実行を後のタスクで遅らせることができる API が他にもあります。postMessage()
を使用してタイムアウトを短縮することをおすすめします。
requestIdleCallback()
を使用して作業を分割することもできますが、タスクは最も優先度の低い、ブラウザのアイドル時間中にのみスケジュールされます。つまり、メインスレッドが特にビジー状態の場合、requestIdleCallback()
でスケジュールされたタスクは実行されない可能性があります。
async
/await
を使用して収益点を作成する
優先度の低いタスクの前にユーザー向けの重要なタスクを確実に実行するには、タスクキューを短時間中断してメインスレッドに譲り渡し、ブラウザがより重要なタスクを実行する機会をブラウザに提供します。
最もわかりやすい方法は、setTimeout()
の呼び出しで解決される Promise
を使用する方法です。
function yieldToMain () {
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
saveSettings()
関数では、各関数呼び出しの後に yieldToMain()
関数を await
すると、各ステップの後にメインスレッドに引き継ぐことができます。これにより、長いタスクを複数のタスクに分割できます。
async function saveSettings () {
// Create an array of functions to run:
const tasks = [
validateForm,
showSpinner,
saveToDatabase,
updateUI,
sendAnalytics
]
// Loop over the tasks:
while (tasks.length > 0) {
// Shift the first task off the tasks array:
const task = tasks.shift();
// Run the task:
task();
// Yield to the main thread:
await yieldToMain();
}
}
重要なポイント: すべての関数呼び出しの後で放棄する必要はありません。たとえば、ユーザー インターフェースの重要な更新につながる 2 つの関数を実行する場合、その間に間に合わないことをおすすめします。可能であれば、まずその処理を実行させ、その後、バックグラウンド処理や、ユーザーに表示されない重要度の低い処理を行う関数間で分離することを検討してください。
専用のスケジューラ API
これまでに説明した API はタスクの分割に役立ちますが、大きな欠点があります。コードを遅延して後のタスクで実行すると、そのコードがタスクキューの最後に追加されます。
ページ上のすべてのコードを管理する場合は、独自のスケジューラを作成してタスクに優先順位を付けることができます。ただし、サードパーティ スクリプトはスケジューラを使用しないため、その場合は処理に優先順位を付けることはできません。データの分割やユーザーの操作にあてはめることしかできません。
スケジューラ API には postTask()
関数が用意されています。これにより、タスクのスケジューリングを細かく行うことができます。また、優先順位の低いタスクがメインスレッドに譲れるようにブラウザが作業に優先順位を付ける際にも役立ちます。postTask()
は Promise を使用し、priority
設定を受け入れます。
postTask()
API には次の 3 つの優先度があります。
'background'
: 優先度が最も低いタスク。'user-visible'
: 優先度が中程度のタスク。priority
が設定されていない場合、これがデフォルトです。'user-blocking'
: 高い優先度で実行する必要がある重要なタスク。
次のサンプルコードでは、postTask()
API を使用して、3 つのタスクを最も優先度が高く、残りの 2 つのタスクを最も低い優先度で実行しています。
function saveSettings () {
// Validate the form at high priority
scheduler.postTask(validateForm, {priority: 'user-blocking'});
// Show the spinner at high priority:
scheduler.postTask(showSpinner, {priority: 'user-blocking'});
// Update the database in the background:
scheduler.postTask(saveToDatabase, {priority: 'background'});
// Update the user interface at high priority:
scheduler.postTask(updateUI, {priority: 'user-blocking'});
// Send analytics data in the background:
scheduler.postTask(sendAnalytics, {priority: 'background'});
};
ここでは、ユーザーの操作など、ブラウザで優先されるタスクが機能するように、タスクの優先度がスケジュールされています。
また、必要に応じて異なる TaskController
インスタンスの優先度を変更する機能など、タスク間で優先度を共有する異なる TaskController
オブジェクトをインスタンス化することもできます。
まもなく提供される scheduler.yield()
API を使用した、継続を伴う組み込み yield
重要なポイント: scheduler.yield()
の詳細については、オリジン トライアル(終了時)と説明をご覧ください。
スケジューラ API への追加として提案されている scheduler.yield()
は、ブラウザのメインスレッドに引き継ぐように特別に設計された API です。使用方法は、このページで前述した yieldToMain()
関数に似ています。
async function saveSettings () {
// Create an array of functions to run:
const tasks = [
validateForm,
showSpinner,
saveToDatabase,
updateUI,
sendAnalytics
]
// Loop over the tasks:
while (tasks.length > 0) {
// Shift the first task off the tasks array:
const task = tasks.shift();
// Run the task:
task();
// Yield to the main thread with the scheduler
// API's own yielding mechanism:
await scheduler.yield();
}
}
このコードはおなじみのものですが、yieldToMain()
の代わりに await scheduler.yield()
を使用しています。
scheduler.yield()
のメリットは継続です。つまり、一連のタスクの途中で放棄した場合、他のスケジュール タスクは、その明け渡し点より後に同じ順序で続行されます。これにより、サードパーティのスクリプトがコードの実行順序を制御できなくなります。
また、priority: 'user-blocking'
で scheduler.postTask()
を使用すると、user-blocking
の優先度が高いため、処理が続行される可能性が高くなります。scheduler.yield()
がより広く利用できるようになるまで、代替手段として使用できます。
setTimeout()
(または scheduler.postTask()
を priority: 'user-visible'
を指定するか、または明示的な priority
を指定しない)を使用すると、キューの最後にタスクがスケジュールされ、他の保留中のタスクが続行前に実行されます。
isInputPending()
での入力でイールド
対応ブラウザ
- 87
- 87
- x
- x
isInputPending()
API は、ユーザーがページを操作しようとしたかどうかをチェックして、入力が保留中の場合にのみ結果を返す方法を提供します。
これにより、JavaScript は保留中の入力がないときでも、タスクキューの後ろに残って終了するのではなく、処理を続行できます。その結果、リリースの意図で説明されているように、メインスレッドに譲れないサイトのパフォーマンスが大幅に向上します。
ただし、この API のリリース以降、特に INP の導入以降、生成に関する Google の認識は向上しました。この API の使用はおすすめしません。代わりに、入力が保留中かどうかに関係なく 結果を出力することをおすすめします。最適化案が変わった理由はいくつかあります。
- ユーザーが操作を行った際に、API が誤って
false
を返すことがあります。 - タスクで成果が得られるケースは、入力だけではありません。アニメーションやその他の定期的なユーザー インターフェースの更新は、レスポンシブ ウェブページを提供するうえで同様に重要です。
- その後、yield に関する問題に対処するために、
scheduler.postTask()
やscheduler.yield()
などのより包括的な yying API が導入されました。
おわりに
タスクの管理は簡単ではありませんが、タスクの管理をすることで、ユーザーの操作に迅速に対応できるようになります。タスクの管理と優先順位付けには、ユースケースに応じてさまざまな方法があります。繰り返しになりますが、タスクを管理するときに考慮すべき主な事項は次のとおりです。
- ユーザー向けの重要なタスクはメインスレッドに任せる。
scheduler.yield()
を試すことを検討してください。postTask()
でタスクに優先順位を付けます。- 最後に、関数で行う処理はできるだけ少なくします。
これらのツールのうち 1 つ以上を使用することで、ユーザーのニーズを優先しながら、重要度の低い作業が引き続き行われるようにアプリの作業を構造化できます。これにより、応答性が向上し、より楽しめるようになるため、ユーザー エクスペリエンスが向上します。
このドキュメントの技術的調査に協力してくれた Philip Walton に感謝します。
サムネイル画像(出典: Unsplash、Amirali Mirhashemian 提供)