← 返回系統更新日誌列表

列車定位 DOM 渲染瓶頸修正

發布日期:2026-03-30 | 系統架構與技術更新報告

專案背景與開發脈絡

在我們開發即時大眾運輸追蹤系統的過程中,最重要的核心功能無疑是「即時列車定位」。為了讓使用者能夠準確掌握每一班列車的當前位置,系統需要高頻率地接收後端伺服器推送的定位資料,並將這些資料即時反映在前端的互動式地圖或路線圖上。這看似簡單的「資料轉畫面」過程,在列車數量龐大且更新頻率極高的情況下,實際上隱藏著極大的效能挑戰。當畫面上同時存在數十甚至數百個移動中的列車節點時,每一次位置的更新都意味著需要對網頁的文檔物件模型(DOM)進行操作。如果處理不當,這些操作將成為拖垮整個網頁效能的致命傷。

開發過程中踩過的坑 (Pitfalls)

在初期的開發階段,為了快速實現功能,我們採用了最直覺的實作方式:每當接收到一批新的列車資料時,前端程式碼便會利用一個迴圈遍歷這批資料,並在迴圈內部逐一創建新的 DOM 元素(例如代表列車的標籤與圖示),接著直接將該元素附加(appendChild)到頁面上現有的容器節點中。在開發初期的測試環境下,因為模擬的列車數量較少,這個方法運作得相當順暢。然而,當我們將系統切換至真實的營運數據流時,災難就發生了。

我們發現,每當資料大量湧入時,網頁會出現嚴重的卡頓與延遲,甚至在某些效能較差的行動裝置上,瀏覽器會直接呈現「無回應」的凍結狀態。經過深入的診斷,我們打開了 Chrome DevTools 的效能分析面板(Performance Tab),記錄了幾秒鐘的運行狀況。火焰圖(Flame Chart)無情地揭露了問題的根源:連續且密集的「重新計算樣式(Recalculate Style)」與「版面配置(Layout)」任務佔據了主執行緒絕大部分的時間。這就是前端開發中惡名昭彰的「佈局顛簸(Layout Thrashing)」。當我們在迴圈中不斷地將新元素直接插入活躍的 DOM 樹時,瀏覽器被迫在每次插入後立即重新計算整個畫面的佈局,這導致了極度昂貴的效能開銷。

診斷與技術修正方案

確認了效能瓶頸在於頻繁的 DOM 寫入操作後,我們必須找到一種方法來減少這些操作對瀏覽器渲染引擎的衝擊。解決方案的核心概念是「批次處理(Batching)」。與其讓瀏覽器做一百次微小的佈局更新,不如我們在記憶體中先將這一百個元素準備好,然後一次性地丟給瀏覽器處理。這正是 Web API 中 DocumentFragment 存在的意義。

DocumentFragment 是一個輕量級的文檔物件,它並不是活躍 DOM 樹的一部分。因此,對它進行任何的節點新增或修改,都不會觸發瀏覽器的重繪(Repaint)或重排(Reflow)。我們對原有的渲染邏輯進行了重構:首先建立一個空的 DocumentFragment,然後在資料遍歷的迴圈中,將所有新創建的列車元素附加到這個 Fragment 裡面。當迴圈結束、所有元素都已經就緒後,我們再執行一次性的 DOM 插入操作,將整個 Fragment 附加到目標容器中。當 Fragment 被插入 DOM 樹時,只有其子節點會被插入,Fragment 本身則不會,這完美地達成了我們批次更新的需求。

修改前 (Before)

function renderTrains(trainData) {
  const container = document.getElementById('map-container');
  // 清空舊有資料
  container.innerHTML = ''; 
  
  trainData.forEach(train => {
    const el = document.createElement('div');
    el.className = 'train-marker';
    el.style.left = `${train.x}px`;
    el.style.top = `${train.y}px`;
    el.textContent = train.id;
    
    // 坑:每次迴圈直接操作活躍的 DOM,引發重排
    container.appendChild(el); 
  });
}

修改後 (After)

function renderTrains(trainData) {
  const container = document.getElementById('map-container');
  container.innerHTML = '';
  
  // 解法:使用 DocumentFragment 在記憶體中批次處理
  const fragment = document.createDocumentFragment();
  
  trainData.forEach(train => {
    const el = document.createElement('div');
    el.className = 'train-marker';
    el.style.left = `${train.x}px`;
    el.style.top = `${train.y}px`;
    el.textContent = train.id;
    
    // 將節點附加到不會觸發重排的 fragment 中
    fragment.appendChild(el);
  });
  
  // 一次性將所有節點插入活躍的 DOM 中
  container.appendChild(fragment);
}

全新的使用者體驗 (UX)

這次的底層技術升級,帶來了立竿見影且極具感官震撼的體驗提升。在更新上線後,使用者最直觀的感受就是「極致的流暢」。過去在地圖縮放或是大量列車同時更新時所產生的畫面撕裂感與游標卡頓完全消失了。現在,即使在尖峰時刻、畫面上同時追蹤上百個運輸單位,列車圖標的移動依然絲滑如水,動畫過渡自然且毫無遲滯。這種無縫銜接的視覺回饋,不僅大幅降低了使用者在查詢時的焦慮感,更讓整個應用程式在操作手感上達到了原生應用(Native App)級別的專業水準。對行動裝置的使用者而言,這也意味著手機發熱減少、電池續航力提升,因為我們釋放了被無謂耗損的 CPU 運算資源。這是一次從程式碼底層出發,卻在終端使用者體驗上開花結果的成功最佳化。