
最近又接了個可視化大屏的活兒需求里果然又出現(xiàn)了那句熟悉的“表格數(shù)據(jù)要滾動起來”。做大屏項目多了你會發(fā)現(xiàn)這個需求幾乎繞不開實時訂單、告警記錄、設(shè)備狀態(tài)、人員名單總有一張表要在大屏上不停地“流動”。網(wǎng)上搜實現(xiàn)方案確實不少但很多代碼照搬進項目里就出問題——滾動卡頓、邊界跳變、后臺切回來動畫錯亂、大屏上字發(fā)虛踩坑踩得多了我把這幾年在可視化大屏項目里用過的表格數(shù)據(jù)滾動方案整理了一遍從最簡單到最復(fù)雜把每種方案的原理、適用場景和坑都寫清楚。這篇文章適合正在做可視化大屏、數(shù)據(jù)監(jiān)控看板或者想給自己的后臺加一個滾動表格但不想踩坑的開發(fā)者。無論你是用原生 JavaScript、Vue 還是 React思路都是通用的代碼例子我盡量給成可以直接抄走用的形態(tài)。1. 先把需求想清楚大屏表格滾動到底要解決什么問題1.1 四種典型需求對應(yīng)不同技術(shù)路線我拆過不少大屏需求表面上看都是“表格滾動”但細(xì)問下來通常是四種完全不同的訴求第一種是“純展示滾動”數(shù)據(jù)源固定或低頻更新比如昨日銷售排行榜、工單完成率排名表格內(nèi)容從頭滾到尾再循環(huán)。這種需求最輕松一個 CSS transform 動畫甚至一行 setInterval 就能搞定。第二種是“實時數(shù)據(jù)追加滾動”比如告警列表、消息推送記錄每隔幾秒或十幾秒就有一批新數(shù)據(jù)進來表格要平滑地往上滾同時把新數(shù)據(jù)加進來。這種就要求方案本身具備“隨時改數(shù)據(jù)源、隨時重置高度”的能力不能用寫死的 CSS 動畫。第三種是“數(shù)據(jù)量較大的滾動”比如要展示幾萬行日志或者監(jiān)控系統(tǒng)吐出來的全量指標(biāo)數(shù)據(jù)。這個時候 DOM 數(shù)量一多任何“把所有行渲染出來再滾動”的方案都會讓大屏主機卡成幻燈片必須上虛擬列表。第四種是“跳躍式輪播”不是平滑滾動而是每 3 秒停一下滾一行像舊式廣告燈箱那樣。有些客戶就喜歡這種觀感說“有節(jié)奏感”這時候你還得換一種實現(xiàn)策略。我的建議是接到需求先別急著寫代碼問清楚一句話——“這些數(shù)據(jù)多久更新一次、大概有多少條”。這兩個問題直接決定下面五套方案里你該選哪套。1.2 大屏環(huán)境與普通后臺頁面的關(guān)鍵差異給大屏寫滾動表格和給后臺管理頁面寫滾動表格很多看似“很正常”的做法在大屏上會翻車。第一個差異是運行時長。大屏項目上線之后往往 7×24 小時不關(guān)機、不刷新頁面。普通控制臺頁面上的 setInterval切到后臺標(biāo)簽頁會被瀏覽器降頻用戶回到頁面之后繼續(xù)跑你很難察覺到異常但大屏上如果定時器累積誤差、requestAnimationFrame 在長時間掛起后產(chǎn)生巨大時間差就會讓滾動速度突然變得離譜或者干脆停住。第二個差異是分辨率。大屏通常不是普通顯示器常見的是 1920×1080 拼接屏、3840×2160 的 4K 屏甚至是異形 LED 屏。頁面要做等比縮放適配縮放之后表格文字會不會發(fā)虛、滾動速度和視覺比例是否協(xié)調(diào)這些都要提前考慮。第三個差異是硬件性能。大屏后端主機的配置差異巨大有的客戶用的是 i5 核顯的迷你機有的用好幾年前的工控機跑一套 3D 地區(qū)地圖可視化大屏已經(jīng)快滿負(fù)荷了。如果你的表格滾動方案動不動觸發(fā)整塊區(qū)域的重繪和回流把 CPU 拉滿那些 3D 地圖場景會一起掉幀客戶開完會就要找你。所以做大屏表格滾動不能只“實現(xiàn)功能”要當(dāng)成一個需要長期穩(wěn)定運行的獨立小模塊來做。1.3 選型前的思考路徑我一般按這個路徑選型數(shù)據(jù)量小于 100 條、行高固定、數(shù)據(jù)幾乎不變首選 CSS transform 無縫循環(huán)代碼量最少性能最好。數(shù)據(jù)量小于 500 條、行高固定、會動態(tài)追加數(shù)據(jù)、需要平滑滾動requestAnimationFrame transform也是我項目里最常用的一套。數(shù)據(jù)量小于 500 條、需要“滾一行停一下”的效果定時器 CSS transition。數(shù)據(jù)量幾千到上萬虛擬列表或者讓后端只返回 TOP 50 條。團隊工期緊、數(shù)據(jù)形態(tài)簡單可以考慮無縫滾動插件但要做好改樣式的心理準(zhǔn)備。下面從簡單到復(fù)雜把這五套方案逐一拆開講。2. 五種實現(xiàn)方案詳解從簡單到復(fù)雜逐個拆2.1 CSS transform 無縫循環(huán)滾動代碼量最少如果數(shù)據(jù)固定不變就用最經(jīng)典的“雙份內(nèi)容 transform 位移”方案。原理不復(fù)雜把表格內(nèi)容復(fù)制成完全相同的兩份首尾相接放到同一個容器里然后讓容器整體向上位移位移到“正好走完一份內(nèi)容高度”時瞬間重置回 0。因為兩份內(nèi)容長得一模一樣用戶肉眼根本看不出重置動作就像無限滾動一樣。核心代碼只有兩段。先看 HTML 結(jié)構(gòu)div classscroll-wrapper div classscroll-content idscrollContent !-- 這里放兩份相同的表格內(nèi)容 -- /div /div外層設(shè)置固定高度和 overflow: hidden內(nèi)層放兩份列表然后寫動畫.scroll-wrapper { height: 400px; overflow: hidden; } .scroll-content { will-change: transform; animation: scrollUp 20s linear infinite; } keyframes scrollUp { 0% { transform: translateY(0); } 100% { transform: translateY(-50%); } }JavaScript 部分只需要把列表數(shù)據(jù)渲染兩份出來。為什么位移 50% 而不是某個固定像素值因為內(nèi)層總高度是列表高度 × 2translateY(-50%) 正好是“一份內(nèi)容的高度”這樣即使內(nèi)容高度變化動畫也能保持“滾完一份再從頭開始”的語義不需要手動改數(shù)值。這套方案的優(yōu)點非常突出不跑 JS 邏輯瀏覽器把動畫丟給合成器處理CPU 占用極低代碼量也極少。缺點也很明顯動畫的位移距離和速度比例是寫死在 CSS 里的如果數(shù)據(jù)是動態(tài)追加的、行高不固定、或者需要“滾動到某一幀停下來”它都很難優(yōu)雅處理。另外提醒一句不要給兩份內(nèi)容之間加 margin 或 gap否則 50% 這個比例不再等于一份內(nèi)容的高度循環(huán)時會出現(xiàn)明顯的跳躍。2.2 requestAnimationFrame 逐行精準(zhǔn)滾動項目里最常用如果你要在滾動過程中追加數(shù)據(jù)、控制速度、懸停暫停CSS 動畫就不夠了我用得最多的方案是 requestAnimationFrame transform。核心思路是用 requestAnimationFrame 驅(qū)動一個循環(huán)每幀根據(jù)時間差算出應(yīng)該位移多少像素更新容器的 transform。因為每幀都在 JS 里算位置所以可以隨時改速度、隨時暫停、隨時追加數(shù)據(jù)并重新計算高度。寫一個最小實現(xiàn)type ScrollOptions { speed: number; // 滾動速度像素/秒 container: HTMLElement; // 滾動的外層容器 }; function startScroll({ speed, container }: ScrollOptions) { const content container.querySelector(.scroll-content) as HTMLElement; // 兩份內(nèi)容保證無縫循環(huán) let offset 0; let lastTime 0; let rafId 0; const step (time: number) { if (!lastTime) lastTime time; // 限制單幀最大時間差防止后臺切回來時瞬間滾很遠(yuǎn) const delta Math.min((time - lastTime) / 1000, 0.1); lastTime time; // 滾動高度取一份內(nèi)容的高度而不是整個容器 const singleHeight content.scrollHeight / 2; offset speed * delta; if (offset singleHeight) { offset 0; } content.style.transform translate3d(0, ${-offset}px, 0); rafId requestAnimationFrame(step); }; rafId requestAnimationFrame(step); return () cancelAnimationFrame(rafId); }這里有幾個關(guān)鍵點必須說清楚。第一一定要用time時間戳算增量而不是每幀加固定值。不同大屏主機的刷新率不一樣有的是 60Hz有的是 30Hz如果每幀固定加 0.5 像素高刷屏上就會滾得比低刷屏快一倍觀感完全失控。用(time - lastTime) / 1000算出真實秒數(shù)再乘以速度就可以做到“無論多少幀率每秒位移量都一樣”。第二Math.min(..., 0.1)一定要加。大屏項目經(jīng)常要開會演示電腦合上蓋子再打開或者切換信號源requestAnimationFrame 會被瀏覽器掛起一段時間恢復(fù)時time直接跳了一大截不做限制的話表格會在 1 秒內(nèi)猛滾半屏非常嚇人。第三如果要追加數(shù)據(jù)不要在 scrollHeight 發(fā)生變化的過程中直接讀它的值。我的習(xí)慣是維護一個dataLength變量數(shù)據(jù)更新時手動計算新的單份高度而不是每次循環(huán)都去讀 scrollHeight。這樣能減少強制同步布局的頻率對性能更友好。這套方案雖然比 CSS 方案復(fù)雜一點但可控性最強我?guī)缀跛写笃另椖康谋砀駶L動都是用它做的。后面第 4 節(jié)我會給一個完整的 Vue3 組件。2.3 定時器 transition 的跳躍式滾動適合慢節(jié)奏展示有些大屏客戶的審美偏好是“一格一格跳”所以我單獨說下定時器方案。簡單實現(xiàn)是定時改變 transform同時給容器加上 transition 過渡let index 0; const rowHeight 36; // 單行高度 const visibleCount 8; // 可視行數(shù) const dataTotal data.length; const list document.getElementById(scrollList); setInterval(() { index (index 1) % (dataTotal - visibleCount 1); list.style.transition transform 0.6s ease; list.style.transform translateY(${-index * rowHeight}px); }, 3000);這段邏輯本身不難理解但有幾個容易翻車的實際坑。一是“跳回第一行”時的動畫。當(dāng) index 到達(dá)最后一行后如果直接重置 index 0transform 會從很高的負(fù)位移瞬間回到 0transition 會捕捉到這個變化產(chǎn)生一個“快速回滾一大段”的動畫。正確地做法是先取消 transition再用 requestAnimationFrame 強制重置到 0下一幀再把 transition 加回來這樣用戶看到的是“從末尾處停頓一下下一行又從頂部出現(xiàn)”。二是 setInterval 的累積誤差問題。JavaScript 定時器并不精確如果大屏主機負(fù)載高回調(diào)可能延遲執(zhí)行長跑之后節(jié)奏會越來越亂。我自己的經(jīng)驗是對這個場景不要過度依賴 setInterval 的計時間隔可以改為每 3 秒去讀取一下“當(dāng)前時間與上次執(zhí)行時間的差”如果差超過 1.5 倍就跳過本輪讓節(jié)奏盡量回到正軌。三是按鈕或事件綁定。因為列表容器一直在位移如果表格行里有按鈕需要點擊或者要實現(xiàn)鼠標(biāo)懸停暫停定時器方案會比較難受因為“當(dāng)前顯示的到底是第幾行”這個狀態(tài)需要額外換算。2.4 無縫滾動插件趕工時可以考慮如果項目周期緊你也用的是 Vue 這類框架可以試試現(xiàn)成的無縫滾動插件。這類插件里 vue-seamless-scroll 用得比較多它的配置項很直白const config { step: 0.5, // 滾動速度數(shù)值越大越快 limitMoveNum: 5, // 數(shù)據(jù)條數(shù)超過多少時啟用滾動 hoverStop: true, // 鼠標(biāo)懸停是否暫停 direction: 1, // 1 向上0 向下 openTouch: true, // 移動端觸摸開關(guān) singleHeight: 0, // 單條數(shù)據(jù)高度0 表示自動計算 singleWidth: 0, waitTime: 1000 // 單次動畫時長 };插件省事但我用過幾次之后基本放棄了原因有三個。第一插件內(nèi)部為了兼容各種數(shù)據(jù)格式做了很多 DOM 操作數(shù)據(jù)量大時反而比手寫 requestAnimationFrame 方案更卡。第二樣式鎖定比較死大屏項目里的表格通常要配合亮色/暗色主題、自定義字體、漸變背景插件默認(rèn)生成的那套結(jié)構(gòu)改起來費勁。第三免費的這類開源組件質(zhì)量參差不齊遇到 bug 你要自己去讀源碼修反而比手寫組件花的時間多。所以我現(xiàn)在的結(jié)論是如果是內(nèi)部項目、數(shù)據(jù)簡單、沒有強定制需求插件可以做如果是給客戶交付的可視化大屏項目或者大屏里帶了 3D 地區(qū)地圖這種重型可視化組件我建議還是用原生方案依賴少一點出問題好排查。2.5 虛擬列表滾動數(shù)據(jù)量上萬時用這個最后說虛擬列表。大屏表格真的需要展示上萬條數(shù)據(jù)嗎大部分場景是不需要的客戶更關(guān)心的是“最近 50 條告警”而不是把幾萬條全量數(shù)據(jù)鋪在屏幕上。但確實會遇到這樣的需求比如要把全量監(jiān)控指標(biāo)流水放進大屏這時候所有的“渲染全部 DOM 再滾動”方案都會卡死虛擬列表是唯一解法。虛擬列表的核心思想是視口高度固定數(shù)據(jù)總量不管多大只渲染可視區(qū)域附近的那十幾行。實現(xiàn)要點如下外層固定高度設(shè)置 overflow: hidden。內(nèi)層用一個占位容器高度 數(shù)據(jù)總條數(shù) × 行高保證滾動條/位移范圍正確。監(jiān)聽滾動到的偏移量計算出當(dāng)前應(yīng)該顯示從第幾行到第幾行。把計算出的那段數(shù)據(jù)渲染出來整體用 transform 定位到對應(yīng)偏移位置。核心計算邏輯大概是const ROW_HEIGHT 36; const VISIBLE_COUNT 10; const BUFFER 2; // 上下各多渲染幾行避免快速滑動時出現(xiàn)空白 function getRenderRange(scrollTop: number, total: number) { const start Math.max(Math.floor(scrollTop / ROW_HEIGHT) - BUFFER, 0); const end Math.min(start VISIBLE_COUNT BUFFER * 2, total); return { start, end }; }對大屏項目來說虛擬列表還有一個附加好處即使是在核顯主機上也不用擔(dān)心表格滾動導(dǎo)致整體 GPU 負(fù)載過高因為 DOM 數(shù)量始終很少。但虛擬列表的坑也不少。比如行高必須固定如果某一行內(nèi)容折行變成兩行整個計算就亂了比如大屏縮放后行高變成 40px但代碼里還按 36px 算會出現(xiàn)渲染行錯位。所以我的建議是能用后端限制數(shù)據(jù)量就不用虛擬列表實在要上虛擬列表確保行高固定并且把行高做成可配置參數(shù)適配大屏?xí)r好調(diào)。3. 大屏適配與性能調(diào)優(yōu)讓滾動不卡不漏細(xì)節(jié)3.1 不同適配方案下滾動速度的差異與處理大屏分辨率五花八門所以頁面通常要做適配。常見的適配方案有三種整體 scale 等比縮放、vw/vh 自適應(yīng)、rem 自適應(yīng)。它們對滾動速度的影響不太一樣。如果你用整體 scale 方案比如固定設(shè)計稿 1920×1080然后計算實際窗口和設(shè)計稿的比例整體縮放那么頁面內(nèi)部其實運行在一套虛擬像素里表格的滾動速度和視覺比例是同步縮放的。假設(shè)設(shè)計稿上表格每秒滾動 30px實際大屏是 3840×2160scale 是 2物理屏幕上每秒滾動 60px但相對于整個屏幕的占比并沒有變所以觀感是協(xié)調(diào)的不需要額外調(diào)整滾動速度。如果你用 vw/vh 布局表格容器的寬度高度都跟隨屏幕變化但表格行高通常還是用 px 或者按寬度比例計算可能導(dǎo)致行高和容器高度不匹配。比如行高 36px在 4K 大屏上看起來非常小如果強行把行高調(diào)成 72px滾動速度還按 30px/s 來設(shè)視覺上就會覺得滾得很慢。這種情況我的經(jīng)驗是把滾動速度也做成和行高相關(guān)的配置行高翻倍時速度也翻倍保持“每秒滾動約 0.8 行”的視覺節(jié)奏。最麻煩的是不等比拉伸方案就是把寬度拉滿、高度拉滿不管原始比例。這種適配下表格會被拉變形文字發(fā)虛滾動距離和視覺比例全對不上。我遇到這種項目一般會明確告訴客戶大屏要等比縮放寧可留黑邊也別拉伸變形。否則表格滾動的所有計算都會變得不可控。3.2 性能優(yōu)化合成層、回流、重繪的取舍大屏表格滾動想流暢關(guān)鍵不是把代碼寫得多花哨而是減少不必要的瀏覽器重繪和回流。我的經(jīng)驗里最重要的三條第一用 transform 而不是 top/left 或者 margin-top 來做位移。transform 的變化在絕大多數(shù)瀏覽器里可以走 GPU 合成不會觸發(fā)布局計算而 top/left 每幀變化都會觸發(fā)重排CPU 一高幀率就往下掉。這也是為什么我在前兩套方案里都用 translate3d(0, y, 0)而不是直接改 top。第二列表項里的樣式要克制。box-shadow、backdrop-filter、大量漸變背景都是重繪大戶。大屏表格本身信息密度就高你要是每行再加個陰影滾動的時候整個屏幕都在瘋狂重繪。我自己項目里滾動列表的行樣式通常只用背景色、邊框、文字顏色最多加一層很輕的底部分隔線。第三數(shù)據(jù)更新時避免一次性重建整個列表。如果新數(shù)據(jù)只是追加了十幾條盡量復(fù)用已有的 DOM 行只更新新增行。用 Vue 的話給每行設(shè)置穩(wěn)定的 key避免組件誤判而全部重建。另外有一種情況特別值得注意如果你用了整體 scale 縮放方案縮放本身就會讓瀏覽器對整塊頁面做合成處理這時候表格內(nèi)部的 will-change 屬性反而不要濫用。過度創(chuàng)建合成層會占用大量 GPU 內(nèi)存在大屏主機的核顯上可能得不償失。我一般只給最外層滾動容器加 will-change: transform列表項內(nèi)部一律不加。3.3 7x24 小時運行的穩(wěn)定性處理大屏是要連續(xù)跑好幾個月的滾動方案必須考慮“長時間運行”這個前提。我覺得有幾件事是必須做的監(jiān)聽visibilitychange事件頁面從隱藏切回可見時重置時間戳防止時間差爆炸。組件卸載時一定把 requestAnimationFrame 和所有事件監(jiān)聽都清掉。大屏項目經(jīng)常要二次進入定時器清理不干凈第二次進入就會出現(xiàn)“兩個滾動同時驅(qū)動同一個容器”速度翻倍或者亂跳。數(shù)據(jù)更新頻率高時可以考慮把“更新 DOM”和“滾動動畫”拆成兩個循環(huán)。DOM 更新用queueMicrotask或requestAnimationFrame批量處理避免在滾動過程中頻繁觸發(fā)布局。有條件的話做個“滾動狀態(tài)自檢”每隔幾分鐘檢查一下容器的 transform 值是否還在變化如果長時間沒變說明動畫可能停了自動重啟。大屏無人值守場景下這招救過我兩次。4. 實操手寫一個通用表格滾動組件Vue3 TypeScript4.1 組件需求與對外參數(shù)說了這么多理論直接上一個能用的組件。我用 Vue3 TypeScript 寫一個TableScrollList.vue它要滿足這些需求傳入數(shù)組數(shù)據(jù)渲染成表格表格整體向上平滑滾動。滾動速度可配置單位是“像素/秒”。鼠標(biāo)懸停時暫停移出后繼續(xù)。數(shù)據(jù)變化時自動重建內(nèi)容重置滾動位置不影響后續(xù)滾動。頁面標(biāo)簽頁切走再回來不出現(xiàn)速度突變。對外參數(shù)這樣設(shè)計type Props { data: Recordstring, any[]; // 表格數(shù)據(jù) rowHeight: number; // 單行高度 speed?: number; // 滾動速度 px/s默認(rèn) 30 hoverStop?: boolean; // 懸停暫停默認(rèn) true };4.2 核心實現(xiàn)代碼與關(guān)鍵點說明組件模板部分template div classtable-scroll :style{ height: containerHeight px } mouseenterhoverStop pause() mouseleavehoverStop resume() div classtable-scroll__body :style{ height: singleHeight * 2 px } div classtable-scroll__list reflistRef template v-for(item, index) in doubleData :keyindex div classtable-scroll__row :style{ height: rowHeight px } slot :itemitem :indexindex / /div /template /div /div /div /template比較關(guān)鍵的是doubleData的構(gòu)造把外部傳入的 data 復(fù)制一份拼接起來保證無縫循環(huán)。容器高度寫成singleHeight * 2而滾動時只滾singleHeight的距離滾完就復(fù)位。Script 部分script setup langts import { ref, computed, onMounted, onBeforeUnmount, nextTick } from vue; const props withDefaults(definePropsProps(), { speed: 30, hoverStop: true, }); const listRef refHTMLElement | null(null); const offset ref(0); const singleHeight computed(() props.data.length * props.rowHeight); const containerHeight computed(() props.rowHeight * Math.min(props.data.length, 8)); const doubleData computed(() [...props.data, ...props.data]); let rafId 0; let lastTime 0; let isPaused false; let isHidden false; function step(time: number) { if (isPaused || isHidden) { lastTime 0; rafId requestAnimationFrame(step); return; } if (!lastTime) lastTime time; const delta Math.min((time - lastTime) / 1000, 0.1); lastTime time; offset.value props.speed * delta; if (offset.value singleHeight.value) { offset.value 0; } if (listRef.value) { listRef.value.style.transform translate3d(0, ${-offset.value}px, 0); } rafId requestAnimationFrame(step); } function pause() { isPaused true; lastTime 0; } function resume() { isPaused false; lastTime 0; } function onVisibilityChange() { isHidden document.hidden; lastTime 0; } onMounted(async () { await nextTick(); lastTime 0; rafId requestAnimationFrame(step); document.addEventListener(visibilitychange, onVisibilityChange); }); onBeforeUnmount(() { cancelAnimationFrame(rafId); document.removeEventListener(visibilitychange, onVisibilityChange); }); /script這段代碼里我覺得需要額外解釋幾個點。containerHeight我限制為最多 8 行的高度。大屏上的表格區(qū)域是有限的如果數(shù)據(jù)有 100 行不能把外層容器撐到 3600px而是固定給 8 行左右的視口高度剩下的靠滾動展示。這個“8”可以根據(jù)設(shè)計稿改也可以做成參數(shù)。singleHeight用 computed 計算只要 props.data 變化它會自動變化滾動循環(huán)的邊界也隨之變化不需要手動去讀 DOM 的高度性能更好也不會出現(xiàn)“數(shù)據(jù)更新后高度還停留在舊值”的問題。visibilitychange里只改isHidden和重置lastTime沒有去 cancel 動畫幀。這樣從后臺切回來的時候動畫自動恢復(fù)不會因為中斷導(dǎo)致速度異常。還有一個很多人容易忽略的細(xì)節(jié)pause()和resume()里都要把lastTime重置為 0。如果不重置從暫停狀態(tài)恢復(fù)時上一次執(zhí)行和當(dāng)前執(zhí)行的時間差會很大delta會被 Math.min 截斷到 0.1 秒雖然不會飛出去但速度會在第一幀慢半拍。4.3 接入表格數(shù)據(jù)時的調(diào)用方式使用方式非常簡單在父組件里這樣寫TableScrollList :datatableData :row-height40 :speed35 template #default{ item } div classtable-cell{{ item.name }}/div div classtable-cell{{ item.time }}/div div classtable-cell{{ item.value }}/div /template /TableScrollListslot 的作用是讓外層自己決定每一行顯示什么內(nèi)容。大屏項目里同一個滾動容器可能一會兒放告警列表一會兒放設(shè)備列表把“滾動邏輯”和“行內(nèi)容”解耦之后這個組件放到哪個大屏頁面里都能復(fù)用。5. 常見問題與排查技巧實錄5.1 滾動到邊界后出現(xiàn)閃跳這是無縫滾動最常見的坑具體表現(xiàn)是表格滾到底部之后瞬間從頂部重新開始肉眼能明顯看到“跳”了一下。原因通常有兩個第一沒有使用雙份數(shù)據(jù)而是單份數(shù)據(jù)滾到底后直接 offset 0。這是無解的單份數(shù)據(jù)到底后不閃跳才是怪事所以無縫循環(huán)的前提一定是雙份或更多份數(shù)據(jù)。第二雙份數(shù)據(jù)之間被插入了邊距或分隔線導(dǎo)致位移 50% 不再等于“一份內(nèi)容的高度”復(fù)位的時候位置差了幾像素這幾像素的差距肉眼完全能看出來。排查方法也很簡單在瀏覽器里給滾動容器加上臨時邊框等滾動到復(fù)位那一幀截個圖對比第一幀和第二份開頭的幾行差多少像素。這個細(xì)節(jié)在開發(fā)環(huán)境的普通顯示器上很難察覺往往要到大屏上才看得見所以寫代碼的時候就從源頭避免兩份內(nèi)容之間不要加任何額外間距高度計算全部用程序完成不要手動拼接模板。5.2 標(biāo)簽頁切走再回來滾動速度異常現(xiàn)象是大屏開著切到別的應(yīng)用再切回來表格猛地滾了一大段或者直接停住不動。這個問題的根源我在前面已經(jīng)提到過requestAnimationFrame 在頁面不可見時會被瀏覽器暫停恢復(fù)時傳入的time參數(shù)會跳躍到當(dāng)前時間如果你直接用time - lastTime計算增量就會得到一個非常大的 delta導(dǎo)致滾動距離暴增。即使沒有暴增定時器在后臺被節(jié)流回來之后也會產(chǎn)生一堆堆積的回調(diào)連續(xù)執(zhí)行現(xiàn)象就是“抽風(fēng)式”滾動。解決辦法就是在step里限制 delta 最大值。我在組件里寫的是Math.min((time - lastTime) / 1000, 0.1)這樣無論頁面離開多久每一幀最多只會按 0.1 秒來計算位移。另外在visibilitychange事件里監(jiān)聽頁面可見性狀態(tài)切回來時重置lastTime從視覺上完全消除速度突變。5.3 數(shù)據(jù)更新后滾動位置錯亂實時數(shù)據(jù)大屏經(jīng)常會遇到這種問題新數(shù)據(jù)推過來了表格內(nèi)容變了但滾動位置還在一個奇怪的高度要么滾到空白區(qū)域要么和新的內(nèi)容錯位。原因在于滾動容器的高度是跟著數(shù)據(jù)走的數(shù)據(jù)更新前后單份內(nèi)容高度變了但滾動 offset 還停留在舊值上。解決思路是監(jiān)聽數(shù)據(jù)變化在數(shù)據(jù)更新后把 offset 重置為 0。如果希望更新之后不是從頭滾起而是“保持當(dāng)前滾動比例”可以記錄舊高度和新高度把 offset 按比例換算一下const lastSingleHeight singleHeight.value; // 數(shù)據(jù)更新后 offset.value offset.value * (newSingleHeight / lastSingleHeight);這里有個經(jīng)驗如果是“追加數(shù)據(jù)”而不是“替換全部數(shù)據(jù)”最好在更新時記錄一下當(dāng)前 offset 對應(yīng)的數(shù)據(jù)索引再換算新的 offset這樣用戶看到的是“列表連續(xù)地長出了新行”而不是從頭再來或者跳過一段。大部分客戶對這個細(xì)節(jié)很敏感。5.4 大屏上文字模糊或被裁切表格滾起來了但客戶說“字看不清”這是大屏項目另一個經(jīng)典問題。一般有兩個原因。第一個原因是適配沒有做等比縮放頁面被拉伸導(dǎo)致文字變形。這個問題只能從適配層面解決跟滾動組件關(guān)系不大但會讓所有文字整體發(fā)虛。第二個原因是表格行高設(shè)置太小文字在大屏的低分辨率顯示下邊緣像素不夠看起來就不銳利。大屏表格里行高一般不要低于 36px字號不要低于 14px設(shè)計稿 1920 下的值縮放之后才勉強清晰。還有被裁切的問題尤其是拼接屏屏幕邊緣顯示區(qū)域不一定是完整的物理像素大屏往往要預(yù)留安全區(qū)。滾動表格如果正好放在屏幕邊緣內(nèi)容很可能被削掉一塊。這種問題在開發(fā)環(huán)境是看不出來的一定要拿到實際的大屏環(huán)境去驗證布局范圍。5.5 問題排查速查表我在最后給一個速查表方便你以后定位問題現(xiàn)象可能原因解決方案滾動到底后閃跳沒有用雙份/多份數(shù)據(jù)或者兩份之間有額外間距數(shù)據(jù)渲染兩份位移距離按單份高度計算不加邊距后臺切回后滾動猛跳rAF 時間戳差過大限制 delta 最大值監(jiān)聽 visibilitychange 重置時間第二次進入大屏動畫錯亂定時器/動畫幀沒有清理干凈組件卸載時 cancelAnimationFrame清事件監(jiān)聽數(shù)據(jù)更新后空白或錯位單份高度變了offset 沒重置數(shù)據(jù)更新后重置 offset 或按比例換算表格滾動時整個頁面卡頓大量重繪回流或陰影濾鏡過多用 transform 位移列表項少用陰影減少 DOM 數(shù)量懸停暫停后恢復(fù)速度異常暫停期間時間戳累積pause 和 resume 里都要把 lastTime 置 0字體發(fā)虛或行被裁切適配拉伸行高太小邊緣安全區(qū)不足等比縮放行高不小于 36px預(yù)留安全區(qū)6. 寫在最后做了這么多年可視化大屏項目我的體會是表格滾動不是一個“能滾起來就行”的功能它背后牽扯到適配、性能、長時穩(wěn)定這些東西。如果你只是臨時寫個小屏展示用 CSS 動畫就夠了但如果是交付給客戶、要 7×24 小時跑的大屏項目我建議直接上 requestAnimationFrame transform 這個組合代碼不復(fù)雜可控性最好也最容易排查問題。最后分享一個小技巧大屏項目驗收之前把頁面開著放一晚上第二天早上再去看一眼表格滾動是不是還正常。這個習(xí)慣幫我發(fā)現(xiàn)過好幾回隱藏的問題比如某些機器在長時間運行后瀏覽器會對動畫做降頻或者某些數(shù)據(jù)更新邏輯在半夜某個時刻計算出異常值。這些問題白天開發(fā)的時候根本不會出現(xiàn)放一晚上就能暴露。