使用 Temporal.Instant 排序高精度時間戳記

網頁功能: Temporal

記錄檢視器、稽核時間軸和遙測 UI 通常會擷取事件,這些事件的 ISO 8601 時間戳記是由伺服器、資料庫或原生時鐘以微秒或奈秒精確度記錄 (例如 2026-04-01T12:00:00.000123456Z)。由於舊版 Date 物件只會儲存毫秒,因此使用 new Date() 或 Date.parse() 剖析這些字串時,會自動截斷毫秒以下的數字,導致同一毫秒內的不同事件發生衝突,並以不穩定的順序排序。

Temporal.Instant 代表世界標準時間時間軸上的確切時間點,精確度可達奈秒,且未附加時區。使用 Temporal.Instant.from() 剖析預錄的高精確度時間戳記時,系統會保留完整的奈秒值、使用 Temporal.Instant.compare() 確定排序,並計算事件之間的確切 Temporal.Duration 間隔。請注意,在瀏覽器中,Temporal.Now.instant() 會刻意限制為毫秒或微秒解析度,以緩解 Spectre 計時攻擊。請使用 Temporal.Instant.from() 保留外部時鐘產生的奈秒時間戳記,並在用戶端本機測量亞毫秒間隔時使用 performance.now()。

導入方式

如要剖析、排序及測量高精確度時間戳記之間的間隔,請按照下列步驟操作:

  1. 將記錄的時間戳記剖析為 Temporal.Instant:在伺服器、資料庫或記錄行提供的時間戳記字串上使用 Temporal.Instant.from(isoString)。保留來源記錄的時間戳記,不要在瀏覽器中替換為 Temporal.Now.instant()。
  2. 依時間順序排序事件:使用 Temporal.Instant.compare(a, b) 做為傳遞至 Array.prototype.sort() 或 Array.prototype.toSorted() 的比較器。系統會比較完整的奈秒值,因此會保留毫秒以下的排序。
  3. 計算間隔:使用 instant.since(previousInstant) 取得連續事件之間的 Temporal.Duration。使用 a.equals(b) 偵測時間戳記相同的事件。
  4. 使用 toLocaleString() 顯示間隔:使用 duration.toLocaleString() 為使用者設定 Temporal.Duration 格式,並委派給 Intl.DurationFormat。請勿轉換為單一 Number 奈秒值來顯示。
  5. 序列化傳輸:使用 Temporal.Instant.prototype.toString() 發出標準 ISO 8601 字串 (一律採用世界標準時間,並加上 Z 後置字元)。

程式碼範例:排序高精確度時間戳記

// Events received with nanosecond-precision ISO 8601 timestamps.
const rawEvents = [
  { nodeId: 'api-2', eventType: 'db.query', ts: '2026-04-01T12:00:00.000123456Z' },
  { nodeId: 'api-1', eventType: 'request.start', ts: '2026-04-01T12:00:00.000120000Z' },
  { nodeId: 'api-2', eventType: 'request.end', ts: '2026-04-01T12:00:00.000987654Z' },
];

// 1. Parse recorded timestamps as Temporal.Instant (never `new Date(ts)`,
// which drops everything below the millisecond).
const events = rawEvents.map(evt => ({
  ...evt,
  timestamp: Temporal.Instant.from(evt.ts),
}));

// 2. Sort chronologically with the static compare() method
function sequenceEvents(items) {
  // Instant.compare returns -1, 0, or 1, which is what sort() expects.
  return [...items].sort((a, b) => Temporal.Instant.compare(a.timestamp, b.timestamp));
}

// 3. Calculate and display the interval between consecutive events
function analyzeTrace(sortedEvents) {
  for (let i = 1; i < sortedEvents.length; i++) {
    const prev = sortedEvents[i - 1];
    const curr = sortedEvents[i];

    // since() returns a Temporal.Duration that retains nanosecond precision.
    const gap = curr.timestamp.since(prev.timestamp);

    // 4. toLocaleString() formats the duration for people (e.g., "3μs 456ns").
    // Avoid gap.total('nanoseconds'): spans over ~104 days exceed Number.MAX_SAFE_INTEGER.
    // 'en-US' is used here for deterministic output; pass undefined to use the user's locale.
    console.log(`${prev.eventType} → ${curr.eventType}: ${gap.toLocaleString('en-US', { style: 'narrow' })}`);
  }
}

analyzeTrace(sequenceEvents(events));

策略執行與最佳做法

  • 建議使用 Temporal.Instant.from() 剖析根據單一時鐘 (例如伺服器、資料庫或記錄管道) 記錄的高精確度時間戳記,以便排序或計算差異。與 Temporal.PlainDateTime 不同,Temporal.Instant 代表通用時間軸上的明確時間點。
  • 務必保留來源記錄的時間戳記。在瀏覽器中,Temporal.Now.instant() 會限制為毫秒/微秒精確度,以緩解 Spectre 計時攻擊和指紋採集;請使用 performance.now()/performance.mark() 取得本機亞毫秒用戶端間隔,並使用 Temporal.Instant.from() 剖析伺服器或原生時鐘的奈秒時間戳記。
  • 建議使用 Temporal.Instant.compare() 進行排序,並使用 a.equals(b) 進行相等性檢查;請勿檢查 Temporal.Instant.compare(a, b) === 0 或比較 .toString() 輸出內容。
  • 請勿使用 new Date() 或 Date.parse() 剖析高精確度時間戳記字串來排序,因為這些字串會截斷至毫秒,並產生衝突。
  • 請勿將區間縮減為單一數字顯示。duration.total('nanoseconds') 會傳回 Number,如果時間範圍超過約 104 天,則會超過 Number.MAX_SAFE_INTEGER,並在不顯示任何訊息的情況下降低精確度。減去 epochNanoseconds (BigInt) 可得出精確值,但會捨棄單位結構;請將間隔保留為 Temporal.Duration,並使用 .toLocaleString() 設定格式。
  • 務必使用 instant.toZonedDateTimeISO(timeZone) 將 Instant 轉換為 Temporal.ZonedDateTime,向使用者顯示事件的本地化時鐘時間。

備用策略

Temporal 的備用方案和瀏覽器支援

如果瀏覽器尚未支援原生 Temporal API,請使用功能偵測並動態載入 temporal-polyfill (約 15 KB min+gz)。

請注意,匯入 temporal-polyfill 不會自動將 Temporal 物件指派給全域範圍。如果程式碼依賴 globalThis.Temporal,請明確指派。

// Check if Temporal is supported natively
(async () => {
  if (typeof Temporal === 'undefined') {
    // Load the polyfill conditionally
    const module = await import('https://esm.sh/temporal-polyfill');
    globalThis.Temporal = module.Temporal;
    // Extend Date.prototype if needed
    Date.prototype.toTemporalInstant = module.toTemporalInstant;
  }
  initializeApp();
})();