前端的時間序列圖表上線後,有使用者反應:圖上標的時間點怪怪的,事件明明發生在下午,圖上卻標在早上。
第一反應當然是「又是後端時區問題」(畢竟才剛被 Day 6 的 UTC 雷炸過),把後端查了個底朝天:資料正確、API 回應正確、時間戳正確。問題不在後端。
凶手是前端圖表函式庫的一個設計決策:它渲染時間軸標籤時,永遠以 UTC 解讀你餵給它的時間戳,不做任何在地時區轉換。 這是文件裡有寫、但沒人會在出事前去讀的那種行為。
而我餵資料的寫法是:
// 錯誤:new Date(...) 以瀏覽器在地時區解讀這串參數
const ts = new Date(2026, 7, 8, 14, 30).getTime() / 1000;
new Date(y, m, d, h, min) 用瀏覽器的在地時區解讀參數——在 UTC+8 的環境裡,「在地 14:30」的 epoch 值,被圖表庫拿去以 UTC 解讀,就渲染成「06:30」。整條時間軸系統性地偏移 8 小時。
正確寫法一字之差:
// 正確:Date.UTC(...) 明確以 UTC 建構,圖表庫以 UTC 解讀,兩邊對齊
const ts = Date.UTC(2026, 7, 8, 14, 30) / 1000;
因為它示範了時區問題最惡劣的形態:兩端各自都「沒有錯」,錯在兩端對同一個數字的解讀方式不一致。 後端給的 epoch 是對的,圖表庫的渲染邏輯也是照文件走的,bug 存在於接縫處。這種 bug 你分開測兩端永遠測不到,只有端到端看最終畫面才會現形。
還有一個實務教訓:這個 bug 的症狀(時間差 8 小時)跟後端時區 bug 的症狀一模一樣,而我當時剛修完一個真正的後端時區 bug,於是天然地往後端找。「同樣的症狀,不同的根因」——除錯時的先入為主,讓我在後端浪費了不少時間。後來這條也寫進了地雷清單,特別標注「這是前端渲染層的問題,別再去翻後端」。
跨越邊界(後端→前端、系統→函式庫、服務→服務)傳遞時間,唯一安全的約定是:傳遞明確為 UTC 的 epoch,接收端明確知道它是 UTC。 任何一端用「在地時間」的心智模型處理,接縫處就會裂開。時區 bug 從來不在某一端裡面,永遠在兩端之間。