🚀

【React】ブラウザのアイドル中にフォームをバリデーションしてパフォーマンス最適化🚀

2023/01/30に公開

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

入力フォームでユーザーが入力した値をどのタイミングでバリデーションしていますか?🤔
リアルタイミングでバリデーションしたり、あるいは「入力内容を確認」みたいなボタンをタップした直後にバリデーションしているのではないでしょうか?

このようなタイミングでのバリデーションはいたって普通ですが、今回は この他のタイミングでのバリデーションを提案したい と思います。そのタイミングは ブラウザのアイドル中(ひまな時) です。

実装イメージとしては ユーザーの入力したデータをブラウザのアイドル中にバリデーションしておき、「入力内容を確認」みたいなボタンをタップした時にはすでにバリーデーションの結果が使える、という感じです。
こうすることで 「入力内容を確認」みたいなボタンをタップした時の応答が早くなります
( Interaction to Next Paint の改善にもつながるでしょう)

ブラウザにはアイドル中に JavaScript を実行できる requestIdleCallback という Web API があります。

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

この記事では requestIdleCallback を良い感じにラップした idle-task (v3.3.1)というライブラリを使ってブラウザのアイドル中にフォームをバリデーションの実装例 を紹介します。

なお、 React でコードを書きます。

React での実装例

React の hooks を使って、ブラウザのアイドル中にバリデーションを実行するロジックを実装します。

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

export default function useValidateWhenIdle(
  input: string,
  validate: (input: string) => boolean
) {
  const idleTaskIdRef = useRef(NaN);

  useEffect(() => {
    const validateTask = () => validate(input);
    idleTaskIdRef.current = setIdleTask(validateTask);
    return () => {
      cancelIdleTask(idleTaskIdRef.current);
    };
  }, [input, validate]);

  return () => forceRunIdleTask(idleTaskIdRef.current);
}

この hooks を利用する側のコンポーネントは次のようになります。

import "./styles.css";
import useValidateWhenIdle from "./useValidateWhenIdle";
import { useState, ChangeEventHandler, FormEventHandler } from "react";
import { configureIdleTask } from "idle-task";

configureIdleTask({
  debug: true
});

const validateUserName = (userName: string): boolean => {
  return /^[a-zA-Z]+$/.test(userName);
};

