performance.now() が monotonic (単調増加) なことを利用すると、システム時計の変化を比較的高精度に得られるなと思ったので、以下のようなClockMonitorクラスを作ってみた。

// ClockMonitor: システム時計の大幅な変更(NTP補正・手動変更等)を検知し、イベントを発行するクラス
// WebAudioやperformance.now()はmonotonicな経過時間だが、絶対時刻(Date.now())はシステム時計依存でジャンプすることがある
// そのため、performance.timeOrigin+performance.now()で絶対時刻を計算している場合、
// システム時計が変化しても自動で補正されない(ズレたままになる)
// このクラスは、定期的にDate.now()とperformance.timeOrigin+performance.now()の差分を監視し、
// 一定以上の差分が発生した場合に"clockchange"イベントを発行することで、
// 利用側がoffset等を補正できるようにする
class ClockMonitor extends EventTarget {
	constructor({ threshold = 2000, interval = 1000 } = {}) {
		super();
		this.threshold = threshold; // 何ms以上の差分で検知するか
		this.interval = interval;   // 監視間隔(ms)
		this.offset = performance.timeOrigin || 0; // performance.now()の起点(初期化時の絶対時刻)
		this._timer = null;
	}

	start() {
		if (this._timer) return;
		this._timer = setInterval(() => {
			const perfNow = performance.now();
			const now = Date.now();
			// 現在の絶対時刻の期待値(初期offset+経過時間)
			const expected = this.offset + perfNow;
			const diff = now - expected;
			// threshold以上の差分が出たらシステム時計変更とみなす
			if (Math.abs(diff) > this.threshold) {
				// offsetを補正し、イベント発行
				this.offset += diff;
				this.dispatchEvent(new CustomEvent("clockchange", {
					detail: { offset: this.offset, diff }
				}));
			}
		}, this.interval);
	}

	stop() {
		if (this._timer) {
			clearInterval(this._timer);
			this._timer = null;
		}
	}
}

他の方法

Date.now() を単に保持しておいて比較することでも、過去への遡りは検出できる。が、未来へ進むのは検出できない (ただのタイマーの遅れと区別できない)

問題点

問題点: performance.now() は monotonic ではあるがスリープで時間の連続性が失われることがある
仕様上は連続することになっているが、一部の環境のブラウザだけ。

  1. トップ
  2. tech
  3. JSでシステム時計の変化(時刻変更、NTP同期)を検知する