Core Web Vitals 效能優化專家
建立摘要
JavaScript 效能 LCP CLS INP

JavaScript 效能優化實戰:從診斷 Long Task 到主執行緒調度全指南

從實驗室到線上環境,精準診斷與解決 JavaScript Long Task。本指南提供 DevTools 實戰步驟、代碼拆分、任務非同步化等優化手法,附前後對比數據,助你搶回主執行緒,大幅改善 INP。

陳柏翰

網站效能工程師

JavaScript 效能優化實戰:從診斷 Long Task 到主執行緒調度全指南
這篇內容要解決什麼?:JavaScript 效能優化實戰:從診斷 Long Task 到主執行緒調度全指南
JavaScript 效能優化實戰:從診斷 Long Task 到主執行緒調度全指南

開場:一個 Long Task 如何毀掉你的 INP

想像一個中型 SPA,其主執行緒上有一段未經優化的圖片處理腳本,執行時間長達 350 毫秒。這段時間內,使用者點擊了一個按鈕,但瀏覽器無暇回應。等到腳本執行完畢,介面才發生變化,這段「互動延遲」就是 INP (Interaction to Next Paint) 分數的核心。一個實驗顯示,透過拆分這個長任務,INP 從 350ms 降至 45ms,使用者感知從「明顯卡頓」變為「流暢即時」。這不是個案,主執行緒的 Long Task 是導致 INP 過高的最直接原因。本文將遵循嚴謹的實驗流程,從精準診斷出發,逐步拆解解決 Long Task、搶回主執行緒的完整優化路線圖。

Part 1: 診斷篇 — 如何精準找到你的效能瓶頸

優化的第一步永遠是量測,但量測工具與數據源的選擇決定了你是在追逐虛假指標還是解決真實問題。Chrome DevTools Performance 面板是你進入主執行緒內部最直接的窗口,而 Google Search Console 的 Core Web Vitals 報告則提供了真實用戶的最終審判。

開發環境的硬體效能通常遠超一般用戶裝置,這導致實驗室 (Lab) 與現場 (Field) 數據存在巨大鴻溝。例如,一個在測試機上 Lighthouse 分數為 95 的頁面,其 CrUX 報告的 INP 數據可能依然處於「需要改善」的區間。解法並非追求更高的實驗室分數,而是去分析 Field 數據。若 Field 數據顯示 INP 未達標,你便需要使用 DevTools 來定位問題。錄製一段互動後,焦點應放在 Performance 面板的三個地方:灰色 Task 上帶有紅色三角形的標記,這直觀標示了超過 50ms 的 Long Task;Summary 面板中的 Total Blocking Time (TBT),它量化了所有長任務超出 50ms 部分的總和;以及必須分開觀察的 Main 軌道(JavaScript 執行)與 Rendering 軌道(樣式、佈局),以避免誤判問題根源。

要讓診斷從偶發的「手動操作」轉變為持續的「系統監控」,可以利用 Long Tasks API。透過 PerformanceObserver 監聽 longtask 事件,並結合現有的真實用戶監控 (RUM) 工具,你便能在線上環境中持續收集長任務的發生頻率與時長分布。這段程式碼在初始化時加入 buffered: true,確保連頁面載入初期發生的長任務也能被捕獲,為你提供完整的初始載入視圖。

Part 2: 優化實戰篇 — 拆解 Long Task,搶回主執行緒

如何系統性地診斷並解決一個 JavaScript Long Task 問題?:遵循「識別 → 分析 → 優化 → 驗證」的四步驟循環,能有效解決主執行緒阻塞問題。
遵循「識別 → 分析 → 優化 → 驗證」的四步驟循環,能有效解決主執行緒阻塞問題。
面對不同類型的主執行緒瓶頸,應該選擇哪種優化策略?:優化策略的選擇取決於瓶頸的根本原因(如代碼體積、執行時長、佈局計算等)。
優化策略的選擇取決於瓶頸的根本原因(如代碼體積、執行時長、佈局計算等)。
「任務拆分」的具體實作方式與原理是什麼?:利用 `requestIdleCallback` 或 `setTimeout` 將長任務拆解為多個短任務,能讓出主執行緒,提升響應性。
利用 `requestIdleCallback` 或 `setTimeout` 將長任務拆解為多個短任務,能讓出主執行緒,提升響應性。

找到長任務後,核心目標是讓瀏覽器能「喘氣」——在每個小任務的間隙處理使用者輸入與渲染。最直接的方法是任務拆分,但手段的選擇需考量效果與相容性。以下提供三種按優先級排列的實戰策略,附上可複製的代碼範例與實驗對比說明。

策略一:使用 setTimeout(fn, 0) 或 queueMicrotask 進行最簡單的拆分。這將後續工作推到下一個事件循環,讓瀏覽器有機會回應。實驗設定:一個計算密集的迴圈。改動前:單一長任務執行,INP 高。改動後:將迴圈拆分為每 100 次迭代一批,每批之間用 setTimeout(fn, 0) 跳出。結果:主執行緒的最大連續忙碌時間從數百毫秒降至約 50ms,INP 得到顯著改善。其代價是每次 yield 都有約 4ms 的最小延遲,適合中等粒度的拆分。

