網站效能取樣與執行時間偵測,前端與後端的 DevTools 與 User Timing 工具應用

在現代網頁應用程式中,效能直接決定使用者體驗。一個快速、順暢的網站能提升停留時間、降低跳出率,甚至改善 SEO 排名與轉換率。反之,載入延遲、互動卡頓會讓使用者立即流失。
而在眾多效能瓶頸中,JavaScript 常是罪魁禍首。複雜邏輯、龐大框架與第三方函式庫,都可能拖慢執行速度。

Chrome DevTools 的性能分析工具 (Network & Performance)

🔎 Network 面板:掌握載入流程

  1. DNS / TCP / TTFB:觀察每個請求的細節,明確辨識是網路還是程式延遲。
  2. 瀑布圖 (Waterfall):直觀看出 JS 檔案下載、解析的時間差。
  3. Preview/Response:快速檢視載入內容,確認是否包含冗餘或未壓縮代碼。
  4. Cookies 分析:檢查是否攜帶過多 Cookie,導致額外傳輸負擔。

👉 行動:利用 Network 面板比對「下載」與「執行」耗時,篩選可能的瓶頸檔案。

🔥 Performance 面板:深入 JS 執行

  1. 火焰圖 (Flame Chart):直觀呈現 JS 函式堆疊與耗時分布。
  2. Bottom-Up / Call Tree:反向追蹤最耗時的函數來源。
  3. Event Log:精準列出事件觸發與回應時間。
  4. 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();
}

VII. 延伸閱讀與資源

贊助商連結

發佈留言