自作サイトを自作OSSで5%クリック速度を向上させた話
こんにちは。ぬこすけです。
最近(2022/11/24)、ブラウザのアイドル中にJavaScriptを実行する良い感じの OSS を公開したお話を記事にしました。
この OSS を使って個人開発したサイトに適用してどのくらいパフォーマンス最適化できるか試してみました!
どんなライブラリを自作したの?
idle-task
という requestIdleCallback
を良い感じにラップしたライブラリです。
requestIdleCallback
については次の記事でくわしく解説しています。
idle-task
にはたくさん機能があるので、詳細については Github の README だったり、 Zenn の記事 を見てもらいたいですが、例えば次のようなことができます。
import { getResultFromIdleTask } from 'idle-task';
// ブラウザのアイドル中に実行される yourFunction の結果を取得
const result = await getResultFromIdleTask(yourFunction);
戻り値のある関数を requestIdleCallback
に登録し、結果を取得しようとすると結構実装が面倒です。
idle-task
はそこらへんを良い感じにラップしてくれています。
自作したサイトってどんなサイト?
この idle-task
の実験台の対象となるのは「 Instagram Hashtag Translator 」 というサイトです。
どんなサイトかいうと、 インスタグラムの投稿で外国語でハッシュタグをつけたい時に、インスタグラム向けにハッシュタグを翻訳し、コピペして投稿できるようにするツール です。
例えば、「猫 カフェ」で英語も含めてハッシュタグを用意したい場合は、「#猫 #カフェ #cat #cafe」という形でアウトプットを出してくれます。
企画の背景や技術スタックなど、詳しい話は記事にまとめているので、ご興味あればご覧ください。
これから本題のパフォーマンス改善のお話に移りますが、その前に Instagram Hashtag Translator の簡単な前提条件を確認しておきましょう。
- 使っている主なライブラリとバージョン
- React v18.2.0
- Next.js v13.0.5
-
app
Directory といった v13 の最新機能は使っていません。
-
- インフラ
- Vercel
そして今回パフォーマンス改善の対象となるのは、次のモバイルで翻訳ボタンを押した時に発火するクリックイベントになります。
先ほどの動画のように、「猫 カフェ」を入力した状態で翻訳ボタンを押します。
計測については、クリック時の処理の最初と最後に performance
API を挟んで計測します。
const start = performance.now();
// ...
// 色々処理
// ...
// かかった時間を出力。小数点は良い感じに丸める。
console.log(Math.ceil((performance.now() - start) * 100) / 100);
前提条件を確認したところで、いよいよ本題に入りたいと思います!
idle-task
を使う前の実装
idle-task
を使う前の翻訳ボタンをクリックした時の処理は次のようになっています。
(かなり簡略化はしてます)
const clickHandler = async () => {
// 分析データ送信用モジュール読み込み
const { default: sendGaEvent } = await import(
'~/lib/analytics/sendGaEvent'
);
// ...
// 分析データ送信
sendGaEvent(data);
// ...
// 翻訳用モジュール読み込み
const { default: translate } = await import('~/infra/translate');
// ...
}
ここでのポイントは 2 あります。
- 動的 import を使ってクリック時にモジュールを遅延読み込みさせる
- 分析用のデータを送信している
1. 動的 import を使ってクリック時にモジュールを遅延読み込みさせる
動的 import はモジュールを遅延的に読み込みさせることができます。
先ほどあげたコード例で言うと、ユーザーがクリックして初めて sendGaEvent
や translate
の JavaScript ファイルを取得し、スクリプトを読み込みます。
言い換えれば、この動的 import を使うことで 初回読み込みが早くなる メリットが受けられます。
しかし、逆に言えばユーザーがクリックして初めてモジュールが読み込まれるので、 クリックした時の処理が遅くなります。これはデメリットです。
2. 分析用のデータを送信している
コード例にあげた処理は、ユーザーが入力した値を元に翻訳するのがメインの処理です。
この処理結果を元に画面が更新されるので重要な処理です。
一方で 分析用のデータの送信はそこまで重要な処理ではありません 。
分析用データの送信を元に画面を更新するわけでもないですし、できればメインの翻訳処理のお邪魔にならないようにしたいところです。
以上の 2 つのポイントについて、次のように問題を整理できます。
- 動的 import を使ってクリック時にモジュールを遅延読み込みさせる
🥲 クリック後にモジュール読み込みが走って遅くなる - 分析用のデータを送信している
🥲 メインの翻訳処理とは関係のない処理で余計に遅くなる
これらの問題を idle-task
を使って解決します。
idle-task
を使った後の実装
idle-task
を使って次のようにコードを書き換えました。
import { setIdleTask, forceRunIdleTask } from 'idle-task';
// ポイント①
const importSendGaEventTaskId = setIdleTask(
() => import('~/common/analytics/sendGaEvent')
);
const importTranslateTaskId = setIdleTask(() => import('~/infra/translate'));
const clickHandler = async () => {
// ポイント①
const { default: sendGaEvent } = await forceRunIdleTask(
importSendGaEventTaskId
);
// ...
// ポイント②
setIdleTask(() => sendGaEvent(data));
// ...
// ポイント①
const { default: translate } = await forceRunIdleTask(importTranslateTaskId);
// ...
}
コメントアウトにポイントを書いていますが、それぞれ詳しく見ていきましょう。
ポイント①
ブラウザのアイドル中にモジュール読み込みをキックしておきます 。
const importSendGaEventTaskId = setIdleTask(
() => import('~/common/analytics/sendGaEvent')
);
const importTranslateTaskId = setIdleTask(() => import('~/infra/translate'));
まず、 setIdleTask
という関数を使ってブラウザのアイドル中に実行したい処理を登録 します。
この例では sendGaEvent
と translate
モジュールをブラウザのアイドル中に読み込みを開始しておくように指示しておきます。
const { default: sendGaEvent } = await forceRunIdleTask(
importSendGaEventTaskId
);
const { default: translate } = await forceRunIdleTask(importTranslateTaskId);
// ...
}
アイドル中に実行した関数の結果は forceRunIdleTask
を使って取得しています。
ブラウザが忙しい場合は必ずしもアイドル中になるとは限りません。
forceRunIdleTask
を使うことでアイドル中に登録した関数が実行されていればその結果を返し、されていなければ即時に実行します。
余談ですが、 idle-task
は waitForIdleTask
という、同じくアイドル中に実行した関数の結果を取得する API を用意しています。
こちらは forceRunIdleTask
と違い、アイドル中に登録した関数が実行されるのを待ちます。
ポイント②
分析用のデータを送信はブラウザのアイドル中に実行させます 。
setIdleTask(() => sendGaEvent(data));
ポイント① でもお話した通り、 setIdleTask
を使うことでブラウザのアイドル中に実行したい処理を登録 できます。
メインの翻訳処理を優先したいため、分析用のデータを送信はブラウザのアイドル中に実行させるように遅延させます。
さて、今までの話を整理すると次のようになります。
- 動的 import を使ってクリック時にモジュールを遅延読み込みさせる
🥲 クリック後にモジュール読み込みが走って遅くなる
😄 ブラウザのアイドル中に事前に読み込みさせておくことで解決 - 分析用のデータを送信している
🥲 メインの翻訳処理とは関係のない処理で余計に遅くなる
😄 ブラウザのアイドル中に処理を実行するように後回しすることで解決
これで問題は解決したようです!
その他にも idle-task
を使って細かいチューニングをしていますが、主な実装のポイントとしては以上になります!
実際、どのくらい翻訳ボタンをクリックした時の処理が早くなったかを見ていきます!
計測結果
実装前 | 実装後 | |
---|---|---|
1回目 | 1110.8 | 1048.8 |
2回目 | 1191.2 | 1068.9 |
3回目 | 1070.4 | 1028.4 |
4回目 | 1084.8 | 1023.2 |
5回目 | 1099.4 | 1080.8 |
平均 | 1111.3 | 1050.0 |
※単位はミリ秒です。
※キャッシュは削除して計測しています。
5.5% ほど改善しました🎉。
実はメインの翻訳の処理自体が 1 秒(= 1000 ミリ秒)くらいかかっているので、メイン処理以外という枠組みで考えると 55% ほど改善したということになります。
絶対値でいうと 50 ~ 60 ミリ秒の改善 で、一見して数値的にはたいしたことないように見えますが、 50 ~ 60 ミリ秒程度の改善は大きい です。
RAILモデル に照らし合わせると、大体 100 ミリ秒以下(ガイドライン的には 50 ミリ秒での実行が推奨)だとユーザーはサクサクに感じます 。
そう考えると 50 ~ 60 ミリ秒程度の改善は大きいのではないでしょうか。
まとめ
自分で作ったサイト「 Instagram Hashtag Translator 」 を、これもまた自分で作ったライブラリ「 idle-task
」を使い、ブラウザのアイドル中に処理を実行させることでパフォーマンスチューニングをするお話でした。
ぜひ idle-task
を使ってみてください!
もしバグや使い方がわからない場合は日本語でも OK なので、 Github の issue なりにコメントいただければと思います!
また、別の自作ライブラリで改善した話も書いているので、よかったら覗いてみてください!
ここまで読んでいただきありがとうございました! by ぬこすけ
Discussion