大約一週前,我收到了幾位測試者的回饋,反映在使用手機瀏覽列車追蹤介面時,畫面有明顯的卡頓感。起初我在自己的桌機上測試時並未察覺異常,畢竟桌機的 CPU 效能足以掩蓋很多問題。但當我打開 Chrome DevTools 的 Performance 面板錄製了一段操作紀錄後,事情變得清楚了——即便在桌機上,每一幀的渲染時間也已經逼近了 16ms 的上限。換到中低階的行動裝置上,掉幀自然不可避免。
問題出在我最初為了求快而選用的渲染架構。當時我使用 setInterval 搭配固定的毫秒間隔來驅動列車位置的視覺更新。每當計時器觸發,JavaScript 會重新計算所有列車元素的座標,然後直接修改它們的 CSS left 與 top 屬性。
這個方法有兩個根本性的問題。第一,setInterval 的觸發時機與瀏覽器的畫面刷新週期完全脫鉤。瀏覽器通常以每秒 60 幀 (60fps) 的頻率刷新畫面,也就是大約每 16.67ms 一幀。但 setInterval 並不會配合這個節奏——它可能在一幀的中間觸發,導致計算完成後必須等到下一幀才能渲染,白白浪費了一幀的時間;也可能在兩幀之間觸發兩次,導致其中一次的計算結果直接被覆蓋,算力完全浪費。
第二,直接修改 left 和 top 這類影響元素幾何位置的屬性,會觸發瀏覽器的重排 (Reflow)。每一次重排都意味著瀏覽器必須重新計算整個頁面的佈局,這是一個代價極高的操作。當畫面上有 30 到 40 個列車元素同時更新位置時,每一幀都會引發數十次重排,主執行緒的負擔可想而知。
解決方案的核心是將整個渲染迴圈遷移到 requestAnimationFrame (rAF) 上。rAF 是瀏覽器原生提供的動畫排程 API,它保證回呼函數會在每一幀的繪製之前被精確調用。這意味著我的座標計算永遠與瀏覽器的刷新週期保持同步——不會過早、不會過晚、更不會遺漏。
同時,我將所有的位置更新從修改 left/top 改為使用 transform: translate3d()。transform 屬性不會觸發重排,瀏覽器會將這類變換交由 GPU 的合成層 (Compositor Layer) 處理,完全繞過了主執行緒的佈局計算。這兩項修改加在一起,效果立竿見影。
// 基於 setInterval 的定時器驅動渲染
var animTimer = setInterval(function() {
trainElements.forEach(function(el, i) {
var pos = calcPosition(trains[i]);
el.style.left = pos.x + 'px';
el.style.top = pos.y + 'px';
});
}, 33); // 大約 30fps
// 基於 requestAnimationFrame 的瀏覽器同步渲染
function renderLoop(timestamp) {
trainElements.forEach(function(el, i) {
const pos = calcPosition(trains[i], timestamp);
el.style.transform =
`translate3d(${pos.x}px, ${pos.y}px, 0)`;
});
requestAnimationFrame(renderLoop);
}
requestAnimationFrame(renderLoop);
重構完成後,我在同一台中階 Android 手機上重新錄製了 Performance Profile。結果令人滿意:重構前,平均每幀的渲染時間約為 28ms,超過了 16.67ms 的及格線,瀏覽器被迫降頻到約 30fps,且經常出現長達 50ms 以上的長幀 (Long Frame)。重構後,平均每幀的渲染時間降到了 6ms 左右,長幀幾乎完全消失,動畫穩定維持在 60fps。
更值得一提的是 CPU 佔用率的變化。由於 transform 的計算被卸載到了 GPU 合成層,主執行緒的 CPU 負載從先前的 45% 以上降到了不足 15%。這意味著即使在動畫持續運行的情況下,使用者與其他介面元素的互動(例如點擊按鈕、捲動頁面)也不會受到任何影響。在修改之前,使用者曾反映在動畫運行時按鈕的點擊回應有「遲鈍感」,這正是主執行緒被佔滿的典型症狀。
對於使用者而言,這次更新帶來的是一個肉眼可見的流暢度提升。列車圖示在軌道上的移動不再有任何肉眼可見的頓挫,就像在觀看一段預先渲染好的動畫影片一樣平順。尤其在捲動頁面的同時觀察列車移動時,兩個動作互不干擾的體驗讓整個應用的質感提升了一個層次。有測試者在回饋中提到:「更新之後,感覺手機都變快了。」這句話雖然有些誇張,但恰恰說明了底層渲染效能對使用者感知的深遠影響。良好的效能表現不是功能的附屬品,它本身就是一項核心功能。