search
LoginSignup
183

posted at

updated at

Organization

setTimeout の真の力、あなたは知っていますか?

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

皆さんは「 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 の真の能力、それは タスクを分割してタスクキューに追加してくれる ことです。

boy_question.png

この記事を閉じようとしたそこのあなた、 ちょっと待ってください!!
ちゃんと 初心者の方にもわかりやすく説明します!

わかっています。「タスク」と「タスクキュー」ってなに?という話でしょう。

「タスク」については次の通りです。

タスクとは、プログラムの初期実行、イベントコールバックの実行、インターバルやタイムアウトの発生など、標準的なメカニズムによって実行がスケジュールされる JavaScript コードのことです。これらはすべてタスクキューにスケジューリングされます。
https://developer.mozilla.org/ja/docs/Web/API/HTML_DOM_API/Microtask_guide

boy_question.png

記事を閉じようとしないでください!!
この説明だと私もタスクが何かわかりません🤯

タスクについては具体例を挙げた方がイメージがつかみやすい です。
例えば、次のような 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 も「タスク」です。

関数 abc はどうでしょうか?
この関数たちはタスクではなく main というタスクの中で実行される作業 と捉えてください。
後でお話しますが、 ここは重要なポイント です。

さて、なんとなくタスクのイメージをつかめたでしょうか?
もしわかりにくかったら「グルーピングしたひとまとまりの処理」くらいのイメージで大丈夫です。

setTimeout の真の能力、それは タスクを分割してタスクキューに追加してくれる ことです。

一方で「タスクキュー」とはなんでしょうか?
これは「 TODO リスト」だったり「やることリスト」のようなイメージを持ってもらえれば OK です!
「 TODO 」や「やること」が「タスク」に相当します。

家事に例えると

ここまでの話を家事に例えてみましょう。

family_kaji_tetsudai.png

料理や洗濯、掃除というタスクがあるとした時、タスクキュー( TODO リスト)は次のようなイメージです。

スクリーンショット 2022-10-21 17.53.33.png

家事をこなすのはブラウザ主婦(主夫)です(以降、ブラウザママとします)。
ブラウザママは左から順番に料理や洗濯、掃除をこなしていきます。

まず、料理をします。
「冷蔵庫から材料を取り出す」「材料を切る」などの作業をこなしていきます。
(先ほどの 関数 abc の例がこれにあたります)

料理を終えたら次のようなタスクキューになります。
次に洗濯というタスクをこなそうとします。

スクリーンショット 2022-10-21 17.57.36.png

このようにブラウザはタスクキューにあるタスクを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);

ブラウザママは律儀なので、今日作る料理のカロリー計算をしてから、じゃがいもを料理します。
これが完了後に次の洗濯をします。

さて、いつも律儀に家事をこなすブラウザママですが、めちゃくちゃ忙しい日もあります。
子供の送り迎えや買い物もしなくちゃいけません。

karou_hoikushi_woman.png

ブラウザママは考えました。
「料理中のカロリー計算は後回しにしよう!」 と。

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 が出てきました。
この処理を入れることでどうなったのでしょうか?

ブラウザママのタスクキューはこうなりました。

スクリーンショット 2022-10-21 18.47.44.png

カロリー計算が後回しになっています
ブラウザママはカロリー計算をしない分、料理を早く終えることができ、さらに他の洗濯や掃除などのタスクを優先的にこなせるようになりました。

setTimeout の真の能力、それは タスクを分割してタスクキューに追加してくれる ことです。

なんとなくおわかりいただけたでしょうか?
言い換えれば、 setTimeout の真の力は タスク内の一部作業を後回しにしてくれる というものなのです。

興味があれば、今回のブラウザママの料理の様子は JavaScript Visualizer 9000 というサイトから確認できます。
「 Run 」ボタンをクリックした後、 「 Step 」ボタンを押すことで 1 つずつ動作が確認できます。
「 Call Stack 」などこの記事で詳しい説明はしていないものありますが、なんとなくブラウザママの料理のイメージは掴めればと思います。

タスク内の一部作業を後回しにしてくれると嬉しいこと

タスク内の一部作業を後回しにしてくれると何が嬉しいのでしょうか?

これについては もしタスクの実行時間が長かったら... と考えると、何が嬉しいかわかるでしょう。

ボタンを押したらモーダルが表示されるサイトがあったとしましょう。
ユーザービリティ的にはボタンを押したらすぐモーダルが表示されてほしいです。

ですが、ボタン押下時のタスクの実行時間が長かったらどうでしょうか?
しばらくモーダルが出てこず、画面はフリーズ しているように見えます。

これは ブラウザはタスクが終了しないとユーザーの画面に反映する作業ができない ためです。
ボタン押下時のタスクの実行時間が長いと、モーダル表示も遅れます。

また、 タスクの実行時間が長いと、後続の重要なタスクにも影響が出てしまいます
ブラウザママの例を思い出してください。
料理以外にも子供の送り迎えという重要なタスクがあった場合、 料理時間が長すぎたら子供の送り迎えも遅れてしまいます

block_asobi_boy.png
(ママ、いつまで経っても来ないなぁ)

このようにタスクの実行時間が長いと色々とデメリットが生じます。
setTimeout によって長いタスクを分割し、緊急性のないものはあと回しにすることでこれらのデメリットを抑えることができます

banzai_kids_boy1.png

ママが早く迎えにきてくれて子供も大喜びです!

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 」タブから確認することができます。

スクリーンショット 2022-10-22 19.26.58.png

キャプチャの Start profiling and reload page を押すと、ページが再読み込みされて、記録が開始されます。
テキトーにボタンなどを押下してみてください。全て記録されます。

スクリーンショット 2022-10-22 20.22.00.png

キャプチャの Stop を押すことで記録が停止し、レポートが表示されます。

スクリーンショット 2022-10-22 19.34.50.png

Main の部分に着目してみてください。 Task というのがたくさん表示されています。
この Task にカーソルを当てると、どのくらい実行時間がかかっているかが表示されます。

スクリーンショット 2022-10-22 19.36.43.png

50 ms を超えると赤字で怒られます。
この Task について、ライブラリ側( ReactVue など)ではなく、自分たちが作っているアプリケーション側の問題であれば、 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 などのブラウザのコンソールに貼り付けて実行してみてください。
どうなるでしょう?

スクリーンショット 2022-10-25 9.35.32.png

上のスクリーンショットの例だと 4 回まではほぼ即時で setTimeout が実行されていますが、それ以降は 5 ミリ秒かかっています。

次の HTML 仕様書によれば実はブラウザでは 5 つタイマーをネストした後は最低でも 4 ミリ秒の遅延 が発生します。

このサイト によれば歴史的な経緯によるものっぽいですが、いずれにせよ、このような setTimeout のマジックも存在するのです。

最後に

setTimeout の真の力、おわかりいただけたでしょうか?
setTimeout は単に「指定した時間に処理が走るようにする関数」というものではなく、深堀りすると面白い事実が出てきます。

難しめな内容ではありますが、初心者の方にもなんとなく理解していただけたら嬉しいです!

setTimeout によるパフォーマンス改善の例も挙げましたが、その他にも色々とパフォーマンス改善を紹介しているので、そちらもぜひ参考にしてみてください!

また、私自身もパフォーマンス改善にチャレンジした経験を記事にしているので、そちらもぜひご覧いただけると嬉しいです!

最後に Twitter もよかったらフォローお願いします!

ここまで記事を閉じずにご覧いただきありがとうございました!

参考サイト

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
183