ブラウザのアイドル中にJavaScriptを実行する良い感じのOSSを公開した
こんにちは。ぬこすけです。
console.log
や setTimeout
など、ブラウザにはたくさんの機能が備わっています。
そのうちの 1 つに requestIdleCallback
というものがあります。
requestIdleCallback
については上の記事で詳しく解説していますが、簡単に説明すると「ブラウザが暇な時に JavaScript の処理を実行させられる」というのが requestIdleCallback
です。
requestIdleCallback
により、ブラウザが暇な時に JavaScript の処理を回すことでパフォーマンス最適化ができるのです。
この requestIdleCallback
をより使いやすくした idle-task
という OSS を公開しました。
idle-task
の特徴
idle-task
には次の 4 つの特徴があります。
- タスクの優先度を指定できる
- 実行結果を取得できる
- 結果をキャッシュ
- タスクの実行時間を最適化
1. タスクの優先度を指定できる
優先度の指定は できるだけ実行を保証 するものです。
前提としてブラウザのアイドル中(ひまな時)に JavaScript (タスク)を実行するにしても、そもそも ブラウザが忙しい場合は実行されるとは限りません 。
タスクの中でも「このタスクはできるだけ実行してほしい」「このタスクは最悪実行されなくも OK 」みたいな分類ができると思います。
なので、 requestIdleCallback
を使う場合はできればタスクの優先順位を指定できるようにしたいところです。
ですが、 requestIdleCallback
で実行タスクの優先順位を管理しようと思うと、自前で実装する必要があり、かなり大変です。
idle-task
では実行タスクの優先順位を管理する仕組みが実装されています。
import { setIdleTask } from 'idle-task';
setIdleTask(() => console.log('優先度の低いタスク'), { prioriy: 'low' });
setIdleTask(() => console.log('優先度の高いタスク'), { prioriy: 'high' });
使い道としては例えば分析用のデータをブラウザのアイドル中に送信する時に ページビューのデータは優先度高、ボタンのクリック等のデータは優先度低でデータを送信 のようなことができます。
2. 実行結果を取得できる
基本的に requestIdleCallback
は戻り値のないタスクを登録し、結果は取得できません。
idle-task
では実行結果を取得することができます 。
import { getResultFromIdleTask } from 'idle-task';
// ブラウザのアイドル期間中にユーザーのアクセストークンをチェックする
const checkAccessTokenWhenIdle = (accessToken: string): Promise<any> => {
const fetchCheckAccessToken = async (): Promise<any> => {
const response = await fetch(`https://yourdomain/api/check?accessToken=${accessToken}`);
// JavaScript の仕様上、 Promise のコールバックはマイクロタスクとして即時実行されるため、レスポンスをいじりたい場合は再度アイドル期間に実行するように推奨。
return getResultFromIdleTask(() => response.json());
};
return getResultFromIdleTask(fetchCheckAccessToken);
}
const { isSuccess } = await checkAccessTokenWhenIdle('1234');
getResultFromIdleTask
を使うことでブラウザのアイドル期間中に実行した結果を取得することができます。
この例ではブラウザのアイドル中にユーザーのアクセストークンチェックをしています。
fetch
後の処理について、 Promise のコールバックはマイクロタスクとして Promise 解決後すぐに実行されてしまいます。
場合によりけりですが、コールバックの処理時間が長かったり、同期的な処理であるようであれば例のようにさらに getResultFromIdleTask
を使って fetch
後のレスポンスを加工する処理をアイドル中に回すようにするのも良いでしょう。
マイクロタスクについては難しいですが、次の記事が参考になります。
3. 結果をキャッシュ
idle-task
では実行結果を自動でキャッシュします 。
import { setIdleTask } from 'idle-task';
const taskId = setIdleTask(() => import('./sendAnalyticsData'))
const button = document.getElementById('button');
button.addEventListener('click', async () => {
const { sendButtonEvent } = await waitForIdleTask(taskId);
setIdleTask(sendButtonEvent, { cache: false });
})
const anchor = document.getElementById('anchor');
anchor.addEventListener('click', async () => {
const { sendAnchorEvent } = await waitForIdleTask(taskId);
setIdleTask(sendAnchorEvent, { cache: false });
})
この例では、まず 動的 import と setIdleTask
を使い、ブラウザのアイドル中にモジュールを読み込みをするようにしています。
次に button
と anchor
の DOM に対して分析データの送信処理をクリックイベントに紐づけています。
最初の setIdleTask
で発行した ID を元に waitForIdleTask
で必要な関数を呼び出し、さらに setIdleTask
でブラウザのアイドル中に実行させています。
2 箇所で waitForIdleTask
を使いモジュールの読み込み結果を参照していますが、どちらも同じ実行結果から参照されています。
idle-task
内で実行結果をキャッシュしているためです。
このように idle-task
では実行結果をキャッシュします 。
余談ですが、 setIdleTask
の第二引数に { cache: false }
を指定しているのは、第一引数で渡した関数の実行結果は不要なのでキャッシュを削除するためです。
4. タスクの実行時間を最適化
タスクの実行は 50 ミリ秒が推奨 されています。これは W3C のドキュメント にも言及があります。
簡単に言えば、 50 ミリ秒でタスクが実行が完了されればユーザーは早いと感じるよね、という目安です。
idle-task
ではタスクの実行を最適化 します。
import { setIdleTask } from 'idle-task';
setIdleTask(task1); // 60 ミリ秒かかった
setIdleTask(task2); // 10 ミリ秒かかった
setIdleTask(task3); // 10 ミリ秒かかった
この例ではブラウザのアイドル中に実行した最初のタスク task1
が 60 ミリ秒かかってしまいました。
他にアイドル中に実行するように登録しているタスクを実行してしまうと、ユーザーの快適な Web サイトの閲覧に影響が出てしまいます。
idle-task
は自動的に次のブラウザのアイドル期間中に実行するようにタスクを後回しにしてくれます 。
task2
と task3
は次のブラウザのアイドル中に実行されます。
このように idle-task
ではタスクの実行を最適化 してくれます。
さいごに
こちらの記事 を書いている時に、「こういう OSS があったら面白いんじゃね?」と思って、気づいたら npm で公開していました笑。
他にも色々機能はあるのですが、主要な機能だけ紹介させていただきました。
詳しく知りたい方はぜひ README.md
を見ていただければと思います!
もしバグや質問などあれば日本語でもかまわないので issue などに記載していただければと思います。
ここまでご覧いただきありがとうございました!
Discussion