本文 AI 產出,尚未審核

日期與時間(Date & Time)性能與精度考量

簡介

在前端與後端的 JavaScript 應用中,日期與時間的處理往往是不可或缺的需求——無論是顯示使用者的本地時間、計算倒數計時、或是記錄事件的時間戳。看似簡單的 new Date(),在大量資料或高頻率的計時需求下,卻可能成為效能瓶頸,甚至因精度不足而產生錯誤。

本篇文章將聚焦於 Date 物件的效能與精度,說明底層實作、不同計時 API 的差異、以及在實務開發中避免常見陷阱的最佳做法。透過具體範例與解析,協助初學者與中級開發者在寫程式時,能夠在 正確性效能 之間取得平衡。


核心概念

1. Date 物件的底層實作與精度限制

Date 物件在 JavaScript 中以 毫秒(ms) 為最小單位,內部以 64 位有號整數儲存自 1970‑01‑01T00:00:00Z 起的毫秒數。這意味著:

  • 精度上限:理論上能表達到 ±2⁵³ 毫秒(約 285,616 年),但實際上瀏覽器大多限制在 ±8,640,000,000,000(約 273,785 年)之內。
  • 時區資訊Date 只儲存 UTC 時間,所有本地化顯示(如 toLocaleString())都在執行時根據系統時區計算。

注意Date 物件不支援微秒(µs)或納秒(ns)等更高解析度的時間戳,若需要更精細的計時,請改用 Performance API。

// 建立一個 Date 物件,顯示其內部的毫秒時間戳
const now = new Date();
console.log('UTC 毫秒時間戳:', now.getTime()); // 例如 1731234567890

2. 時間精度:毫秒 vs 微秒 vs 高解析度計時

在需要 高頻率測量(如動畫、遊戲迴圈、或性能測試)時,Date.now() 的毫秒精度往往不足。此時可改用 performance.now(),它提供 微秒級(實際精度約 0.1~0.5 ms,取決於瀏覽器與硬體)的時間戳,且不會受到系統時間變更的影響。

// 使用 performance.now() 取得高解析度時間戳
const start = performance.now(); // 例如 12345.678
// 執行一段需測試的程式
for (let i = 0; i < 1e6; i++) {} // 模擬工作
const elapsed = performance.now() - start;
console.log(`執行時間:${elapsed.toFixed(3)} ms`);

實務建議

  • 只要是 相對時間差(例如「執行多久」),使用 performance.now()
  • 若需要 絕對時間點(例如「寫入資料庫的時間戳」),仍以 Date.now() 為主,或自行將 performance.now() 加上 Date.now() 的基準值。

3. Date.now() vs new Date() 的效能比較

在大量產生時間戳的情境(如每秒上千筆日誌),Date.now()執行成本 明顯低於 new Date(),因為前者直接返回毫秒數,後者則要建構完整的物件。

console.time('Date.now() 1000000 次');
for (let i = 0; i < 1_000_000; i++) {
  Date.now();
}
console.timeEnd('Date.now() 1000000 次');

console.time('new Date() 1000000 次');
for (let i = 0; i < 1_000_000; i++) {
  new Date();
}
console.timeEnd('new Date() 1000000 次');

測試結果(以 Chrome 為例)

  • Date.now() 約 5~8 ms
  • new Date() 約 3045 ms
    因此在「只需要時間戳」的情況下,避免不必要的 new Date(),可減少 5
    10 倍的效能開銷。

4. 時區、夏令時間與日期運算的代價

Date 物件在本地化顯示或進行時區轉換時,需要查詢系統的時區資訊與夏令時間規則,這會帶來額外的計算成本。若在迴圈中不斷呼叫 toLocaleString()toString()getTimezoneOffset(),將顯著拖慢效能。

// 不建議:在大量資料迭代中每筆都轉成本地字串
const timestamps = Array.from({ length: 10000 }, (_, i) => Date.now() + i * 1000);
const localStrings = timestamps.map(ts => new Date(ts).toLocaleString());
// 上例會觸發 10,000 次時區計算

優化方式

  1. 一次性取得時區偏移,之後以數值運算處理。
  2. 若需要大量格式化,使用第三方庫(如 date-fns-tz)的 快取機制 或自行實作簡易的 UTC → 本地轉換。
// 只取一次時區偏移 (分鐘)
const tzOffset = new Date().getTimezoneOffset(); // 例如 -480 (UTC+8)
const utcTimes = timestamps.map(ts => ts - tzOffset * 60 * 1000);

5. 大量日期運算的最佳策略

在需要 批次計算(如排程、報表產生)時,建議:

  • 使用純數值(毫秒)進行加減、比較,減少 Date 物件的建立。
  • 避免 Date.parse(),因其必須解析字串,成本高且易受不一致的字串格式影響。
  • 採用 Intl.DateTimeFormat 只在最後一步顯示時才格式化,避免在計算過程中頻繁轉換。
