🚀

その処理、 setInterval じゃなくてブラウザが暇な時にやっちゃえば?

2023/02/06に公開

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

setInterval などで定期的にブラウザで実行させている処理があったりするでしょう。
例えば、定期的にアクセストークンが有効かチェックし、無効だったら新しいトークンを取得するなど。

Busy Man

ブラウザはページのコンテンツを表示するために JavaScript の実行や UI の更新で忙しいです。
もし定期実行している処理がコンテンツ表示にかかわらない優先度の低い処理なのであれば、 できるだけブラウザの重要なタスクに影響を与えないようにしたい ものです。

この記事では できるだけブラウザの重要なタスクに影響を与えずに定期的に処理を実行させる方法を、アクセストークンのローテーションを例に紹介 します。

実装する機能

この記事で実装する機能は次の通りです。 React の hooks を使って実装します。

  1. ブラウザのアイドル中(暇なとき)にアクセストークンを有効かチェックし、無効だったら新しいトークンを取得する
  2. 定期的に 1 の処理を実行させるために、 1 の処理が完了しても再度 1 を実行するように数秒後にスケジューリングする
  3. ブラウザが忙しくアイドル状態にならない場合を考慮して、数秒に 1 回、アクセストークンチェックが走ってない時はチェックを強制実行する

ブラウザのアイドル中に処理を実行させるために idle-task (v3.3.3)というライブラリを使います。

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

このライブラリは内部的に requestIdleCallback という Web API を使っています。
requestIdleCallback については次の記事で詳しく解説しています。

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

ソースコード

ソースコードをお見せする前に、まずは今回実装する画面を先にお見せします。

https://8hic7w.csb.app/

画面をしばらく放置しておくと、「 now token is ~ 」の箇所で新しいトークンが表示されます。

実装はどんな感じでしょうか? React の hooks の実装は次の通りです。

import { useEffect, useState } from "react";
import { setIdleTask, cancelIdleTask } from "idle-task";

interface MockResponse {
  readonly status: number;
  readonly json: () => Promise<any>;
}

const mockFetchCheckAccessToken = async (
  _token: string
): Promise<MockResponse> => {
  if (Math.random() < 0.3) {
    return {
      status: 401,
      json: async () => null
    };
  }
  return {
    status: 200,
    json: async () => null
  };
};

const mockFetchRefreshToken = async (
  _oldToken: string
): Promise<MockResponse> => {
  return {
    status: 200,
    json: async () =>
      [...Array(10)].map(() => Math.floor(Math.random() * 10)).join("")
  };
};

const useAccessTokenWhenIdle = (): string => {
  const [nowToken, setNowToken] = useState("none");

  useEffect(() => {
    const checkAccessToken = async (): Promise<void> => {
      const checkTokenResponse = await mockFetchCheckAccessToken(nowToken);
      if (checkTokenResponse.status === 200) {
        return;
      }
      const newTokenResponse = await mockFetchRefreshToken(nowToken);
      const newToken = await newTokenResponse.json();
      console.log("refresh", newToken);
      setNowToken(newToken);
    };
    const idleTaskId = setIdleTask(checkAccessToken, {
      revalidateInterval: 5000
    });
    return () => {
      cancelIdleTask(idleTaskId);
    };
  }, [nowToken]);

  return nowToken;
};

export default useAccessTokenWhenIdle;

ポイントとなるコードはここです。

    const idleTaskId = setIdleTask(checkAccessToken, {
      revalidateInterval: 5000
    });

setIdleTaskでブラウザのアイドル中に実行したい関数を登録 できます。
ここでは checkAccessToken はブラウザのアイドル中に実行されます。

オプションには revalidateInterval が指定されています。
これは 指定した時間が経つと再度ブラウザのアイドル中に関数が実行されるようにスケジューリング されます。
この例では「 5 秒後に checkAccessToken がブラウザのアイドル中に実行されるようにスケジューリングする」という意味になります。

このように setIdleTaskrevalidateInterval を指定することで定期的にブラウザのアイドル中に処理を実行できます。

  1. ブラウザのアイドル中(暇なとき)にアクセストークンを有効かチェックし、無効だったら新しいトークンを取得する
  2. 定期的に 1 の処理を実行させるために、 1 の処理が完了しても再度 1 を実行するように数秒後にスケジューリングする

実装する機能 に書いた 1 と 2 が実装できました。
1 と 2 だけでも十分ですが懸念もあります。
それは「 ブラウザが忙しくて必ずしもアイドル状態になるとは限らない 」ということです。

  1. ブラウザが忙しくアイドル状態にならない場合を考慮して、数秒に 1 回、アクセストークンチェックが走ってない時はチェックを強制実行する

できるだけ処理の実行を保証するために 3 を実装します。

useAccessTokenWhenIdle hooks の利用側のコードを見てみましょう。

import "./styles.css";
import useAccessTokenWhenIdle from "./useAccessTokenWhenIdle";
import { configureIdleTask } from "idle-task";

configureIdleTask({
  debug: true,
  interval: 1000 * 10
});

export default function App() {
  const token = useAccessTokenWhenIdle();

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      Now token is {token}
    </div>
  );
}

ポイントはこのコードです。

configureIdleTask({
  debug: true,
  interval: 1000 * 10
});

configureIdleTaskinterval を指定しています。
これは 指定した時間ごとに setIdleTask で登録された関数を、できるだけブラウザのパフォーマンスに影響を与えない範囲で実行 します。
requestIdleCallbacktimeout オプション に似ています)

この例では 10 秒に 1 回、 setIdleTask で登録された checkAccessTokenidle-task が管理するキューに残っていた場合は、ブラウザのパフォーマンスに影響を与えない範囲で実行します。

できるだけ処理の実行を保証したい場合はこのように configureIdleTaskinterval を指定することで解決 します。

  1. ブラウザが忙しくアイドル状態にならない場合を考慮して、数秒に 1 回、アクセストークンチェックが走ってない時はチェックを強制実行する

ということで 3 の実装も完了しました!🎉

まとめ

定期的にブラウザのアイドル中にアクセストークンをローテーションする実装例を紹介しました。

アクセストークンのローテーション以外にも、例えばユーザーが編集中の記事を下書き保存する場合にも使えるでしょう。

もし idle-task でバグや質問などあれば GitHub の issue または記事にコメントください。ついでに Github Star ⭐️ ももらえると嬉しいです!

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

他にもブラウザのアイドル中にフォームをバリデーションしてパフォーマンス最適化する例も紹介してるので、興味ある方はぜひ。

https://zenn.dev/nuko_suke_dev/articles/79a5d061f0eb84

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

Discussion