「メインスレッドをブロックしない」と言われた場合「長いタスクを分割する」ことを おすすめします どういう意味でしょうか
JavaScript アプリの速度を保つための一般的なアドバイスは、次のようにまとめられます。
- 「メインスレッドをブロックしない。」
- 「長いタスクを分けて。」
これはすばらしいアドバイスですが、どのような作業が必要になるでしょうか。配布する JavaScript を少なくすることは良いことですが、それだけでユーザー インターフェースの応答性も向上するということですか?そうかもしれませんが、そうでないかもしれません。
JavaScript でタスクを最適化する方法を理解するには、まずタスクとは何か、ブラウザがどのようにタスクを処理するのかを理解する必要があります。
タスクとは
タスクとは、ブラウザが実行する個別の作業です。これには、レンダリング、HTML および CSS の解析、JavaScript の実行、その他ユーザーが直接制御できないような作業が含まれます。中でも、記述する JavaScript が、おそらくタスクの最大ソースとなります。
<ph type="x-smartling-placeholder">JavaScript に関連付けられたタスクは、いくつかの点でパフォーマンスに影響します。
- ブラウザは起動時に JavaScript ファイルをダウンロードすると、その JavaScript を解析してコンパイルするタスクをキューに入れ、後で実行できるようにします。
- ページの実行中に、イベント ハンドラ、JavaScript 駆動型アニメーション、バックグラウンド アクティビティ(アナリティクス コレクションなど)を介したインタラクションの促進など、JavaScript が機能したときにタスクがキューに入れられることもあります。
Web Worker や同様の API を除き、すべての処理はメインスレッドで行われます。
メインスレッドとは
ブラウザ内のほとんどのタスクはメインスレッドで実行されます。ここで、記述した JavaScript のほとんどが実行されます。
メインスレッドは一度に 1 つのタスクしか処理できません。50 ミリ秒を超えるタスクは、時間のかかるタスクとなります。50 ミリ秒を超えるタスクの場合、タスクの合計時間から 50 ミリ秒を引いた値が、タスクのブロック期間と呼ばれます。
ブラウザは、タスクの実行中に操作が発生するのをブロックします。ただし、タスクの実行時間が長すぎない限り、ユーザーはそのことを認識できません。しかし、長時間のタスクが多いときにユーザーがページを操作しようとすると、ユーザー インターフェースが応答していないと感じます。メインスレッドが長時間ブロックされると、ユーザー インターフェースが応答しなくなることもあります。
<ph type="x-smartling-placeholder">メインスレッドが長時間ブロックされないようにするため、長いタスクを複数の小さなタスクに分割できます。
<ph type="x-smartling-placeholder">タスクを分割することで、ブラウザはユーザーの操作を含む優先度の高い作業にすばやく反応できるため、これは重要です。その後、残りのタスクが完了まで実行され、最初にキューに入れた作業が確実に完了します。
<ph type="x-smartling-placeholder">上の図では、ユーザー操作によってキューに入れられたイベント ハンドラは、1 つの長いタスクの開始を待たなければならなかったので、操作の実行が遅れます。このシナリオでは、遅延が発生している可能性があります。画面下部では、イベント ハンドラの実行が早く開始されるため、インタラクションが「すぐに」行われたように感じられた可能性があります。
タスクの分割が重要な理由がわかったところで、次は JavaScript で分離する方法を学びます。
タスク管理戦略
ソフトウェア アーキテクチャでよくあるアドバイスは、作業を小さな機能に分割することです。
function saveSettings () {
validateForm();
showSpinner();
saveToDatabase();
updateUI();
sendAnalytics();
}
この例では、saveSettings()
という名前の関数があります。この関数は、フォームの検証、スピナーの表示、アプリケーション バックエンドへのデータの送信、ユーザー インターフェースの更新、分析の送信を行う 5 つの関数を呼び出します。
概念的には、saveSettings()
は適切に設計されています。これらの関数をデバッグする必要がある場合は、プロジェクト ツリーを走査して、各関数の機能を把握します。このような作業を分割することで、プロジェクトの操作と管理が容易になります。
ただし、JavaScript では各関数が saveSettings()
関数内で実行されるため、各関数を個別のタスクとして実行しない可能性があります。これは、5 つの関数すべてが 1 つのタスクとして実行されることを意味します。
最良のシナリオでは、これらの関数のいずれか 1 つでも、タスクの合計時間に 50 ミリ秒以上寄与する可能性があります。最悪の場合、特にリソースの制約があるデバイスでは、このようなタスクの多くが長時間実行される可能性があります。
コードの実行を手動で延期する
デベロッパーがタスクを小さなものに分割するために使用した方法の一つに、setTimeout()
があります。この手法では、関数を 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);
}
これは「収束」と呼ばれ、順次実行する必要がある一連の関数に最適です。
<ph type="x-smartling-placeholder">ただし、コードが常にこの方法で整理されているとは限りません。たとえば、大量のデータをループで処理する必要がある場合、イテレーションが多い場合、そのタスクは長時間かかる可能性があります。
function processData () {
for (const item of largeDataArray) {
// Process the individual item here.
}
}
ここで setTimeout()
を使用すると、デベロッパーのエルゴノミクスの観点から問題が発生します。また、個々の反復処理を迅速に実行しても、データ配列全体の処理に非常に長い時間がかかる可能性があります。いずれにしても、setTimeout()
はジョブに適したツールではありません。少なくとも、その使い方を実践した場合には適していません。
async
/await
を使用して収益ポイントを作成する
ユーザー向けの重要なタスクを優先度の低いタスクより前に実行できるようにするには、 タスクキューを少し中断してメインスレッドに待機できます。 ブラウザでより重要なタスクを実行する機会が増えます。
前述のように、setTimeout
を使用してメインスレッドに処理を進めることができます。ただし、利便性と読みやすさのために、Promise
内で setTimeout
を呼び出し、その resolve
メソッドをコールバックとして渡すことができます。
function yieldToMain () {
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
yieldToMain()
関数の利点は、どの async
関数でも await
を実行できることです。前の例を基にして、実行する関数の配列を作成し、関数が 1 つ実行されるたびにメインスレッドに結果を返すことができます。
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();
}
}
その結果、かつてはモノリシックなタスクが別々のタスクに分割されました。
<ph type="x-smartling-placeholder"> <ph type="x-smartling-placeholder">専用のスケジューラ API
setTimeout
は、タスクを分割する効果的な方法ですが、後続のタスクで実行するコードを遅らせてメインスレッドに移行すると、そのタスクがキューの最後に追加されます。
ページ上のすべてのコードを管理している場合は、タスクに優先順位を付ける機能を備えた独自のスケジューラを作成できますが、サードパーティのスクリプトはスケジューラを使用しません。事実上、そのような環境では作業に優先順位を付けることはできません。データを分割するか、明示的にユーザーの操作に委ねることのみ可能です。
スケジューラ API には、タスクのきめ細かなスケジューリングを可能にする postTask()
関数があります。ブラウザが作業に優先順位を付け、優先度の低いタスクがメインスレッドに送られるようにする方法の 1 つです。postTask()
は Promise を使用し、次の 3 つの priority
設定のいずれかを受け入れます。
- 優先度が最も低いタスクの場合は
'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'});
};
ここでは、タスクの優先度は、ブラウザで優先されるタスク(ユーザーの操作など)が必要に応じてその間に組み入れられるようにスケジュールされています。
<ph type="x-smartling-placeholder">これは、postTask()
の使用例の簡単な例です。必要に応じて異なる TaskController
インスタンスの優先度を変更する機能など、タスク間で優先度を共有可能な、さまざまな TaskController
オブジェクトをインスタンス化できます。
今後の scheduler.yield()
API を使用した継続を使用した組み込みの収益
<ph type="x-smartling-placeholder">
スケジューラ 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()
のメリットは継続です。つまり、一連のタスクの途中で収益を出すと、他のスケジュールされたタスクは、その収益ポイント以降も同じ順序で続行されます。これにより、サードパーティ スクリプトのコードがコードの実行順序を中断するのを防ぐことができます。
また、scheduler.postTask()
を priority: 'user-blocking'
とともに使用すると、user-blocking
の優先度が高いため継続する可能性が高くなります。当面は、このアプローチを代替として使用できます。
setTimeout()
(または scheduler.postTask()
で priority: 'user-visibile'
を指定するか、明示的な priority
を指定しない)を使用すると、タスクがキューの後ろにスケジュールされ、続行の前に他の保留中のタスクを実行できるようになります。
isInputPending()
を使用しない
対応ブラウザ
- 87
- 87
- x
- x
isInputPending()
API は、ユーザーがページを操作しようとしたかどうかを確認し、入力が保留中の場合にのみ離脱する手段を提供します。
これにより、保留中の入力がない場合に JavaScript が処理を続行できるようになり、処理が放棄されてタスクキューの最後方に送られることはなくなります。これにより、Intent to Ship で詳しく説明しているように、他の方法ではメインスレッドに戻れないサイトのパフォーマンスが大幅に向上します。
しかし、この API のリリース以降、特に INP の導入により、収益に関する Google の認識が高まっています。現在は、この API の使用はおすすめしません。代わりに、さまざまな理由から、入力が保留中かどうかに関係なく 明快にすることをおすすめします。
- ユーザーがなんらかの状況でなんらかの操作をしたにもかかわらず、
isInputPending()
が誤ってfalse
を返すことがあります。 - タスクが生成されるのは入力だけではありません。レスポンシブ ウェブページを提供するうえで、アニメーションやその他の定期的なユーザー インターフェースの更新も同様に重要です。
- その後、
scheduler.postTask()
やscheduler.yield()
など、生成する懸念に対応する、より包括的な収益生成 API が導入されました。
まとめ
タスクの管理は容易なことではありませんが、タスクの管理は容易なことから、ユーザーの操作に対してより迅速にページが反応するようになります。タスクの管理や優先順位付けに関する唯一のアドバイスはありませんが、むしろ、さまざまな手法があります。繰り返しになりますが、タスクを管理する際は、次の点を考慮する必要があります。
- 重要なユーザー向けタスクのメインスレッドに移る。
postTask()
を使用してタスクに優先順位を付ける。scheduler.yield()
でテストすることを検討してください。- 最後に、関数の処理量をできる限り少なくします。
これらのツールの 1 つ以上を使用すれば、ユーザーのニーズを優先しながら、重要性の低い作業は中断しないようにアプリケーションの作業を構造化できるはずです。これにより、ユーザー エクスペリエンスが向上し、応答性が向上し、使いやすくなります。
このガイドの技術的な調査に協力してくれた Philip Walton に感謝します。
サムネイル画像は Unsplash から提供。Amirali Mirhashemian 提供。