// 範例:計算未來 30 天的每日開始時間(UTC)
// 只使用毫秒,最後一次性格式化
const DAY_MS = 24 * 60 * 60 * 1000;
const start = Date.UTC(2025, 0, 1); // 2025-01-01 00:00:00 UTC
const dates = [];

for (let i = 0; i < 30; i++) {
  dates.push(start + i * DAY_MS);
}

// 只在需要顯示時才轉成字串
const formatter = new Intl.DateTimeFormat('zh-TW', {
  year: 'numeric',
  month: '2-digit',
  day: '2-digit',
  hour: '2-digit',
  minute: '2-digit',
  timeZone: 'Asia/Taipei',
});

const display = dates.map(ts => formatter.format(ts));
console.log(display);

常見陷阱與最佳實踐

陷阱 可能的問題 推薦做法
使用 new Date(string) 直接解析不標準字串 不同瀏覽器解析結果不一致,導致跨平台錯誤 使用 ISO 8601 標準(YYYY-MM-DDTHH:mm:ss.sssZ)或 Date.parse() 前先正規化
在迴圈內頻繁呼叫 toLocaleString() 大量時區計算拖慢效能 只在最終呈現階段呼叫,或自行實作快取版的本地化函式
Date.now() 取得時間戳後再使用 new Date() 轉換 多餘的物件建構,浪費記憶體與 CPU 若只需比較或計算,直接使用毫秒數;若需要格式化,最後一次性轉換
忽略夏令時間(DST)變化 在 DST 變更的日期上計算錯誤(如跨日或跨時段的加減) 使用 Intl API 或第三方庫(luxondate-fns-tz)取得正確的時區規則
performance.now() 的相對時間直接寫入資料庫 失去絕對時間參照,難以與其他系統對齊 Date.now() 為基準,加上 performance.now() 的差值,得到「高精度的絕對時間」

最佳實踐清單

  1. 先決定需求:是「絕對時間」還是「相對時間」。
  2. 盡量使用數值(毫秒) 進行運算,僅在 UI 階段才轉換為 Date 或字串。
  3. 高頻率計時performance.now()持久化時間戳Date.now().
  4. 時區相關 → 使用 Intl.DateTimeFormatdate-fns-tz,避免自行寫複雜的時區換算。
  5. 測試跨瀏覽器:特別是字串解析與時區顯示,確保行為一致。

實際應用場景

1. 前端動畫與遊戲迴圈

在 60 FPS(每秒 60 幀)的遊戲中,需要精確計算每一幀的渲染時間。使用 performance.now() 可避免因系統時間調整(手動改時、NTP)造成的跳躍。

let last = performance.now();
function gameLoop() {
  const now = performance.now();
  const delta = now - last; // 兩幀之間的毫秒差
  updateGame(delta);
  renderGame();
  last = now;
  requestAnimationFrame(gameLoop);
}
gameLoop();

2. 日誌系統與分布式追蹤

後端服務常以 UTC 毫秒時間戳 記錄事件,因為它不受時區變更影響。若需要更精細的排程(如每毫秒觸發),可在寫入前把 performance.now() 加上 Date.now() 的基準值。

function getHighPrecisionTimestamp() {
  return Date.now() + performance.now() % 1; // 加上小數部分
}
logEvent({ ts: getHighPrecisionTimestamp(), message: 'User clicked' });

3. 報表產生與日期區間查詢

大量的日期區間查詢(如「過去 90 天每日銷售」)若每筆資料都產生 Date 物件會嚴重拖慢資料庫的查詢速度。最佳做法是 先把區間轉成毫秒範圍,在資料庫層面使用數值比較。

-- 假設資料表 sales 有 column created_at (bigint, 儲存 UTC 毫秒)
SELECT DATE(FROM_UNIXTIME(created_at / 1000)) AS day, SUM(amount)
FROM sales
WHERE created_at BETWEEN :startMs AND :endMs
GROUP BY day;

在 JavaScript 中只需要:

const now = Date.now();
const ninetyDaysAgo = now - 90 * 24 * 60 * 60 * 1000;
fetch(`/api/sales?start=${ninetyDaysAgo}&end=${now}`);

總結

  • Date 物件的最小精度為 毫秒,適合絕對時間戳的儲存與比較;若需要 微秒級更高解析度,請使用 performance.now()
  • 效能差異 明顯:Date.now()new Date() 的 1/5~1/10,盡量在只需要時間戳的情境下直接呼叫 Date.now()
  • 時區與夏令時間的計算成本高,應盡量 一次取得偏移,或在最後一步才做本地化格式化。
  • 大量日期運算時,以 毫秒數值 為基礎,減少 Date 物件的建立;必要時使用 Intl.DateTimeFormat 或專門的時區函式庫。
  • 常見陷阱包括不標準字串解析、頻繁本地化、忽略 DST,以及將相對時間直接寫入持久化層。遵循上述最佳實踐,可讓程式碼在 正確性、可維護性與效能 之間取得最佳平衡。

掌握了這些概念與技巧,你就能在日常開發、性能優化、以及跨平台時間處理上更加游刃有餘。祝你寫出 既快又準 的日期時間程式碼!