使用 Temporal.Instant 对高精度时间戳进行排序

Web 功能: Temporal

日志查看器、审核时间轴和遥测界面通常会提取事件,这些事件的 ISO 8601 时间戳由服务器、数据库或原生时钟以微秒或纳秒精度记录(例如 2026-04-01T12:00:00.000123456Z)。由于旧版 Date 对象仅存储毫秒,因此使用 new Date() 或 Date.parse() 解析这些字符串会静默截断亚毫秒数字,导致同一毫秒内的不同事件发生冲突并以不稳定的顺序排序。

Temporal.Instant 表示 UTC 时间轴上的确切时间点,精确到纳秒,不附加时区。使用 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 字符串(始终采用 UTC 格式,带有 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();
})();