rangeCache 快取機制
概述
在開發 Angular 圖表組件時,遇到了每次滑鼠 hover 都會觸發複雜查找邏輯的效能問題。透過實作 rangeCache
快取機制,將 O(n) 的查找時間降低到 O(1),大幅提升使用者體驗。
問題背景
原始問題
Chart.js 的 tooltip callbacks 在每次滑鼠移動時都會執行,原本的實作方式:
// ❌ 效能不佳的寫法
footer: (tooltipItems) => this.createTooltipFooter(tooltipItems)
private createTooltipFooter(tooltipItems: any[]): string {
const item = tooltipItems[0];
const datasetLabel = item.dataset.label || '';
const labelWithoutPercent = item.label.slice(0, -1);
// 每次 hover 都要執行這些 O(n) 操作
return this.getRange(datasetLabel, labelWithoutPercent);
}
private getRange(columnName: string, label: string): string {
const data = this.barChartData().data;
// O(n) 查找標籤索引
const index = data.labels.findIndex((item) => item.toString() === label);
// O(n) 查找資料集
const dataset = data.datasets.find((dataset) => dataset.label === columnName);
return dataset.range[index];
}
效能問題
頻繁的陣列查找:每次 hover 都要執行
findIndex
和find
重複的資料存取:每次都要呼叫
this.barChartData()
時間複雜度高:每次查找都是 O(n),當資料量大時影響明顯
rangeCache 解決方案
核心概念
將原本需要「動態查找」的邏輯改為「預先建立快取」,把所有可能的查找結果預先計算並存在 Map 中。
資料結構設計
// 快取結構:Map<string, string>
// key: "資料集名稱-標籤值"
// value: "對應的範圍資訊"
// 範例:
Map {
"feature_12_min-0~10" => "3040 ~ 3040",
"feature_12_min-10~20" => "3040 ~ 3040",
"label-0~10" => "2 ~ 14.1",
"label-10~20" => "14.1 ~ 26.2"
}
實作程式碼
// 使用 computed signal 確保資料變更時自動更新快取
private rangeCache = computed(() => {
const data = this.barChartData()?.data;
if (!data?.datasets || !data?.labels) {
return new Map<string, string>();
}
const cache = new Map<string, string>();
// 預先建立所有可能的 key-value 對應
data.datasets.forEach(dataset => {
if (dataset.range && Array.isArray(dataset.range)) {
data.labels.forEach((label, index) => {
// 建立快取 key: "資料集名稱-標籤值"
const key = `${dataset.label}-${label}`;
cache.set(key, dataset.range[index] || '');
});
}
});
return cache;
});
使用方式
// ✅ 優化後的 tooltip callback
tooltip: {
callbacks: {
title: () => '',
footer: (tooltipItems) => {
if (!tooltipItems.length) return '';
const item = tooltipItems[0];
const datasetLabel = item.dataset.label || '';
const labelWithoutPercent = item.label.slice(0, -1);
// O(1) 快取查找,取代原本的 O(n) 查找
const cacheKey = `${datasetLabel}-${labelWithoutPercent}`;
const range = this.rangeCache().get(cacheKey) || '';
return `${item.label}\n${range}`;
},
},
},
實際案例分析
原始資料結構
{
"data": {
"labels": ["0~10", "10~20", "20~30", "30~40", "40~50"],
"datasets": [
{
"label": "feature_12_min",
"data": [0, 0, 0, 0, 3],
"range": ["3040 ~ 3040", "3040 ~ 3040", "3040 ~ 3040", "3040 ~ 3040", "3040 ~ 3040"]
},
{
"label": "label",
"data": [2, 0, 0, 0, 1],
"range": ["2 ~ 14.1", "14.1 ~ 26.2", "26.2 ~ 38.3", "38.3 ~ 50.4", "50.4 ~ 62.5"]
}
]
}
}
快取建立過程
// Step 1: 處理第一個 dataset
dataset = {
label: "feature_12_min",
range: ["3040 ~ 3040", "3040 ~ 3040", "3040 ~ 3040", "3040 ~ 3040", "3040 ~ 3040"]
}
// 遍歷每個 label 建立快取項目
cache.set("feature_12_min-0~10", "3040 ~ 3040")
cache.set("feature_12_min-10~20", "3040 ~ 3040")
cache.set("feature_12_min-20~30", "3040 ~ 3040")
cache.set("feature_12_min-30~40", "3040 ~ 3040")
cache.set("feature_12_min-40~50", "3040 ~ 3040")
// Step 2: 處理第二個 dataset
dataset = {
label: "label",
range: ["2 ~ 14.1", "14.1 ~ 26.2", "26.2 ~ 38.3", "38.3 ~ 50.4", "50.4 ~ 62.5"]
}
cache.set("label-0~10", "2 ~ 14.1")
cache.set("label-10~20", "14.1 ~ 26.2")
cache.set("label-20~30", "26.2 ~ 38.3")
cache.set("label-30~40", "38.3 ~ 50.4")
cache.set("label-40~50", "50.4 ~ 62.5")
使用者 hover 時的查找過程
// 使用者 hover 到 "feature_12_min" 的 "10~20%" 區間
tooltipItems = [{
dataset: { label: "feature_12_min" },
label: "10~20%"
}]
// 建立查找 key
const datasetLabel = "feature_12_min"
const labelWithoutPercent = "10~20" // 移除 %
const cacheKey = "feature_12_min-10~20"
// O(1) 快取查找
const range = rangeCache.get("feature_12_min-10~20") // "3040 ~ 3040"
// 返回結果
return "10~20%\n3040 ~ 3040"
效能對比
時間複雜度分析
建立快取
無
O(m × n) 一次性
每次查找
O(n)
O(1)
總查找成本
O(k × n)
O(k)
其中:
m = datasets 數量
n = labels 數量
k = hover 次數
實際效能提升
以一個擁有 13 個 datasets 和 10 個 labels 的圖表為例:
舊版本每次 hover:
findIndex
遍歷 10 個 labels:10 次比較find
遍歷 13 個 datasets:最多 13 次比較總計:最多 23 次操作
新版本每次 hover:
Map.get()
雜湊查找:1 次操作效能提升:23 倍
記憶體使用分析
// 記憶體使用量估算
const entriesCount = datasetsCount × labelsCount // 13 × 10 = 130 個快取項目
const averageKeyLength = 20 // "feature_12_min-0~10" 約 20 字元
const averageValueLength = 15 // "3040 ~ 3040" 約 15 字元
const totalMemory = entriesCount × (averageKeyLength + averageValueLength) × 2 // 約 9KB
// 記憶體成本極低,效能提升顯著
適用場景
最適合的情況
頻繁查找:使用者會經常 hover 圖表
查找邏輯複雜:需要多次陣列遍歷
資料相對穩定:不會頻繁變更
查找結果有限:可預先列舉所有可能性
不適合的情況
資料頻繁變更:會導致快取頻繁重建
記憶體敏感:快取項目數量極大的情況
查找邏輯簡單:原本就是 O(1) 的查找
最佳實踐
1. 使用 Computed Signal
// ✅ 推薦:自動響應資料變更
private rangeCache = computed(() => {
// 當 barChartData() 變更時自動重新計算
});
// ❌ 避免:手動管理快取更新
private rangeCache = new Map();
2. 錯誤處理
private rangeCache = computed(() => {
const data = this.barChartData()?.data;
// 邊界檢查
if (!data?.datasets || !data?.labels) {
return new Map<string, string>();
}
const cache = new Map<string, string>();
data.datasets.forEach(dataset => {
// 確保 range 屬性存在且為陣列
if (dataset.range && Array.isArray(dataset.range)) {
data.labels.forEach((label, index) => {
const key = `${dataset.label}-${label}`;
// 防止陣列越界
cache.set(key, dataset.range[index] || '');
});
}
});
return cache;
});
3. 快取 Key 設計原則
// ✅ 好的 key 設計
const key = `${dataset.label}-${label}`; // "feature_1-0~10"
// ❌ 避免的 key 設計
const key = `${dataset.label}${label}`; // 可能產生衝突
const key = `${index}-${dataset.label}`; // 依賴索引,不穩定
4. 效能監控
private rangeCache = computed(() => {
const startTime = performance.now();
// 建立快取邏輯
const cache = new Map<string, string>();
// ...
const endTime = performance.now();
console.log(`Cache built in ${endTime - startTime}ms with ${cache.size} entries`);
return cache;
});
擴展應用
多層快取
當查找邏輯更複雜時,可以建立多層快取:
// 範圍資訊快取
private rangeCache = computed(() => { /* ... */ });
// 統計資訊快取
private statsCache = computed(() => { /* ... */ });
// 顏色快取
private colorCache = new Map<number, string[]>();
泛型快取工具
/**
* 通用快取建立器
*/
private createLookupCache<T>(
data: any[],
keyBuilder: (item: any, index: number) => string,
valueBuilder: (item: any, index: number) => T
): Map<string, T> {
const cache = new Map<string, T>();
data.forEach((item, index) => {
const key = keyBuilder(item, index);
const value = valueBuilder(item, index);
cache.set(key, value);
});
return cache;
}
小結
rangeCache 是一個典型的「以空間換時間」的最佳化策略。透過預先建立查找表,將複雜的 O(n) 查找降低到 O(1),在使用者體驗和系統效能上都有顯著提升。
關鍵收穫:
善用 Angular 的 computed signal 實現自動快取更新
選擇合適的快取 key 設計避免衝突
在記憶體使用和查找效能間找到平衡點
透過效能分析驗證最佳化效果
這種模式可以應用到任何需要頻繁查找的場景中,是前端效能最佳化的重要技巧之一。
Last updated