
Webページに対象日時までのカウントダウンタイマーを表示する JavaScript のサンプルコードはよくありますが、大抵の方法は、一定間隔で処理を繰り返す setInterval メソッドを利用して1秒ごとにタイマーを更新するというものです。
しかし、setInterval によるタイマーはブラウザの状態によっては遅延が発生するなど正確性が保証されておらず、メモリリークやブラウザに与える負荷が高くなり、パフォーマンス低下を引き起こす恐れがあるなど、多くのデメリットがあります。
特に、ミリ秒単位でカウントダウンタイマーを表示したい場合は setInterval で指定できる最小間隔である 10ミリ秒以上での間隔となることや、そもそもWebページ上の要素を更新するような処理を setInterval でミリ秒単位で繰り返し実行するのは適切ではありません。
そこで、今回はモニターのリフレッシュレート(1秒間に画面を更新できる回数)によってアニメーション(繰り返し処理)を実行できる requestAnimationFrame  メソッドを使った、正確で負荷がなく1ミリ秒単位にも対応するカウントダウンタイマーのサンプルをご紹介します。
まずは以下のデモをご覧ください。
See the Pen Count down timer to target date by digistate (@digistate) on CodePen.
それでは、このカウントダウンタイマーの構造をみていきましょう。
HTML
HTML は、カウントダウンタイマーを表示したい要素1つのみです。
<div id="countdown"></div>
“countdown” という ID が与えられていますが、後述する JavaScript でカウントダウンタイマー用の要素を取得できれば class="countdown" でも何でもOKです。
JavaScript
現在日時から未来の対象日時までのカウントダウンタイマーを表示する処理は、大まかに以下の流れで実行します。
タイマーを更新・終了する流れ
時差がなくなった場合はタイマーの更新をせず、終了メッセージを表示するなどしてカウントダウンを完了します。
時差があった場合は、時差を日、時、分、秒に分解し、カウントダウンタイマー用の表示形式にして #countdown 要素内の HTML として更新し、再び Step 2 から処理を繰り返します。
対象日時の設定
まずはカウントダウンを表示するための未来の対象日時(期限)を Date() メソッドで設定します。
// 目標日時を指定(年, 月(0-11), 日, 時, 分, 秒) const targetDate = new Date( 2023, 11, 1, 17, 0, 0 ); // 出力例: // Fri Dec 01 2023 17:00:00 GMT+0900 (日本標準時)
Date() メソッドにカンマ区切りで対象の年、月、日、時、分、秒を整数で指定していますが、この形式では「月」は 0〜11 の範囲(「実際の対象月 – 1」 ※12月の場合 : 11)であることに注意してください。
または、日付を表す文字列型での指定でも対象日時のデータを取得できます。
// 以下のように文字列型の日付フォーマットの指定でもOK const targetDate = new Date( '2023/12/1 17:00:00' );
Date() メソッドに渡す日付のフォーマットの指定方法は色々あるので、以下を参考にして要件にあうものを選んでください。
現在日時から対象日時までの時差の取得
ページを表示した時点の日時から対象日時までの時間差を求めます。
// 現在日時の取得 const now = new Date(); // 時差 const distance = targetDate - now;
時差を比較し処理を分岐
時差がある場合はカウントダウンタイマーの表示と再度時差の比較からタイマー更新の実行を行い、時差がなくなった場合はタイマーの更新をせずにそのままスルーします。
if ( distance < 0 ) {
  // 時差がない場合(終了メッセージを表示するなど)
} else {
  // カウントダウンタイマーの表示と再帰処理の指示
}
時差を日、時、分、秒に分解
時差がある場合は、タイマーとして表示するために distance (時差のミリ秒)を日、時、分、秒の単位ごとで割ってそれぞれの値を求めます。
// 日 const days = Math.floor( distance / ( 1000 * 60 * 60 * 24 ) ); // 時 const hours = Math.floor( ( distance % ( 1000 * 60 * 60 * 24 ) ) / ( 1000 * 60 * 60 ) ); // 分 const minutes = Math.floor( ( distance % ( 1000 * 60 * 60 ) ) / ( 1000 * 60 ) ); // 秒 const seconds = Math.floor( ( distance % ( 1000 * 60 ) ) / 1000 ); // ミリ秒 const miliseconds = distance < 0 ? 0 : Math.floor( distance % 1000 );
1秒 = 1000ミリ秒 なので、上記の計算は以下の解釈になります。
- 日数(days)の計算
- 
1日のミリ秒 = 1000 ミリ秒 x 60秒 x 60分 x 24時間 従って、 
 求める日数
 = 時差のミリ秒 / 1日のミリ秒
 = distance / ( 1000 x 60 x 60 x 24 )
- 時間(hours)の計算
- 
1時間のミリ秒 = 1000 ミリ秒 x 60秒 x 60分 時差のミリ秒から日数分までのミリ秒を除いた(割った)余りのミリ秒を割る。 
 従って、
 求める時間
 = ( 時差のミリ秒 % / 1日のミリ秒 ) / 1時間のミリ秒
 = ( distance % ( 1000 x 60 x 60 x 24 ) ) / ( 1000 x 60 x 60 )
