在日常的通勤體驗中,乘客最常抱怨的問題之一就是「車廂過於擁擠」。特別是在尖峰時段,如果能提前知道即將進站的列車哪一節車廂比較空,乘客就能在月台上提前移動到對應的候車位置。為了解決這個長久以來的痛點,我們在這次的系統更新中,隆重推出了「車廂載客量即時顯示」功能模組。這項新功能無疑為整體的使用者體驗(UX)帶來了質的飛躍。
現在,當使用者在應用程式中查詢班次動態時,介面上會以非常直觀的顏色編碼顯示每一節車廂的即時擁擠度。我們將載客量分為三個層級:綠色代表座位寬鬆、黃色代表站立空間普通、紅色則代表車廂擁擠。這項資訊的透明化,不僅減少了乘客在列車進站時於月台上盲目奔跑的焦慮感,更有效地促進了人流在各節車廂間的平均分佈,大幅提升了整體的搭乘品質與安全性。
不過,在開發這個即時數據模組的過程中,我卻遭遇了一個相當棘手的效能瓶頸。為了提供最精確的資訊,我們的後端系統會將全線所有運作中列車、所有車廂的載客量數據,打包成一個極度龐大且結構複雜的多維陣列(Multidimensional Array)傳送給前端。在初版的實作中,為了將這些即時數據對應並渲染到畫面上,我直接使用了多層嵌套的 for 迴圈來進行同步的陣列遍歷與 DOM 節點查找。
這個「坑」的具體表現極具破壞性:每當前端透過 API 獲取最新的載客量數據並嘗試更新畫面時,整個應用程式的主執行緒(Main Thread)會被死死卡住(Freeze)長達數秒鐘。在這段延遲時間內,使用者無法滾動頁面,點擊按鈕也沒有任何反應,畫面出現了嚴重的卡頓與掉幀(Jank)。對於一個強調「即時動態」的應用程式來說,這種中斷使用者操作流程的體驗是完全無法接受的。
為了精準診斷並定位這個效能問題,我開啟了瀏覽器的效能分析工具(Performance Profiler),並錄製了一段數據更新時的效能軌跡。結果正如我所料,火焰圖(Flame Chart)中出現了標示為「Long Task」的紅色長條警告,直指那個負責解析數據的渲染函式。多層嵌套的迴圈不僅時間複雜度高達 O(N^3),而且在迴圈最內層還頻繁地進行了低效的字串組合與 DOM 查詢(如 getElementById),這無疑是效能殺手。
為了解決這場效能危機,我對資料處理邏輯進行了徹底的重構。我放棄了在每次渲染時都去深層遍歷原始多維陣列的作法。取而代之的是,在收到數據的當下,我先利用 JavaScript 原生且高度優化的陣列方法(如 reduce),將這個龐大的數據結構「扁平化」,預先建構出一個以「列車編號」為鍵值(Key)的 Lookup Dictionary(雜湊表)。這種作法將原本 O(N^3) 的時間複雜度,在查詢階段大幅降至 O(1)。同時,我將 DOM 更新操作打包進 requestAnimationFrame 中,確保畫面的重繪與瀏覽器的渲染週期同步。修改之後,資料解析的耗時從原本的數百毫秒驟降至不到十毫秒,主執行緒不再阻塞,即時載客量的畫面更新變得如絲綢般滑順。
function updateTrainLoads(rawData) {
// 效能極差的 O(N^3) 嵌套迴圈與頻繁 DOM 操作
for (let i = 0; i < rawData.length; i++) {
let train = rawData[i];
for (let j = 0; j < train.cars.length; j++) {
let car = train.cars[j];
// 每次都去重新查詢 DOM,嚴重拖慢效能
let element = document.getElementById(
'train-' + train.id + '-car-' + car.id
);
if (element) {
element.className = 'load-indicator ' +
getLoadStatus(car.passengers);
}
}
}
}
function updateTrainLoads(rawData) {
// 預先建構 Lookup Dictionary,將查詢時間複雜度降至 O(1)
const loadDict = rawData.reduce((acc, train) => {
acc[train.id] = train.cars.reduce((carAcc, car) => {
carAcc[car.id] = getLoadStatus(car.passengers);
return carAcc;
}, {});
return acc;
}, {});
// 透過 requestAnimationFrame 確保畫面更新不卡頓
requestAnimationFrame(() => {
document.querySelectorAll('.train-car').forEach(el => {
const trainId = el.dataset.trainId;
const carId = el.dataset.carId;
if (loadDict[trainId] && loadDict[trainId][carId]) {
el.className = `train-car load-${loadDict[trainId][carId]}`;
}
});
});
}