NodeJS 速習チュートリアル

Node.js Timers モジュール

1. Timers モジュールとは?

Timers モジュールは、特定の時間後、または一定の間隔でコードを実行するための関数を提供します。
ブラウザの JavaScript とは異なり、Node.js のタイミング関数は Timers モジュールの一部として実装されていますが、明示的にインポートしなくても グローバル(Global) に利用可能です。

1.1 主な機能

  • setTimeout(): 指定した遅延時間後に実行
  • setInterval(): 一定の間隔で繰り返し実行
  • setImmediate(): 次の イベントループ(Event Loop) の反復で実行
  • Promise ベースの API: モダンな async/await パターンに対応

これらの機能は、レスポンシブなアプリケーションの構築、ポーリング(Polling) の実装、遅延処理のハンドリングなどに不可欠です。

2. タイマーのクイックスタート

以下は、Timers モジュールを使用してコードの実行をスケジュールする簡単な例です。

例:タイマーの基本

const { setTimeout, setInterval, setImmediate } = require('timers');

console.log('タイマーを開始します...');

// 1秒後に一度だけ実行
setTimeout(() => {
  console.log('これは1秒後に実行されます');
}, 1000);

// 1秒ごとに繰り返し実行
let counter = 0;
const interval = setInterval(() => {
  counter++;
  console.log(`インターバルのカウント: ${counter}`);
  // 3回実行されたら停止
  if (counter >= 3) clearInterval(interval);
}, 1000);

// 次のイベントループの反復で実行
setImmediate(() => {
  console.log('これはイベントループの次の反復で実行されます');
});

console.log('タイマーのスケジュールが完了しました');

3. Timers モジュールの使用方法

Timers モジュールの関数はグローバルに利用できるため、明示的に require する必要はありません。
ただし、高度な機能へのアクセスやコードの明示性を高めるために、モジュールをインポートすることも可能です。

const timers = require('timers');

// または、Promises API を使用する場合 (Node.js 15.0.0+)
const timersPromises = require('timers/promises');

4. setTimeout() と clearTimeout()

setTimeout() 関数は、指定されたミリ秒数の経過後に コールバック(Callback) を実行します。
この関数は、タイムアウトをキャンセルするために使用できる Timeout オブジェクト を返します。

4.1 よくあるユースケース

  • 非クリティカルなタスクの実行を遅らせる
  • 操作のタイムアウト(制限時間)を実装する
  • CPU 負荷の高いタスク を分割する
  • リトライロジックの実装

例:setTimeout の使用

// 基本的な使用法
setTimeout(() => {
  console.log('このメッセージは2秒後に表示されます');
}, 2000);

// 引数を渡す場合
setTimeout((name) => {
  console.log(`こんにちは、${name}さん!`);
}, 1000, 'Node.js');

// タイムアウトを保存してクリアする
const timeoutId = setTimeout(() => {
  console.log('これは表示されません');
}, 5000);

// 実行前にタイムアウトをキャンセル
clearTimeout(timeoutId);
console.log('タイムアウトがキャンセルされました');

4.2 Promise ベースの setTimeout

Node.js 15.0.0 以降では、タイマー用の Promise ベース API が提供されています。

const { setTimeout } = require('timers/promises');

async function delayedGreeting() {
  console.log('開始...');

  // 2秒待機
  await setTimeout(2000);

  console.log('2秒経過しました');

  // 値を指定して1秒待機
  const result = await setTimeout(1000, 'Hello, World!');

  console.log('さらに1秒経過:', result);
}

delayedGreeting().catch(console.error);

5. setInterval() と clearInterval()

setInterval() 関数は、指定された間隔(ミリ秒)ごとに繰り返し関数を呼び出します。
この関数は、インターバルを停止するために使用できる Interval オブジェクト を返します。

5.1 よくあるユースケース

  • アップデートのポーリング
  • 定期的なメンテナンス、クリーンアップタスク
  • ハートビート(Heartbeat) メカニズムの実装
  • UI 要素の定期的な更新

       注意: イベントループが他の操作によってブロックされている場合、実際の実行間隔は指定された時間よりも長くなる可能性があります。

例:setInterval の使用

// 基本的なインターバル
let counter = 0;
const intervalId = setInterval(() => {
  counter++;
  console.log(`インターバルが ${counter} 回実行されました`);

  // 5回実行後に停止
  if (counter >= 5) {
    clearInterval(intervalId);
    console.log('インターバルを停止しました');
  }
}, 1000);

// 引数付きのインターバル
const nameInterval = setInterval((name) => {
  console.log(`Hello, ${name}!`);
}, 2000, 'Node.js');

// 6秒後にインターバルを停止
setTimeout(() => {
  clearInterval(nameInterval);
  console.log('名前の表示インターバルを停止しました');
}, 6000);

5.2 Promise ベースの setInterval

インターバルに Promise API を使用し、非同期イテレータとして扱うことができます。

const { setInterval } = require('timers/promises');

async function repeatedGreeting() {
  console.log('インターバルを開始します...');

  // setInterval から非同期イテレータを作成
  const interval = setInterval(1000, 'tick');

  // 5回に制限
  let counter = 0;

  for await (const tick of interval) {
    console.log(counter + 1, tick);
    counter++;

    if (counter >= 5) {
      break; // ループを抜けるとインターバルも停止します
    }
  }

  console.log('インターバルが終了しました');
}

repeatedGreeting().catch(console.error);

6. setImmediate() と clearImmediate()

setImmediate() 関数は、現在の操作が完了した後、I/O イベントの直後かつタイマーよりも前に、イベントループの次の反復でコールバックを実行するようにスケジュールします。
これは setTimeout(callback, 0) と似ていますが、より効率的です。

