🚀

ブラウザのアイドル中にJavaScriptを実行する良い感じのOSSを公開した

2022/11/24に公開

こんにちは。ぬこすけです。

console.logsetTimeout など、ブラウザにはたくさんの機能が備わっています。
そのうちの 1 つに requestIdleCallback というものがあります。

https://qiita.com/nuko-suke/items/c8c31ee34fd539805910

requestIdleCallback については上の記事で詳しく解説していますが、簡単に説明すると「ブラウザが暇な時に JavaScript の処理を実行させられる」というのが requestIdleCallback です。

requestIdleCallback により、ブラウザが暇な時に JavaScript の処理を回すことでパフォーマンス最適化ができるのです。

この requestIdleCallback をより使いやすくした idle-task という OSS を公開しました。

https://www.npmjs.com/package/idle-task

idle-task の特徴

idle-task には次の 4 つの特徴があります。

  1. タスクの優先度を指定できる
  2. 実行結果を取得できる
  3. 結果をキャッシュ
  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 後のレスポンスを加工する処理をアイドル中に回すようにするのも良いでしょう。

マイクロタスクについては難しいですが、次の記事が参考になります。

https://developer.mozilla.org/ja/docs/Web/API/HTML_DOM_API/Microtask_guide

https://ja.javascript.info/microtask-queue

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 });
})

この例では、まず 動的 importsetIdleTask を使い、ブラウザのアイドル中にモジュールを読み込みをするようにしています。

次に buttonanchor の 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 は自動的に次のブラウザのアイドル期間中に実行するようにタスクを後回しにしてくれます
task2task3 は次のブラウザのアイドル中に実行されます。

このように idle-task ではタスクの実行を最適化 してくれます。

さいごに

こちらの記事 を書いている時に、「こういう OSS があったら面白いんじゃね?」と思って、気づいたら npm で公開していました笑。

https://qiita.com/nuko-suke/items/c8c31ee34fd539805910

他にも色々機能はあるのですが、主要な機能だけ紹介させていただきました。
詳しく知りたい方はぜひ README.md を見ていただければと思います!

https://github.com/hiroki0525/idle-task

もしバグや質問などあれば日本語でもかまわないので issue などに記載していただければと思います。

ここまでご覧いただきありがとうございました!

Discussion