策略二:對於非關鍵的低優先級任務(如預載數據、分析追蹤),可以使用 scheduler.postTask() API(Chrome/Edge 支援良好)指定 priority 為 'background',或使用 requestIdleCallback(注意 Safari 支援較晚)。實驗設定:頁面載入後需要預取一些用戶資料。改動前:在主執行緒直接 fetch,可能阻塞其他互動。改動後:使用 scheduler.postTask 將 fetch 任務設定為低優先級。結果:預取任務完全在瀏覽器空閒時執行,對首次互動響應零影響。

此項判斷應依實際預算、資料量與合作條件評估,不採用沒有可核對來源的固定門檻。

Part 3: 深度原理篇 — 從 V8 到 Worker 的進階探索

解決了眼前的長任務,若想進一步壓榨執行效率,便需要理解 JavaScript 引擎的底層運作。V8 引擎的 JIT (Just-In-Time) 編譯器會動態將熱點代碼編譯為優化機器碼,其效率高度依賴代碼的「可預測性」。核心概念是 Hidden Classes (隱藏類別),它為物件的屬性結構建立內部快取。實驗顯示:一個函數若每次呼叫時,傳入物件的屬性順序不一致,會導致 V8 無法複用優化的 Hidden Class 路徑,執行速度可能相差數倍。因此,在高頻呼叫的核心函數中,應保持物件結構穩定,避免動態新增刪除屬性。

當單一計算任務過於龐大,無法通過拆分解決時,將其移出主執行緒是終極手段,這時便需區分 Web Worker 與 Service Worker 的職責。它們都運行在獨立執行緒,且都不能存取 DOM。簡言之,Web Worker 是「計算外包工」,負責 CPU 密集型任務(如影像處理、複雜算法),透過 postMessage 與主執行緒通訊。需注意傳輸成本,大型 ArrayBuffer 可使用 Transferable Objects 進行零複製轉移。Service Worker 則是「網路攔截層」,負責控制快取策略、實現離線功能與背景同步,它攔截的是 fetch 請求。選擇錯誤會導致架構混亂:試圖在 Service Worker 中進行圖片編解碼,或將快取策略寫在 Web Worker 中,都是不合適的。

結論:建立你的 JavaScript 效能監控與優化流程

JavaScript 效能優化是一套從診斷、優化到監控的循環流程。起點是基於 Field 數據(如 CrUX 的 INP)確認真實問題,接著使用 DevTools Performance 面板定位具體的 Long Task 成因。根據成因選擇對應的優化策略:體積問題用代碼分割 (Code Splitting) 與 Tree Shaking,執行問題用任務拆分與非同步調度,佈局問題用批量讀寫。最後,透過 Long Tasks API 或 RUM 工具將監控線上化,完成閉環。記住,優化優先級應基於 ROI:改善 Long Task 對 INP 的提升通常最為直接且有感。

常見問題

什麼是 Long Task?它怎麼影響 INP?

此項判斷應依實際預算、資料量與合作條件評估,不採用沒有可核對來源的固定門檻。

Lighthouse 分數高,但用戶還是覺得慢,是怎麼回事?

這通常是由於實驗室數據 (Lab) 與現場數據 (Field) 的差異所致。Lighthouse 在受控的模擬環境(如 4 倍 CPU 降速)中運行,沒有第三方腳本干擾,也無法反映真實用戶的裝置效能與網路條件。真實用戶可能在低階手機上運行,同時有廣告腳本、擴充功能在消耗資源。解決方案是查看 Google Search Console 中基於 CrUX 數據的 Core Web Vitals 報告,它反映了真實用戶的體驗。

Web Worker 和 Service Worker 有什麼差別?

兩者核心職責不同。Web Worker 用於在後台執行 CPU 密集型計算任務(如影像處理、大型資料解析),避免阻塞主執行緒的互動與渲染。Service Worker 用於攔截與處理網路請求,實現快取策略、離線訪問和背景同步,是提升網路效能與可靠性的關鍵。它們都不能直接操作 DOM。

如何快速判斷我的頁面是否有記憶體洩漏?

一個快速的初步診斷方法是打開 Chrome DevTools 的 Performance Monitor 面板,觀察「JS heap size」指標。在使用者進行一系列常規操作(如開啟/關閉面板、切換標籤頁)後,如果記憶體占用持續攀升且在操作結束後沒有明顯回落,就很可能存在記憶體洩漏。更精確的診斷需要使用 Memory 面板中的 Heap Snapshot 功能,通過拍攝操作前後的快照並比較,找出持續增長的物件類型及其引用鏈。

Tree Shaking 設定正確了,為什麼 bundle 還是很大?

Tree Shaking 失效的常見原因包括:依賴套件未提供有效的 ESM 格式(如仍為 CommonJS)、專案中使用了 Barrel File(即 index.js 大量 re-export 子模組)導致打包工具難以分析、或 package.json 中缺少 "sideEffects": false 聲明。使用 Webpack Bundle Analyzer 等工具可視化分析 bundle 組成,是定位此類問題的關鍵步驟。

陳柏翰

網站效能工程師

專注前端效能優化超過 8 年,曾協助電商、媒體和 SaaS 平台改善 Core Web Vitals 指標。相信效能優化應該基於數據而非直覺。

50+ 效能實驗
8 年 前端經驗
200+ 優化案例

想看更多?

探索所有效能實驗

查看完整的實驗報告列表,找到你需要的效能優化方案。

所有實驗報告 免費效能診斷