在現代網頁應用程式中,效能直接決定使用者體驗。一個快速、順暢的網站能提升停留時間、降低跳出率,甚至改善 SEO 排名與轉換率。反之,載入延遲、互動卡頓會讓使用者立即流失。
而在眾多效能瓶頸中,JavaScript 常是罪魁禍首。複雜邏輯、龐大框架與第三方函式庫,都可能拖慢執行速度。
Chrome DevTools 的性能分析工具 (Network & Performance)
🔎 Network 面板:掌握載入流程
- DNS / TCP / TTFB:觀察每個請求的細節,明確辨識是網路還是程式延遲。
- 瀑布圖 (Waterfall):直觀看出 JS 檔案下載、解析的時間差。
- Preview/Response:快速檢視載入內容,確認是否包含冗餘或未壓縮代碼。
- Cookies 分析:檢查是否攜帶過多 Cookie,導致額外傳輸負擔。
👉 行動:利用 Network 面板比對「下載」與「執行」耗時,篩選可能的瓶頸檔案。
🔥 Performance 面板:深入 JS 執行
- 火焰圖 (Flame Chart):直觀呈現 JS 函式堆疊與耗時分布。
- Bottom-Up / Call Tree:反向追蹤最耗時的函數來源。
- Event Log:精準列出事件觸發與回應時間。
- Memory:觀察記憶體使用,避免記憶體洩漏造成效能下降。
👉 行動:錄製一段操作流程,利用火焰圖鎖定「耗時最長的函式」並標記優化目標。
User Timing API:更精準的 JavaScript 時間測量
⚙️ User Timing API 是什麼?
它讓開發者在程式碼中自行「插旗」,標記開始與結束點,並量測兩點之間的耗時。這比單純依賴 DevTools 更具彈性。
📌 核心方法
// 標記時間點
performance.mark('start_task');
// 任務執行
doSomething();
// 計算耗時
performance.mark('end_task');
performance.measure('task_duration', 'start_task', 'end_task');
// 取得結果
const measures = performance.getEntriesByName('task_duration');
console.log(measures[0].duration + "ms");
🔍 在 Performance 面板檢視
錄製後,在「User Timings」區塊就能清楚看到自訂標記,與其他事件整合在同一條時間線。
👉 行動:在迴圈、第三方 API 或渲染邏輯前後插入 mark(),精準追蹤熱點。
簡易範例:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Lib Load Test</title>
<script>
performance.mark("lib_start");
</script>
<!-- 模擬載入大型第三方套件 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment.min.js"></script>
<script>
performance.mark("lib_end");
performance.measure("lib_duration", "lib_start", "lib_end");
console.log("Library load time:",
performance.getEntriesByName("lib_duration")[0].duration.toFixed(2), "ms");
</script>
</head>
<body>
<h1>第三方函式庫測試</h1>
</body>
</html>

迴圈耗時計算
<script>
const numbers = Array.from({length: 100000}, (_, i) => i);
performance.mark("loop_start");
const result = numbers.filter(n => n % 2 === 0).map(n => n * 2);
performance.mark("loop_end");
performance.measure("loop_duration", "loop_start", "loop_end");
console.log("Loop耗時:",
performance.getEntriesByName("loop_duration")[0].duration.toFixed(2), "ms");
</script>

