Back

JavaScriptでホリデーカウントダウンタイマーを構築する

JavaScriptでホリデーカウントダウンタイマーを構築する

毎年12月になると、開発者は同じリクエストに直面します。クリスマス、大晦日、またはその他の祝日までのカウントダウンを構築してほしいというものです。一見シンプルに思えますが、タイマーが数分ずれたり、ユーザーがタブを切り替えると動作しなくなったり、深夜を過ぎると負の数が表示されたりします。

この記事では、これらのエッジケースを適切に処理する信頼性の高いJavaScriptカウントダウンタイマーの構築方法を紹介します。カウンターをデクリメントするのではなく、毎回時間を再計算する理由、JavaScriptのタイムゾーンを正しく処理する方法、そしてほとんどの実装でつまずく一般的な落とし穴を回避する方法を学びます。

重要なポイント

  • タイマーのドリフトを避けるため、カウンターをデクリメントするのではなく、Date.now()を使用して毎回残り時間を再計算する
  • 外部ライブラリを使用せずに、new Date(year, month, day)を使用してユーザーのローカルタイムゾーンで日付を作成する
  • 特にシングルページアプリケーションにおいて、メモリリークを防ぐために常にインターバルIDを保存してクリアする
  • 実際の時間差に基づいて計算することで、ブラウザのタブスロットリングに対する耐性を構築する

カウンターのデクリメントが失敗する理由

ホリデーカウントダウンJavaScriptウィジェットを構築する際の素朴なアプローチは次のようなものです。残り秒数を変数に設定し、setIntervalを使用して毎秒1ずつ減算します。これは実際には機能しません。

問題はsetIntervalタイマーのドリフトです。JavaScriptタイマーは正確な実行を保証しません。ユーザーがタブを切り替えると、ブラウザはリソースを節約するためにタイマーをスロットリングします。1秒に1回発火することもあれば、1分に1回発火することもあります。カウンターは時間が正常に経過したかのようにデクリメントし続けますが、実際にはそうではありません。

修正方法はシンプルです。Date.now()とターゲット日付を比較して、毎回残り時間を再計算します。こうすることで、タイマーの発火が遅れても、表示される時間は正確なままです。

ターゲット日付の設定

ホリデーカウントダウンの場合、通常はユーザーのローカルタイムゾーンで特定のカレンダー日付の深夜0時を設定したいでしょう。正しく設定する方法は次のとおりです。

function getTargetDate(month, day) {
  const now = new Date()
  const year = now.getFullYear()
  
  // If the holiday has passed this year, target next year
  const target = new Date(year, month - 1, day, 0, 0, 0)
  if (target <= now) {
    target.setFullYear(year + 1)
  }
  
  return target
}

const christmasTarget = getTargetDate(12, 25)

new Date(year, month, day)を使用すると、ユーザーのローカルタイムゾーンで自動的に日付が作成されます。これによりライブラリを必要とせずにJavaScriptのタイムゾーンを処理できます。カウントダウンは、ユーザーがどこにいても12月25日の深夜0時までの時間を表示します。

夏時間に関する注意: JavaScriptのDateオブジェクトは、この方法で日付を構築すると、夏時間の移行を自動的に処理します。時計が変更されてもカウントダウンは正確なままです。

コアカウントダウンロジック

一般的な問題に対処する本番環境対応の実装は次のとおりです。

function createCountdown(targetDate, onUpdate, onComplete) {
  let intervalId = null
  
  function calculateRemaining() {
    const now = Date.now()
    const difference = targetDate.getTime() - now
    
    // Prevent negative values
    if (difference <= 0) {
      clearInterval(intervalId)
      onComplete()
      return null
    }
    
    return {
      days: Math.floor(difference / (1000 * 60 * 60 * 24)),
      hours: Math.floor((difference / (1000 * 60 * 60)) % 24),
      minutes: Math.floor((difference / (1000 * 60)) % 60),
      seconds: Math.floor((difference / 1000) % 60)
    }
  }
  
  function tick() {
    const remaining = calculateRemaining()
    if (remaining) {
      onUpdate(remaining)
    }
  }
  
  // Run immediately, then every second
  tick()
  intervalId = setInterval(tick, 1000)
  
  // Return cleanup function
  return () => clearInterval(intervalId)
}