6.1 setImmediate() を使用すべき場面

  • 現在の操作が完了した直後にコードを実行したいとき
  • 長時間の操作を小さなチャンクに分割するとき
  • コールバックを必ず I/O 操作の完了後に実行させたいとき
  • 再帰関数での スタックオーバーフロー(Stack Overflow) を防ぐとき
console.log('開始');

setTimeout(() => {
  console.log('setTimeout のコールバック');
}, 0);

setImmediate(() => {
  console.log('setImmediate のコールバック');
});

process.nextTick(() => {
  console.log('nextTick のコールバック');
});

console.log('終了');

典型的な実行順序は以下の通りです:

  1. 開始
  2. 終了
  3. nextTick callback
  4. setTimeout callback または setImmediate callback(この順序は変動する可能性があります)

       注意: メインモジュールから呼び出した場合、setTimeout(0)setImmediate() の順序は予測不可能です。しかし、I/O コールバック内 では、setImmediate() は常にタイマーよりも先に実行されます。

7. process.nextTick()

Timers モジュールの一部ではありませんが、process.nextTick() は関連する重要な関数です。これはコールバックを次のイベントループの反復まで遅らせますが、あらゆる I/O イベントやタイマーよりも に実行します。

7.1 主な特徴

  • あらゆる I/O やタイマーよりも先に実行される
  • setImmediate() よりも優先度が高い
  • イベントループが続行される前に、キューにあるすべてのコールバックを処理する
  • 使いすぎると I/O スタベーション(I/O Starvation) を引き起こす可能性がある

8. 高度なタイマーパターン

8.1 デバウンス (Debouncing)

関数が頻繁に呼び出されるのを防ぎ、最後の呼び出しから一定時間経過した後に実行します。

function debounce(func, delay) {
  let timeoutId;
  return function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => func.apply(this, args), delay);
  };
}

// 例:リサイズイベントのハンドリング
const handleResize = debounce(() => {
  console.log('ウィンドウがリサイズされました');
}, 300);

8.2 スロットリング (Throttling)

一定時間内に一度だけ関数が実行されるように制限します。

function throttle(func, limit) {
  let inThrottle = false;
  return function(...args) {
    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  };
}

// 例:スクロールイベントのハンドリング
const handleScroll = throttle(() => {
  console.log('スクロールを処理中');
}, 200);

8.3 逐次的なタイムアウト (Sequential Timeouts)

各操作の間に遅延を挟んで、一連の処理を順番に実行します。

function sequentialTimeouts(callbacks, delay = 1000) {
  let index = 0;
  function next() {
    if (index < callbacks.length) {
      callbacks[index]();
      index++;
      setTimeout(next, delay);
    }
  }
  next();
}

// 使用例
sequentialTimeouts([
  () => console.log('ステップ 1'),
  () => console.log('ステップ 2'),
  () => console.log('ステップ 3')
], 1000);

9. タイマーの挙動とベストプラクティス

9.1 精度とパフォーマンス

Node.js のタイマーはミリ秒単位で厳密に正確ではありません。実際の遅延は以下の要因で長くなることがあります。

  • システム負荷と CPU 使用率
  • イベントループをブロックする操作
  • 他のタイマーや I/O 操作
  • システムタイマーの解像度(通常 1〜15ms)

9.2 メモリとリソース管理

タイマーの適切な管理は、メモリリークや過剰なリソース消費を防ぐために極めて重要です。

  • メモリリークの典型的なパターン
    • 必要なくなった後も setInterval が動き続けている
    • タイマーのコールバック内で巨大なオブジェクトをクロージャとして保持している

推奨されるプラクティス:

  1. 不要になったタイマー(Interval / Timeout)は必ずクリアする
  2. クリーンアップ関数で clearTimeoutclearInterval を呼び出せるよう、タイマー ID を適切に管理する
  3. サーバーコンテキストなど長時間動作するアプリでは、停止用のメソッドを提供する
// 良い例:サーバー停止時にタイマーも止める
function startServer() {
  const intervalId = setInterval(() => {
    console.log('サーバーが動作中...');
  }, 60000);

  return {
    stop: () => {
      clearInterval(intervalId);
      console.log('サーバーが停止しました');
    }
  };
}

const server = startServer();
// 3分後にサーバーを停止
setTimeout(() => {
  server.stop();
}, 180000);

9.3 0ミリ秒のタイムアウト (Zero-Delay) によるタスク分割

setTimeout(callback, 0) を使用すると、コールバックは即座には実行されず、現在のイベントループサイクルが完了した後に実行されます。これを利用して、CPU 負荷の高い処理を「分割」し、他の I/O リクエストを処理する隙間を作ることができます。

function processArray(array, processFunction) {
  const chunkSize = 1000;
  let index = 0;

  function processChunk() {
    const chunk = array.slice(index, index + chunkSize);
    chunk.forEach(processFunction);

    index += chunkSize;

    if (index < array.length) {
      // イベントループに制御を戻す
      setTimeout(processChunk, 0); 
    } else {
      console.log('全処理が完了しました');
    }
  }

  processChunk();
}

console.log('処理を開始...');
processArray(new Array(10000).fill(0), (item) => { /* 処理 */ });
console.log('このログは処理完了前に表示され、イベントループがブロックされていないことを証明します');

10. まとめ

Node.js の Timers モジュールは、スケーラブルでレスポンシブなアプリケーションを構築するための強力なツールです。

  • setTimeoutsetInterval で実行時間を制御する
  • setImmediateprocess.nextTick でイベントループの優先順位を管理する
  • Promise ベースの API を活用して可読性の高い非同期コードを書く
  • タイマーのクリアを忘れずに行い、メモリリークを防止する

これらの特性を理解することで、Node.js の非同期アーキテクチャのポテンシャルを最大限に引き出すことができます。