export default function App() {
  const [userName, setUserName] = useState("");
  const validateUserNameWhenIdle = useValidateWhenIdle(userName, validateUserName);
  const handleUserNameChange: ChangeEventHandler<HTMLInputElement> = ({
    target
  }) => {
    setUserName(target.value);
  };
  const handleSubmit: FormEventHandler<HTMLFormElement> = async (event) => {
    event.preventDefault();
    const isValid = await validateUserNameWhenIdle();
    if (isValid) {
      alert(`YourName: ${userName}`);
    } else {
      alert(`${userName} is invalid`);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        FirstName:
        <input type="text" value={userName} onChange={handleUserNameChange} />
      </label>
      <input type="submit" value="Submit" />
    </form>
  );
}

まずは useValidateWhenIdle の hooks を詳しく見てみましょう。

useValidateWhenIdle は 2 つの引数を受け取ります。

  • input : バリデーション対象の文字列
  • validate : バリデーション対象の文字列を受け取ってバリデーションする関数

そして結果はバリデーション結果の Promise です。

さらに内部の処理まで詳しくみます。

  const idleTaskIdRef = useRef(NaN);

  useEffect(() => {
    const validateTask = () => validate(input);
    idleTaskIdRef.current = setIdleTask(validateTask);
    return () => {
      cancelIdleTask(idleTaskIdRef.current);
    };
  }, [input, validate]);

idle-tasksetIdleTask は、引数の関数をブラウザのアイドル中に実行させます。
setIdleTask(validateTask)validateTask というユーザーの入力した値をバリデーションする関数をブラウザのアイドル中に実行させるわけです。

setIdleTask の戻り値は ID です。
この ID を使ってアイドル中の関数の実行をキャンセルしたり、関数の結果を取得することができます。

ID は useRef を使って値を管理しておきます。
そして、 useEffect を使って、ユーザーが入力した値が変更される度にブラウザのアイドル中にバリデーションが実行されるようにします。
useEffect のクリーンナップ関数では cancelIdleTask と使い、まだ対象の処理が実行されていない場合はキャンセルします。

  return () => forceRunIdleTask(idleTaskIdRef.current);

setIdleTask で登録した関数の結果は forceRunIdleTask で取得できます。
もし対象の関数がすでにブラウザのアイドル中に実行されていたらその結果を返し、まだ実行されていなかった場合は即時実行します。

続いて useValidateWhenIdle の hooks を使う側のコンポーネントを詳しく見てみましょう。

import "./styles.css";
import useValidateWhenIdle from "./useValidateWhenIdle";
import { useState, ChangeEventHandler, FormEventHandler } from "react";
import { configureIdleTask } from "idle-task";

configureIdleTask({
  debug: true
});

const validateUserName = (userName: string): boolean => {
  return /^[a-zA-Z]+$/.test(userName);
};

export default function App() {
  const [userName, setUserName] = useState("");
  const validateUserNameWhenIdle = useValidateWhenIdle(userName, validateUserName);
  const handleUserNameChange: ChangeEventHandler<HTMLInputElement> = ({
    target
  }) => {
    setUserName(target.value);
  };
  const handleSubmit: FormEventHandler<HTMLFormElement> = async (event) => {
    event.preventDefault();
    const isValid = await validateUserNameWhenIdle();
    if (isValid) {
      alert(`YourName: ${userName}`);
    } else {
      alert(`${userName} is invalid`);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <label>
        FirstName:
        <input type="text" value={userName} onChange={handleUserNameChange} />
      </label>
      <input type="submit" value="Submit" />
    </form>
  );
}

React の基本的なフォームの実装例です。
useState を使ってユーザーに入力させたいデータを状態管理します。
handleUserNameChange というユーザーが入力した時に発火する関数を用意し、 input タグに紐づけます。
そして Submit ボタンが押されたら handleSubmit が実行されます。

さて、ここで注目すべきポイントを取り上げます。

const validateUserNameWhenIdle = useValidateWhenIdle(userName, validateUserName);

先ほど用意した useValidateWhenIdle が出てきました。
第一引数にバリデーションの対象となる文字列、第二引数にバリデーションのロジックを定義した関数が指定されています。
そして戻り値には関数が返ってきています。
バリーデーション結果の Promise は次のように Submit ボタンが押したときに使います。

  const handleSubmit: FormEventHandler<HTMLFormElement> = async (event) => {
    event.preventDefault();
    const isValid = await validateUserNameWhenIdle();
    if (isValid) {
      alert(`YourName: ${userName}`);
    } else {
      alert(`${userName} is invalid`);
    }
  };

Submit ボタンが押したときに async/await を使ってバリデーション結果を取得しています。
その結果を元に alert を出しているわけです。

今まで紹介したコードで実装したページは次の URL で確認できます。

https://kzjljm.csb.app/

実際にブラウザのアイドル中にバリデーションが行われているか見てみましょう!
次のコードを追加することで、バリデーション関数の実施結果がブラウザのコンソールに出力されます。

import { configureIdleTask } from "idle-task";

configureIdleTask({
  debug: true
});

先ほど紹介したページで入力エリアにテキトーな文字を入れると、バリデーションがアイドル中に行われていることがわかります。

ブラウザのコンソールに出力されたバリデーション関数の実施結果
(数値は丸められるので 0 ms のようになっています)

また Chrome ディベロッパーツールの Performance タブから計測すると、ブラウザのアイドル中に関数が動いていることがわかります。

Performanceタブによる計測結果

このようにバリデーションがブラウザのアイドル中に行われている様子がわかりました。

今までで紹介したコード例は CodeSandbox に載せています。

まとめ

リアルタイミングでバリデーション、あるいは「入力内容を確認」みたいなボタンをタップした直後にバリデーションする以外にも、ブラウザのアイドル中にバリデーションをかましておく方法をご紹介しました。

今回紹介した idle-task というライブラリでもしバグや質問などあれば Github の issue なり この記事でコメントいただければと思います。日本語で OK です!
ついでに Github Star ⭐をくれると嬉しいです!

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

よりフロントエンドのパフォーマンス最適化に興味がある方はこちらの記事もぜひ参考にしてみてください。

https://qiita.com/nuko-suke/items/50ba4e35289e98d95753

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

Discussion