- 分(minutes)の計算
- 
1分のミリ秒 = 1000 ミリ秒 x 60秒 時差のミリ秒から時間分までのミリ秒を除いた(割った)余りのミリ秒を割る。 
 従って、
 求める分
 = ( 時差のミリ秒 % / 1時間のミリ秒 ) / 1分間のミリ秒
 = ( distance % ( 1000 x 60 x 60 ) ) / ( 1000 x 60 )
- 秒(seconds)の計算
- 
1秒のミリ秒 = 1000 ミリ秒 x 60秒 時差のミリ秒から分までのミリ秒を除いた(割った)余りのミリ秒を割る。 
 従って、
 求める秒
 = ( 時差のミリ秒 % / 1分のミリ秒 ) / 1秒のミリ秒
 = ( distance % ( 1000 x 60 ) ) / 1000
- ミリ秒(miliseconds)の計算
- 
時差のミリ秒を1秒間のミリ秒(1000)で割った余り。 求めるミリ秒 
 = 時差のミリ秒 % 1秒ミリ秒
 = distance % 1000
タイマーの更新(HTMLの書き換え)
日、時、分、秒に分解した数値(小数点以下切り捨て)を元に、カウントダウンタイマーを表示する要素内の HTML を書き換えます。
// カウントダウンタイマーを表示する要素
const ele = document.getElementById( 'countdown' );
// タイマーのHTML出力
ele.innerHTML = `開催まであと<br /><span class="days">${ days }</span>日と<span class="hours">${ String( hours ).padStart( 2, '0' ) }</span>時間<span class="minutes">${ String( minutes ).padStart( 2, '0' ) }</span>分<span class="seconds">${ String( seconds ).padStart( 2, '0' ) }</span>.${ String( miliseconds ).padStart( 3, '0' ) }秒`;
時、分、秒は時点によっては桁数が変わること(1桁または2桁)があり、特に時間間隔が短い秒、ミリ秒では桁数の変化が早く、タイマー要素の表示幅が桁数に応じて変わることで表示ががたついてしまいます。
そのため、対象の文字列が指定した長さ(文字数)以外のときは先頭に任意の文字列を連結する padStart() というメソッドを利用して常に2桁(1桁のときは先頭に ‘0’ を付ける)で表示するようにします。
String( hours ).padStart( 2, '0' ) // hours = 4 の場合は 04 となる
タイマーの実行(繰り返し処理)
あとは、タイマーを実行するために requestAnimationFrame を利用してモニターのリフレッシュレートを基準とした繰り返し処理を実行し、その中でタイマーの更新をミリ秒単位で高速に処理します。
// カウントダウン処理
const countdown = () => {
  if ( distance < 0 ) {
    // タイマー終了
  } else {
    // タイマー更新処理
    ...
    // 繰り返し
    window.requestAnimationFrame( countdown );
  }
}
// カウントダウンアニメーションの実行
window.requestAnimationFrame( countdown );
全体のコード
ここまでの処理をまとめると、全体の JavaScript のコードは以下のようになります。
// 目標日時を指定(年, 月(0-11), 日, 時, 分, 秒)
const targetDate = new Date( 2023, 11, 1, 17, 0, 0 );
// カウントダウンタイマー
const ele = document.getElementById( 'countdown' );
// カウントダウン処理
const countdown = () => {
  // 現在日時
  const now = new Date();
  // 時差
  const distance = targetDate - now;
  if ( distance < 0 ) {
    ele.innerHTML = "カウントダウン終了!";
  } else {
    const days = Math.floor( distance / ( 1000 * 60 * 60 * 24 ) );
    const hours = Math.floor( ( distance % ( 1000 * 60 * 60 * 24 ) ) / ( 1000 * 60 * 60 ) );
    const minutes = Math.floor( ( distance % ( 1000 * 60 * 60 ) ) / ( 1000 * 60 ) );
    const seconds = Math.floor( ( distance % ( 1000 * 60 ) ) / 1000 );
    const miliseconds = distance < 0 ? 0 : Math.floor( distance % 1000 );
    // カウントダウンタイマーのHTML更新
    ele.innerHTML = `開催まであと<br /><span class="days">${ days }</span>日と<span class="hours">${ String( hours ).padStart( 2, '0' ) }</span>時間<span class="minutes">${ String( minutes ).padStart( 2, '0' ) }</span>分<span class="seconds">${ String( seconds ).padStart( 2, '0' ) }</span>.${ String( miliseconds ).padStart( 3, '0' ) }秒`;
    // 再度タイマー更新の実行
    window.requestAnimationFrame( countdown );
  }
}
// カウントダウンタイマーの起動
window.requestAnimationFrame( countdown );
requestAnimationFrame を利用することで、ブラウザに負荷をかけず正確でミリ秒単位でのカウントダウンタイマーを表示することができました。
とてもシンプルで要件によっては実用性のあるコンテンツとして利用できるので、「DigiPress Ex – Blocks」プラグインのカスタムブロックや「DigiPress Ex – Shortcodes」プラグインのショートコードとして今後実装してみようと思います。

