📄 虛擬滾動
當頁面需要渲染 1000+ 筆資料時,虛擬滾動可以將 DOM 節點從 1000+ 降至 20-30 個,記憶體使用降低 80%。
📋 面試情境題
Q: 畫面上的 table 有不止一個,且如果各自有超過一百筆資料,同時又有頻繁更新 DOM 的事件,會用什麼方法去優化這頁的效能?
問題分析(Situation)
實際專案場景
在平台專案中,我們可能有頁面需要處理大量資料:
📊 某頁面歷史紀錄頁面
├─ 儲值記錄表格:1000+ 筆
├─ 提款記錄表格:800+ 筆
├─ 投注記錄表格:5000+ 筆
└─ 每筆記錄 8-10 個欄位(時間、金額、狀態等)
❌ 未優化的問題
├─ DOM 節點數:1000 筆 × 10 欄位 = 10,000+ 個節點
├─ 記憶體佔用:約 150-200 MB
├─ 首次渲染時間:3-5 秒(白屏)
├─ 滾動卡頓:FPS < 20
└─ WebSocket 更新時:整個表格重新渲染(非常卡)
問題嚴重性
// ❌ 傳統做法
<tr v-for="record in allRecords">  // 1000+ 筆全部渲染
  <td>{{ record.time }}</td>
  <td>{{ record.amount }}</td>
  // ... 8-10 個欄位
</tr>
// 結果:
// - 初始渲染:10,000+ 個 DOM 節點
// - 使用者實際可見:20-30 筆
// - 浪費:99% 的節點使用者根本看不到
解決方案(Action)
Virtual Scrolling(虛擬滾動)
先考慮虛擬滾動的優化問題,從這個角度出發,大概有兩個方向,一個是選擇官方背書的三方套件 vue-virtual-scroller,根據參數和需求,來決定可視範圍的 row。
// 只渲染可見區域的 row,例如:
// - 100 筆資料,只渲染可見的 20 筆
// - 大幅減少 DOM 節點數量
另一種則是選擇自己手刻,但考慮到實際開發的成本,以及涵括的情境,我應該會比較傾向採用官方背書的三方套件。
資料更新頻率控制
✅ 解法一:requestAnimationFrame(RAF) 概念:瀏覽器每秒最多重繪 60 次(60 FPS),更新再快人眼也看不到,所以我們配合螢幕刷新率更新
// ❌ 原本:收到資料就立刻更新(每秒可能 100 次)
socket.on('price', (newPrice) => {
  btcPrice.value = newPrice;
});
// ✅ 改良:收集資料,配合螢幕刷新率一次更新(每秒最多 60 次)
let latestPrice = null;
let isScheduled = false;
socket.on('price', (newPrice) => {
  latestPrice = newPrice; // 暫存最新價格
  if (!isScheduled) {
    isScheduled = true;
    requestAnimationFrame(() => {
      btcPrice.value = latestPrice; // 在瀏覽器準備重繪時才更新
      isScheduled = false;
    });
  }
});
✅ 解法二:throttle(節流) 概念:強制限制更新頻率,例如「每 100ms 最多更新 1 次」
// lodash 的 throttle(如果專案有用)
import { throttle } from 'lodash-es';
const updatePrice = throttle((newPrice) => {
  btcPrice.value = newPrice;
}, 100); // 每 100ms 最多執行 1 次
socket.on('price', updatePrice);
Vue3 特定優化
有一些 Vue3 的語法糖會提供優化效能,例如 v-memo,但我個人很少使用 這個情境。
// 1. v-memo - 記憶化不常變動的列
<tr v-for="row in data"
  :key="row.id"
  v-memo="[row.price, row.volume]">  // 只在這些欄位變化時重新渲染
</tr>
// 2. 凍結靜態資料,避免響應式開銷
const staticData = Object.freeze(largeDataArray)
// 3. shallowRef 處理大陣列
const tableData = shallowRef([...])  // 只追蹤陣列本身,不追蹤內部物件
// 4. 使用 key 優化 diff 算法(讓唯一值的 id 來追蹤每個 item,讓 DOM 的更新可以局限在有變化的節點,節省效能)
<tr v-for="row in data" :key="row.id">  // 穩定的 key**
RAF:配合螢幕刷新(約 16ms),適合動畫、滾動 throttle:自訂間隔(如 100ms),適合搜尋、resize