こんにちは。ぬこすけ です。
皆さんは「 setTimeout
とはどんな関数でしょう?」と聞いたら、どう答えますか?
おそらく、ほとんどの人が「指定した時間に処理が走るようにする関数」と答えるのではないでしょうか?
function main() {
console.log('動いたよ!!');
}
// 大体 3 秒後に main 関数が動き出す
setTimeout(main, 3000);
大雑把な理解としては問題ないですが、実は setTimeout
の隠された能力はそれだけではありません。
setTimeout
の隠された能力を知るとどんな良いことがあるのでしょうか?
例えば、次のような良いことがあります。
- ブラウザに対する理解が深まる
- Web サイトのパフォーマンスを向上させることができる
- ドヤれる
まず、 ブラウザに対する理解が深まります 。
かのアリストテレスはこう言い残しました。
Knowing setTimeout is the beginning of all wisdom.
( setTimeout を知ることは、すべての知恵の始まりである)
setTimeout
を知ることは(ブラウザの)すべての知恵のはじまり と言って過言ではありません。
普段なにげなく setTimeout
を使っていても、「実はブラウザは裏でこんなことしているんだー」という新しい知見を得ることができます。
また、 setTimeout
を使いこなせれば Web サイトのパフォーマンス改善が期待 できます。
SEO で重要な Core Web Vitals のスコアを上げることにもつながるでしょう。
あと、 ドヤれます 。
職場のベテランエンジニアや、転職時のエンジニア面接官に「 setTimeout
とはどんな関数でしょう?」と聞いてみてください。
単に「指定した時間に処理が走るようにする関数」と答えたら、 あなたの方が知識は上 です。ドヤってください。
ということで、この記事では setTimeout
の隠された能力について解説します。
本来、難しめな内容なのですが 初心者の方にもわかりやすく説明していきます 。
setTimeout
の隠された秘密とは...?
setTimeout
の真の能力、それは タスクを分割してタスクキューに追加してくれる ことです。
この記事を閉じようとしたそこのあなた、 ちょっと待ってください!!
ちゃんと 初心者の方にもわかりやすく説明します!
わかっています。「タスク」と「タスクキュー」ってなに?という話でしょう。
「タスク」については次の通りです。
タスクとは、プログラムの初期実行、イベントコールバックの実行、インターバルやタイムアウトの発生など、標準的なメカニズムによって実行がスケジュールされる JavaScript コードのことです。これらはすべてタスクキューにスケジューリングされます。
https://developer.mozilla.org/ja/docs/Web/API/HTML_DOM_API/Microtask_guide
記事を閉じようとしないでください!!
この説明だと私もタスクが何かわかりません🤯
タスクについては具体例を挙げた方がイメージがつかみやすい です。
例えば、次のような script
タグのある HTML があったとします。
<script>
function main() {
console.log("スクリプト");
}
main();
</script>
ブラウザはこの HTML を読み込むと、 script
タグ内のコード main
関数を実行します。
この時 main
関数の実行が「タスク」です。
もう1個例を挙げてみましょう。
次のような JavaScript があったとします。
function a() {
console.log('a');
}
function b() {
console.log('b');
}
function c() {
console.log('c');
}
function main() {
a();
b();
c();
}
window.addEventListener('load', main);
この例では、 load
というイベントに main
という関数をコールバックとして登録しており、ページが完全に読み込まれると main
関数が実行されます。
この main
も「タスク」です。
関数 a
や b
、 c
はどうでしょうか?
この関数たちはタスクではなく main
というタスクの中で実行される作業 と捉えてください。
後でお話しますが、 ここは重要なポイント です。
さて、なんとなくタスクのイメージをつかめたでしょうか?
もしわかりにくかったら「グルーピングしたひとまとまりの処理」くらいのイメージで大丈夫です。
setTimeout
の真の能力、それは タスクを分割してタスクキューに追加してくれる ことです。
一方で「タスクキュー」とはなんでしょうか?
これは「 TODO リスト」だったり「やることリスト」のようなイメージを持ってもらえれば OK です!
「 TODO 」や「やること」が「タスク」に相当します。
家事に例えると
ここまでの話を家事に例えてみましょう。
料理や洗濯、掃除というタスクがあるとした時、タスクキュー( TODO リスト)は次のようなイメージです。
家事をこなすのはブラウザ主婦(主夫)です(以降、ブラウザママとします)。
ブラウザママは左から順番に料理や洗濯、掃除をこなしていきます。
まず、料理をします。
「冷蔵庫から材料を取り出す」「材料を切る」などの作業をこなしていきます。
(先ほどの 関数 a
や b
、 c
の例がこれにあたります)
料理を終えたら次のようなタスクキューになります。
次に洗濯というタスクをこなそうとします。
このようにブラウザはタスクキューにあるタスクを1つずつこなしていきます。
1つのタスクの中にも「冷蔵庫から材料を取り出す」「材料を切る」などの細かい作業があるので、頑張ってこなします。
このブラウザママですが、かなり律儀です。
料理が完了するまで他のことはしません 。
料理をソースコードで表現してみましょう。
function calculateCalorie() {
console.log('料理のカロリー計算をする');
}
function takePotato() {
console.log('じゃがいもを冷蔵庫から持ってくる');
}
function slicePotato() {
console.log('じゃがいもを切る');
}
function boilPoteto() {
console.log('じゃがいもをゆでる');
}
function cook() {
// 全部終わるまで次のタスクへいけない
calculateCalorie();
takePotato();
slicePotato();
boilPoteto();
}
window.addEventListener('昼ごはん', cook);
ブラウザママは律儀なので、今日作る料理のカロリー計算をしてから、じゃがいもを料理します。
これが完了後に次の洗濯をします。
さて、いつも律儀に家事をこなすブラウザママですが、めちゃくちゃ忙しい日もあります。
子供の送り迎えや買い物もしなくちゃいけません。
ブラウザママは考えました。
「料理中のカロリー計算は後回しにしよう!」 と。
function calculateCalorie() {
console.log('料理のカロリー計算をする');
}
function takePotato() {
console.log('じゃがいもを冷蔵庫から持ってくる');
}
function slicePotato() {
console.log('じゃがいもを切る');
}
function boilPoteto() {
console.log('じゃがいもをゆでる');
}
function cook() {
setTimeout(calculateCalorie, 0); // setTimeout の出番!!!
takePotato();
slicePotato();
boilPoteto();
}
window.addEventListener('昼ごはん', cook);
おや、setTimeout
が出てきました。
この処理を入れることでどうなったのでしょうか?
ブラウザママのタスクキューはこうなりました。
カロリー計算が後回しになっています 。
ブラウザママはカロリー計算をしない分、料理を早く終えることができ、さらに他の洗濯や掃除などのタスクを優先的にこなせるようになりました。
setTimeout
の真の能力、それは タスクを分割してタスクキューに追加してくれる ことです。
なんとなくおわかりいただけたでしょうか?
言い換えれば、 setTimeout
の真の力は タスク内の一部作業を後回しにしてくれる というものなのです。
興味があれば、今回のブラウザママの料理の様子は JavaScript Visualizer 9000 というサイトから確認できます。
「 Run 」ボタンをクリックした後、 「 Step 」ボタンを押すことで 1 つずつ動作が確認できます。
「 Call Stack 」などこの記事で詳しい説明はしていないものありますが、なんとなくブラウザママの料理のイメージは掴めればと思います。
タスク内の一部作業を後回しにしてくれると嬉しいこと
タスク内の一部作業を後回しにしてくれると何が嬉しいのでしょうか?
これについては もしタスクの実行時間が長かったら... と考えると、何が嬉しいかわかるでしょう。
ボタンを押したらモーダルが表示されるサイトがあったとしましょう。
ユーザービリティ的にはボタンを押したらすぐモーダルが表示されてほしいです。
ですが、ボタン押下時のタスクの実行時間が長かったらどうでしょうか?
しばらくモーダルが出てこず、画面はフリーズ しているように見えます。
これは ブラウザはタスクが終了しないとユーザーの画面に反映する作業ができない ためです。
ボタン押下時のタスクの実行時間が長いと、モーダル表示も遅れます。
また、 タスクの実行時間が長いと、後続の重要なタスクにも影響が出てしまいます。
ブラウザママの例を思い出してください。
料理以外にも子供の送り迎えという重要なタスクがあった場合、 料理時間が長すぎたら子供の送り迎えも遅れてしまいます 。
このようにタスクの実行時間が長いと色々とデメリットが生じます。
setTimeout
によって長いタスクを分割し、緊急性のないものはあと回しにすることでこれらのデメリットを抑えることができます 。
ママが早く迎えにきてくれて子供も大喜びです!
setTimeout
を使うべきケース
では、どのような時に setTimeout
を使えば良いのでしょうか。
ユーザーの画面に影響のない処理に使う というケースが考えられます。
最たる例は分析データ送信でしょうか。
const button = document.getElementById('button');
button.addEventListener('click', () => {
const data = updateSomeData();
showModal(data);
// 分析データの送信は遅らせる
setTimeout(sendAnalyticsData, 0)
});
この例ではボタンを押下した後、何かしら UI に必要なデータを更新し、モーダルを表示します。
分析データの送信はユーザーの画面に影響が出ない部分なので、 setTimeout
を使ってタスク分割&遅延させ、ボタンクリック後にすぐモーダルが表示するようにしています。
分析データ送信以外の例としては、データのキャッシュが挙げられます。
const cache = new Map();
const button = document.getElementById('button');
button.addEventListener('click', async () => {
const userInputText = getUserInputText();
const cachedResult = cache.get(userInputText);
if (cachedResult) {
updatePage(cachedResult);
return;
}
const result = await fetchData();
updatePage(result);
// データのキャッシュは遅らせる
setTimeout(() => cache.set(userInputText, result), 0);
});
データのキャッシュもはユーザーの画面に影響が出ない部分なので、 setTimeout
を使ってタスク分割&遅延しています。
ユースケースは色々考えられますが、次のサイトでもいくつか例が紹介されているので、そちらも参考にすると良いでしょう。
ユーザーの画面に影響のない処理といえど、何がなんでも setTimeout
を使うと開発者は疲れます😇
また、 setTimeout
自体のバッファも生じてしまいます。
目安としてタスクは 50 ms 以内 が推奨されています。
もしある処理が 50 ms を超えるようであれば setTimeout
によるタスク分割を検討しても良いでしょう。
タスクが時間かかっているって、どうすればわかるの?
ではあるタスクが時間がかかっているというのはどうすればわかるのでしょうか。
詳しい解説をするとそれだけで 1 記事作れちゃうので、詳しい説明は割愛しますが、簡単に紹介はしておきます。
Chrome の Developer ツールの「 Perfomance 」タブから確認することができます。
キャプチャの Start profiling and reload page
を押すと、ページが再読み込みされて、記録が開始されます。
テキトーにボタンなどを押下してみてください。全て記録されます。
キャプチャの Stop
を押すことで記録が停止し、レポートが表示されます。
Main
の部分に着目してみてください。 Task
というのがたくさん表示されています。
この Task
にカーソルを当てると、どのくらい実行時間がかかっているかが表示されます。
50 ms を超えると赤字で怒られます。
この Task
について、ライブラリ側( React
や Vue
など)ではなく、自分たちが作っているアプリケーション側の問題であれば、 1 つの対策として setTimeout
を使うのを検討しても良いでしょう。
Chrome の Developer ツールでの計測を紹介しましたが、簡単にコードを入れることでも確認できます。
const button = document.getElementById('button');
button.addEventListener('click', () => {
// 処理開始時間を記録
const start = performance.now();
// 処理
const data = updateSomeData();
showModal(data);
sendAnalyticsData();
// かかった時間を出力
console.log(performance.now() - start);
});
「指定した時間に処理が走るようにする関数」の本当の意味とは...?
冒頭で次のようなコードを紹介しました。
function main() {
console.log('動いたよ!!');
}
// 大体 3 秒後に main 関数が動き出す
setTimeout(main, 3000);
コメントアウトに「 大体 3 秒後に main 関数が動き出す」と書かれていたことに気づいていたでしょうか?
少し詳しい方なら、「 setTimeout
は指定した時間には正確には実行されない」ことを知っていたかもしれません。
では 「指定した時間には正確には実行されない」のはなぜでしょうか?
ここまで記事を読んでいただけた方はなんとなく察しがついているかもしれません。
正確には、 setTimeout
は指定した時間に処理が走らせるのではなく、 指定した時間にタスクキューにいれる のです。
setTimeout() または setInterval() で作成したタイムアウトまたはインターバルに達すると、対応するコールバックがタスクキューに追加されます。
https://developer.mozilla.org/ja/docs/Web/API/HTML_DOM_API/Microtask_guide
setTimeout(main, 3000)
の例では、 3 秒後に main
関数がタスクキュー( TODO リスト)にいれられます 。
このため、他のタスクで忙しい時は、 3 秒後に実行されるとは限らないのです 。
他にも 3 秒後にただちに実行されない理由はありますが、この setTimeout
の特性を覚えておくのは JavaScript への理解が深まるのではないでしょうか。
4ミリ秒マジック!!
その「ただちに実行されない理由」を 1 つご紹介します。
ここからの話は「へぇー、そうなんだ〜」くらいな気持ちで読んでもらって大丈夫です(読み飛ばしても OK です)。
次のような setTimeout
を使ったコードがあります。
let start = Date.now();
let count = 1;
function test() {
if (count > 10) {
alert('終了');
return;
}
const end = Date.now();
console.log(`${count}回目は${end - start}ms`);
start = end;
count++;
setTimeout(test, 0);
}
setTimeout(test, 0);
これを Chrome などのブラウザのコンソールに貼り付けて実行してみてください。
どうなるでしょう?
上のスクリーンショットの例だと 4 回まではほぼ即時で setTimeout
が実行されていますが、それ以降は 5 ミリ秒かかっています。
次の HTML 仕様書によれば実はブラウザでは 5 つタイマーをネストした後は最低でも 4 ミリ秒の遅延 が発生します。
このサイト によれば歴史的な経緯によるものっぽいですが、いずれにせよ、このような setTimeout
のマジックも存在するのです。
最後に
setTimeout
の真の力、おわかりいただけたでしょうか?
setTimeout
は単に「指定した時間に処理が走るようにする関数」というものではなく、深堀りすると面白い事実が出てきます。
難しめな内容ではありますが、初心者の方にもなんとなく理解していただけたら嬉しいです!
setTimeout
によるパフォーマンス改善の例も挙げましたが、その他にも色々とパフォーマンス改善を紹介しているので、そちらもぜひ参考にしてみてください!
また、私自身もパフォーマンス改善にチャレンジした経験を記事にしているので、そちらもぜひご覧いただけると嬉しいです!
最後に Twitter もよかったらフォローお願いします!
ここまで記事を閉じずにご覧いただきありがとうございました!
参考サイト