このアプローチは、いくつかの問題を一度に解決します。

  • ドリフトなし: 毎回新たに時間が再計算されます
  • 負の値なし: カウントダウンが完了するとインターバルが停止してクリアされます
  • 適切なクリーンアップ: 返される関数により、必要に応じてタイマーを停止できます
  • タブスロットリングへの耐性: ティックが遅延しても、表示される時間は正確なままです

DOMへの接続

カウントダウンをHTMLに接続します。

const display = document.getElementById('countdown')
const cleanup = createCountdown(
  christmasTarget,
  ({ days, hours, minutes, seconds }) => {
    display.textContent = `${days}d ${hours}h ${minutes}m ${seconds}s`
  },
  () => {
    display.textContent = "It's here!"
  }
)

// Call cleanup() when navigating away in a SPA

エッジケースの処理

既に経過している場合: getTargetDate関数は、祝日が経過している場合、自動的に翌年にロールオーバーします。

インターバルのクリア: 常にインターバルIDを保存し、カウントダウンが完了したらクリアしてください。これを怠ると、特にシングルページアプリケーションでメモリリークが発生します。

アクセシビリティ: スクリーンリーダーが邪魔にならずに更新をアナウンスできるように、カウントダウンコンテナにaria-live="polite"を追加することを検討してください。

今後の展望

今後登場するTemporal APIにより、JavaScriptの日付と時刻の処理が大幅に簡単になり、明示的なタイムゾーンサポートが組み込まれます。すべてのブラウザに実装されるまでは、ここで示したパターンが信頼できる選択肢です。

まとめ

ホリデーカウントダウンJavaScriptウィジェットは、デクリメントされたカウンターではなく、時間差計算を使用して構築してください。タイマーは、ブラウザのスロットリング、タブの切り替え、夏時間の移行に関係なく正確なままであり、よりシンプルなアプローチが引き起こすデバッグの頭痛を回避できます。

よくある質問

ブラウザはリソースを節約するために、バックグラウンドタブのJavaScriptタイマーをスロットリングします。毎秒カウンターをデクリメントすると、タブが非アクティブの間も時間が正常に経過したかのようにカウントが続きます。代わりに、Date.now()とターゲット日付を比較して、毎回残り時間を再計算してください。これにより、タイマーが実際にどれくらいの頻度で発火するかに関係なく、正確性が保証されます。

new Date(year, month, day)のように数値引数を持つDateコンストラクタを使用して、ユーザーのローカルタイムゾーンで自動的に日付を作成します。このアプローチは外部ライブラリを必要とせず、各ユーザーが固定されたUTC時間ではなく、自分のタイムゾーンの深夜0時までのカウントダウンを見ることができます。

メモリリークは、カウントダウンが完了した後もsetIntervalが実行され続ける場合、またはシングルページアプリケーションでユーザーが移動する場合に発生します。常にsetIntervalが返すインターバルIDを保存し、カウントダウンが終了したとき、またはコンポーネントがアンマウントされたときにclearIntervalを呼び出してください。簡単に破棄できるように、カウントダウンクリエーターからクリーンアップ関数を返してください。

Temporal APIは、明示的なタイムゾーンサポートとより明確なセマンティクスにより、日付と時刻の処理を簡素化します。ただし、カウンターをデクリメントするのではなく時間差を再計算するという中核原則は引き続き関連性があります。Temporalがすべてのブラウザに実装されるまで、この記事のパターンは信頼性の高いクロスブラウザソリューションを提供します。

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay