UIがイベントを受け付けてくれないのだが
JavaScriptはシングルスレッドなので、重い処理がスレッドのCPUリソースを占有してしまうと、他のイベントをくけつけてくれなくなってしまう。読者諸君はそんな経験はないだろうか。
WebWorkerなどを使って複数のスレッドに分けて並列処理を行うなどの方法もあるが、今回はrequestIdleCallback()などを使って処理に優先順位をつける、優先的に画面イベントが実行されるような実装を紹介したい。
重い処理を疑似的に作ってみる
では検証のために重い処理をやったときのサンプルを作ってみる。
<html>
<body>
優先順位は低いけど重い処理のスタートボタン<br/>
<button id="start" onClick="startTask();" >start</button><br/><br/>
時計を表示する<br/>
<div id="clock"></div><br/>
あいさつを表示</br>
<div id="hello"></div>
<button id="eng" onClick='showHello("Hello")' >英語</button>
<button id="japan" onClick='showHello("こんにちは!")' >日本語</button><br/><br/>
<script>
//1.タスク定義
function task(ms, taskId) {
const startTime = performance.now();
//ループ処理をmsの間実施する
while (performance.now() - startTime < ms);
console.log("task : " + taskId + " done");
}
//2.タスク実行
function runTasks(taskList){
while (taskList.length > 0) {
const task = taskList.shift();
task();
}
}
//3.startボタン イベント
function startTask() {
//処理時間60msのタスクを100個タスクリストに積む
const taskList = [];
for(let i = 0 ; i < 100; i++) {
taskList.push(
() => task(60, i));
}
//タスク実行
runTasks(taskList);
}
//4.時計設定
function showClock1() {
var nowTime = new Date();
var nowHour = String(nowTime.getHours()).padStart( 2, '0');
var nowMin = String(nowTime.getMinutes()).padStart( 2, '0');
var nowSec = String(nowTime.getSeconds()).padStart( 2, '0');
var msg = nowHour + ":" + nowMin + ":" + nowSec ;
console.log(msg);
document.getElementById("clock").innerHTML = msg;
}
setInterval('showClock1()',1000);
//5. 挨拶を表示
function showHello(msg) {
document.getElementById("hello").innerHTML= msg;
}
</script>
</body>
</html>
処理の簡単な説明を行う。
- 1 タスクの定義、引数ms(マイクロ秒)だけループを実施する。
- 2 タスクの実行、タスクリストの配列を順番に実行する処理。
- 3 startボタンを押したときに、100個のタスクをタスクリストに積み込みタスクを実行させる。
- これによて、UA(UserAgent)のスレッドは重い処理で占有されてしまう。押しては危険なボタン。
- 4 時計設定 画面イベントが止まっているのを視覚的に表せるために、画面上に毎秒時刻を表示させる。
- 5 ユーザーが行うボタンイベントで実施される処理。この例では日本語と英語の挨拶が素敵に表示される。
startボタンを押すと、UAは、100個のタスクの実行に追われてしまい。時計の描画がとまり、ボタンイベントも受け付けない悲しい状態になる。
スレッドの様子
その時のスレッドの様子を開発者ツールで見てみよう。
きっちり6秒程度スレッドが占有されているのが分かる。この間は画面描画や、ボタンイベントを受け付けない状態になる。ユーザーはさぞかしストレスがたまるであろう。
requestIdleCallback()を使用して改善しよう
requestIdelCllback()とは?
requestIdelCllback()は1995年にGoogleにより最初のDraftが提案されたAPIである。残念ながら2022年12月時点でもDraft段階である。
しかしながら、Safari以外の主なブラウザーで既に実装ずみであり、ブラウザーを限定してもよいときであれば使用を検討できる。
お試し1
runTasks()の内部処理を以下の様にrequestIdleCallback()をつかって指定する。
//2.タスク実行
function runTasks(taskList){
requestIdleCallback(() => {
// 実行するタスクが残っていて
while (taskList.length > 0) {
const task = taskList.shift();
task();
}
});
}
これでは残念ながらうまくいかない。
requestIdleCallback()に指定したCallBack関数のタスク処理が始まれば、きっちり6秒間処理を続けてしまうからだ。どうにか、ループ処理実施中に処理を止めることはできないだろうか。
お試しその2
こういう時のためにrequestIdelCallback()は、どのぐらいの時間Callback関数を実施できる猶予があるか、実行時に渡すよう仕様が決められている。
//2.タスク実行
function runTasks(taskList){
requestIdleCallback((deadline) => {
// 実行するタスクが残っていて、deadlineがまだ残っているときに実行する(※1)
while (taskList.length > 0 && deadline.timeRemaining() > 0) {
const task = taskList.shift();
task();
}
// まだタスクが残っている場合は、再び runTasks を実行する(※2)
if (taskList.length > 0) {
runTasks(taskList);
}
});
}
deadline.timeRemaining()を使用すると、後どのぐらいこの処理を実施しつづけている猶予があるかCallBack関数内で知ることが出来る。
(※1)のソースの様にすれば、猶予があるときがだけタスクリストの処理が実施される様に出来る。
タスクリスト配列に未実施で残されたタスクは、(※2)の様に再帰的にrunTasks()を呼び出し、再びブラウザーに余裕が出来るときまで、CallBack関数はキューイングされる。
結果
このソースであれば、startボタンを押しても、時計は毎秒表示され続け、挨拶表示ボタンも常に動作することが出来る。
タスク処理中も、合間を縫って優先順位の高い画面イベント処理を続けることが出来た。
再びスレッドの様子
このソース実行中の様子を、再度開発者ツールで表示する。
スクリプト処理が小さな細切れになっていることが分かる。また、タスクの合間を縫ってボタンイベントの処理が実行されているのが分かる。
まとめ
有効性
以上のようなサンプルで、このAPIの使いどころを示すことが出来た。もしイベントを受け付けないUIがあれば検討しては如何だろうか。
文中書いたが、2022年の段階では一応Draft段階なので、使用する際には今後仕様が変更されてしまうリスクは0ではない。また、Safariでの実装がなされていないので、対象ブラウザーの非機能要件も確認する必要がある。
この様に使用する上でマイナスポイントがあるのだが、それをクリアできるのであれば十分検討に値すると思われる。
マイクロタスク
賢明な読者は気づいたかもしれないが、この例でこのAPIに渡したCallBack処理のタスクが一つ一つは、小さな処理時間の単位になっている必要がある。
今回のサンプルを、3.タスク実行にて、処理時間を2000(マイクロ秒)にして実行して検証してみたが、時計は2秒間止まってしまい、挨拶ボタンも反応が鈍くなってしまうことが確認された。
W3Cで提案されている仕様では、このAPIのdeadlineの最大値は50msにすべきであるとされている。
the length of these idle periods should be capped to a maximum value of 50ms.
つまり、このAPIに渡すタスクリストの一つ一つは50ms以下の処理になっていることが望ましい。
使用する際に各タスクの処理時間をあらかじめ計測し、必要があればタスクを分割などを考えないと、思ったように効果が出ないことがあるのは注意する必要がある。