[Lv3] 大量資料優化策略:方案選擇與實作
當畫面需要顯示上萬筆資料時,如何在效能、使用者體驗和開發成本間取得平衡?
📋 面試情境題
Q: 當畫面上有上萬筆資料時,該如何進行優化處理?
這是一個開放性問題,面試官期待聽到的不只是單一解決方案,而是:
- 需求評估:真的需要一次顯示這麼多資料嗎?
- 方案選擇:有哪些方案?各自的優缺點?
- 全面思考:前端 + 後端 + UX 的綜合考量
- 實際經驗:選擇的理由和實施效果
第一步:需求評估
在選擇技術方案前,先問自己這些問題:
核心問題
❓ 使用者真的需要看到所有資料嗎?
→ 大部分情況下,使用者只關心前 50-100 筆
→ 可以透過篩選、搜尋、排序來縮小範圍
❓ 資料是否需要實時更新?
→ WebSocket 即時更新 vs 定時輪詢 vs 僅初次載入
❓ 使用者的操作模式是什麼?
→ 瀏覽為主 → 虛擬滾動
→ 查找特定資料 → 搜尋 + 分頁
→ 逐筆檢視 → 無限滾動
❓ 資料結構是否固定?
→ 高度固定 → 虛擬滾動容易實作
→ 高度不固定 → 需要動態高度計算
❓ 是否需要全部選取、列印、匯出?
→ 需要 → 虛擬滾動會有限制
→ 不需要 → 虛擬滾動最佳選擇
實際案例分析
// 案例 1:歷史交易記錄(10,000+ 筆)
使用者行為:查看最近的交易、偶爾搜尋特定日期
最佳方案:後端分頁 + 搜尋
// 案例 2:即時遊戲列表(3,000+ 款)
使用者行為:瀏覽、分類篩選、流暢滾動
最佳方案:虛擬滾動 + 前端篩選
// 案例 3:社交動態(無限增長)
使用者行為:持續往下滑、不需要跳頁
最佳方案:無限滾動 + 分批載入
// 案例 4:數據報表(複雜表格)
使用者行為:查看、排序、匯出
最佳方案:後端分頁 + 匯出 API
💡 優化方案總覽
方案對比表
| 方案 | 適用場景 | 優點 | 缺點 | 實作難度 | 效能 |
|---|---|---|---|---|---|
| 後端分頁 | 大部分場景 | 簡單可靠、SEO 友好 | 需要翻頁、體驗中斷 | 1/5 簡單 | 3/5 中等 |
| 虛擬滾動 | 大量固定高度資料 | 極致效能、流暢滾動 | 實作複雜、無法原生搜尋 | 4/5 複雜 | 5/5 極佳 |
| 無限滾動 | 社交媒體、新聞流 | 連續體驗、實作簡單 | 記憶體累積、無法跳頁 | 2/5 簡單 | 3/5 中等 |
| 數據分批 | 初次載入優化 | 漸進式載入、配合骨架屏 | 需要後端配合 | 2/5 簡單 | 3/5 中 等 |
| Web Worker | 大量計算、排序、過濾 | 不阻塞主線程 | 通訊開銷、除錯困難 | 3/5 中等 | 4/5 良好 |
| 混合方案 | 複雜需求 | 結合多種方案優點 | 複雜度高 | 4/5 複雜 | 4/5 良好 |
📚 方案詳解
1. 後端分頁(Pagination)★ 首選方案
推薦指數:5/5(強烈推薦)
最常見、最可靠的方案,適合 80% 的場景
實作方式
// 前端請求
async function fetchData(page = 1, pageSize = 20) {
const response = await fetch(`/api/data?page=${page}&pageSize=${pageSize}`);
return response.json();
}
// 後端 API(以 Node.js + MongoDB 為例)
app.get('/api/data', async (req, res) => {
const page = parseInt(req.query.page) || 1;
const pageSize = parseInt(req.query.pageSize) || 20;
const skip = (page - 1) * pageSize;
const data = await Collection.find().skip(skip).limit(pageSize).lean(); // 只返回純物 件,不含 Mongoose 方法
const total = await Collection.countDocuments();
res.json({
data,
pagination: {
page,
pageSize,
total,
totalPages: Math.ceil(total / pageSize),
},
});
});
優化技巧
// 1. 游標分頁(Cursor-based Pagination)
// 適合實時更新的資料,避免重複或遺漏
const data = await Collection.find({ _id: { $gt: cursor } })
.limit(20)
.sort({ _id: 1 });
// 2. 快取熱門頁面
const cacheKey = `data:page:${page}`;
const cached = await redis.get(cacheKey);
if (cached) return JSON.parse(cached);
// 3. 只返回必要欄位
const data = await Collection.find()
.select('id name price status') // 只選取需要的欄位
.skip(skip)
.limit(pageSize);
適用場景
✅ 適合
├─ 管理後台(訂單列表、使用者列表)
├─ 資料查詢系統(歷史記錄)
├─ 公開網站(部落格、新聞)
└─ 需要 SEO 的頁面
❌ 不適合
├─ 需要流暢滾動體驗