進階範例:
第三方函式庫效能問題
情境:頁面載入就打包了大型第三方套件(例如圖表/日期處理),導致主執行緒長時間忙碌。
量測(在頁面互動點前後插旗)
<script>
performance.mark('lib_load_start');
</script>
<script src="/static/js/moment.js"></script> <!-- 假設這是較大套件 -->
<script>
performance.mark('lib_load_end');
performance.measure('lib_load_duration', 'lib_load_start', 'lib_load_end');
// 觀察量測結果
const m = performance.getEntriesByName('lib_load_duration')[0];
console.log('Third-party lib load:', m.duration.toFixed(2), 'ms');
</script>優化 1:改用輕量替代方案(如 dayjs 取代 moment)
<script>
performance.mark('dayjs_load_start');
</script>
<script src="https://cdn.jsdelivr.net/npm/dayjs@1/dayjs.min.js"></script>
<script>
performance.mark('dayjs_load_end');
performance.measure('dayjs_load_duration', 'dayjs_load_start', 'dayjs_load_end');
const m = performance.getEntriesByName('dayjs_load_duration')[0];
console.log('dayjs load:', m.duration.toFixed(2), 'ms');
</script>比較兩者的 duration,通常 dayjs 會顯著更小。
優化 2:Lazy Loading/Code Splitting(互動才載)
<button id="showChart">顯示圖表</button>
<div id="chart"></div>
<script>
performance.mark('idle_before_click');
document.getElementById('showChart').addEventListener('click', async () => {
performance.mark('chart_import_start');
// 僅在需要時才載入大型圖表套件
const { default: Chart } = await import('/static/charts/chart.min.js');
performance.mark('chart_import_end');
performance.measure('chart_import', 'chart_import_start', 'chart_import_end');
// 初始化圖表(範例)
const el = document.getElementById('chart');
new Chart(el, {/* options */});
console.log('Chart imported in ms:',
performance.getEntriesByName('chart_import')[0].duration.toFixed(2));
});
</script>案例二:複雜迴圈或演算法
情境:你需要篩選+轉換大量陣列資料;原本使用雙重掃描或不必要的中間陣列,導致耗時。
量測基線(filter + map)
const numbers = Array.from({ length: 500_000 }, (_, i) => i);
performance.mark('fm_start');
const squaredOddNumbers = numbers
.filter(n => n % 2 !== 0)
.map(n => n * n);
performance.mark('fm_end');
performance.measure('filter_map', 'fm_start', 'fm_end');
console.log('filter+map:',
performance.getEntriesByName('filter_map')[0].duration.toFixed(2), 'ms',
'(len=', squaredOddNumbers.length, ')');優化 A:用 flatMap(單趟、無中間陣列)
performance.mark('flatmap_start');
const squaredOddNumbers2 = numbers.flatMap(n => (n % 2 ? [n * n] : []));
performance.mark('flatmap_end');
performance.measure('flatmap', 'flatmap_start', 'flatmap_end');
console.log('flatMap:',
performance.getEntriesByName('flatmap')[0].duration.toFixed(2), 'ms',
'(len=', squaredOddNumbers2.length, ')');優化 B:資料結構換 Set(避免 O(n²))
**問題示例(慢):**在大陣列中反覆 indexOf/includes 查詢:
const data = Array.from({ length: 200_000 }, (_, i) => i);
const blacklist = Array.from({ length: 20_000 }, (_, i) => i * 7); // 可重複
// 慢:每次 includes 都是 O(n)
performance.mark('includes_start');
const filtered1 = data.filter(x => !blacklist.includes(x));
performance.mark('includes_end');
performance.measure('includes_filter', 'includes_start', 'includes_end');
console.log('includes filter:',
performance.getEntriesByName('includes_filter')[0].duration.toFixed(2), 'ms');**優化(快):**先轉為 Set,O(1) 查詢
const blackSet = new Set(blacklist);
performance.mark('set_start');
const filtered2 = data.filter(x => !blackSet.has(x));
performance.mark('set_end');
performance.measure('set_filter', 'set_start', 'set_end');
console.log('Set filter:',
performance.getEntriesByName('set_filter')[0].duration.toFixed(2), 'ms');你會看到
Set版本通常快非常多(尤其資料量越大越明顯)。
案例三:渲染阻塞(Rendering Blocking)
情境:同步載入與執行的腳本阻塞 First Paint,使用者看到白屏更久。
問題示例(阻塞 FP)
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>阻塞示例</title>
<!-- 這支會在解析 HTML 時阻塞 -->
<script src="/static/js/heavy-sync.js"></script>
</head>
<body>
<h1>頁面內容</h1>
</body>
</html>解法 1:defer(最佳化解析+在 DOM 準備後執行)
<script src="/static/js/heavy.js" defer></script>- 會平行下載、延後執行(在
DOMContentLoaded前),不阻塞 HTML 解析。 - 多支
defer會保持相對順序。
解法 2:async(適合無依賴的小腳本)
<script src="/static/js/ads-or-analytics.js" async></script>- 平行下載 + 載完就執行,不保證順序。
- 適合互相無依賴的腳本(如分析/廣告)。
解法 3:路由級 Code Splitting(只有需要時才載)
以 SPA/前端打包為例(Webpack / Vite 都支援 dynamic import):
// 只在進入報表路由時才載入大型圖表模組
router.addRoute({
path: '/report',
component: async () => {
const m = await import('./pages/ReportPage.js'); // 自動切塊
return m.default;
}
});
補強:把 CPU 密集工作移到 Web Worker(避免卡主執行緒)
main.js
performance.mark('worker_start');
const worker = new Worker('/static/worker.js');
worker.onmessage = (e) => {
performance.mark('worker_end');
performance.measure('worker_task', 'worker_start', 'worker_end');
console.log('Worker task ms:',
performance.getEntriesByName('worker_task')[0].duration.toFixed(2));
// 更新 UI,不會卡
document.querySelector('#result').textContent = e.data.result;
};
worker.postMessage({ type: 'HEAVY_TASK', payload: 1_000_000 });
worker.js
self.onmessage = (e) => {
if (e.data.type === 'HEAVY_TASK') {
// 模擬重計算
let sum = 0;
for (let i = 0; i < e.data.payload; i++) sum += Math.sqrt(i);
self.postMessage({ result: sum });
}
};
把量測結果丟到 Console / 上報
function dumpMeasures(label = '') {
const ms = performance.getEntriesByType('measure');
console.group(label || 'Performance measures');
ms.forEach(m => console.log(m.name, m.duration.toFixed(2), 'ms'));
console.groupEnd();
}
// 需要時清空
function resetPerf() {
performance.clearMarks();
performance.clearMeasures();
}
