Core Web Vitals 效能優化專家
建立摘要
關鍵渲染路徑 LCP CLS INP

Render Blocking 渲染阻塞怎麼消除:CSS 和 JS 完整修復指南

render blocking 渲染阻塞消除完整指南:inline critical CSS 決策樹、JS defer 與 async 差異、@import 鏈串聯阻塞診斷修復、第三方腳本處理,含 LCP 3.1s 降至 1.8s 實驗數據。

陳

陳柏翰

網站效能工程師

3 分鐘閱讀
Render Blocking 渲染阻塞怎麼消除:CSS 和 JS 完整修復指南

Render Blocking 渲染阻塞怎麼消除:CSS 和 JS 完整修復指南

上個月我在一個 Next.js 專案跑 Lighthouse,分數 54,Opportunities 裡面第一條就是「Eliminate render-blocking resources」,估計節省 1.8 秒。我把 2 個 render-blocking CSS 和 3 個同步 JS 處理掉之後,Lighthouse 跑到 81,LCP 從 3.1 秒降到 1.8 秒,FCP 從 2.4 秒壓到 1.1 秒。這篇是我做完之後整理的完整筆記,包含 CSS 和 JS 的診斷到修復流程,以及幾個 SERP 上找不到的細節。

Render Blocking 渲染阻塞消除完整修復指南封面圖:CSS defer、JS async、@import 鏈診斷流程
從 Lighthouse 警告到 LCP 改善的完整 render blocking 消除工作流程

渲染阻塞是什麼,瀏覽器為什麼要阻塞首次繪製?

渲染阻塞資源是指放在 <head> 裡、讓瀏覽器在下載和解析完成之前無法輸出任何畫面的 CSS 或 JS 檔案。瀏覽器強制等待,是因為 CSS 決定每個 DOM 元素的樣式,如果先畫出沒有樣式的內容再套入樣式,就會出現 FOUC(Flash of Unstyled Content,無樣式內容閃爍)。這是瀏覽器刻意的設計,不是 bug。

背後的機制是這樣:瀏覽器在渲染之前需要建立 Render Tree,而 Render Tree 需要同時有 DOM Tree 和 CSSOM(CSS Object Model)。所有外部 CSS 在下載並解析完成前,CSSOM 無法完整,Render Tree 就無法建立,畫面就卡在空白。這就是為什麼就算你的 HTML 已經下載完了,瀏覽器還是顯示空白頁的原因。

對效能的影響是直接的:每一個 render-blocking 資源都在推遲 First Contentful Paint(FCP),而 FCP 又是 LCP 的前置條件。詳細的渲染阻塞機制與 關鍵渲染路徑的完整理論,可以看我之前寫的 CRP 完整指南。

哪些資源會阻塞渲染?完整分類清單

不是所有 CSS 和 JS 都會阻塞渲染,判斷標準是資源的位置和是否帶有特定屬性。以下是完整的阻塞與非阻塞資源分類清單。

資源類型 條件 阻塞行為
外部 CSS <link rel="stylesheet">,無 media 限制 完全阻塞渲染(render-blocking)
外部 CSS <link rel="stylesheet" media="print"> 不阻塞渲染(只在列印時載入)
外部 JS <script src="...">,無 async/defer,在 head 阻塞 HTML 解析(parser-blocking)= 延遲 FCP
外部 JS <script defer src="..."> 不阻塞解析,DOM 完成後執行
外部 JS <script async src="..."> 不阻塞解析,下載完立刻執行
內嵌 JS <script>...</script>,在 head 阻塞解析,直到執行完
圖片 / 影片 任何位置 不阻塞渲染(但影響 LCP 和 bandwidth)
Web Font WOFF2/WOFF via CSS @font-face 不阻塞渲染,但可能造成 FOIT(字型閃爍)

值得注意的一點:「阻塞渲染」和「阻塞解析器(parser-blocking)」是兩件不同的事。CSS 是 render-blocking,JS(無 async/defer)是 parser-blocking。Parser-blocking 的 JS 如果放在 <body> 最後,對 FCP 影響很小;但放在 <head> 裡,實際上也會延遲首次繪製。

用 Chrome DevTools 和 Lighthouse 診斷渲染阻塞

要修之前先要找到問題。Lighthouse 會直接告訴你哪些資源在阻塞,但 DevTools 的 Network 和 Coverage 面板能給你更多細節。

Lighthouse 診斷步驟:

  1. 打開 Chrome,按 F12 開啟 DevTools,切到「Lighthouse」頁籤
  2. 選「Performance」,Device 選「Mobile」(行動裝置條件更嚴苛,問題更明顯)
  3. 點「Analyze page load」,等跑完
  4. 在「Opportunities」區塊找「Eliminate render-blocking resources」,展開看哪些檔案在阻塞以及估計節省時間

DevTools Coverage 面板找 critical vs non-critical CSS:

  1. 按 Ctrl+Shift+P(Mac: Cmd+Shift+P)打開 Command Menu
  2. 搜尋並執行「Show Coverage」
  3. 點 Coverage 面板左上角的錄製按鈕,重新載入頁面
  4. 在列表找到你的 CSS 檔案,點進去
  5. 紅色條 = 這行 CSS 在初始載入時未使用(non-critical);灰色/綠色 = 首次繪製需要(critical)

Network 面板看 waterfall:在 Network 面板按 Ctrl+Shift+R 硬重新載入,看請求瀑布圖。Render-blocking 的 CSS 和 JS 會在 waterfall 的早期出現長條,藍色垂直線(DOMContentLoaded)之前都是阻塞期。如果某個 CSS 很早就開始下載但需要等很久,旁邊又沒有任何內容渲染,那就是你的瓶頸。

CSS 和 JS 渲染阻塞消除決策樹圖:inline vs preload vs defer vs media query 四個路徑
CSS 渲染阻塞消除決策樹:根據 CSS 用途選擇最適合的處理方式

CSS 渲染阻塞消除:inline critical CSS 還是 defer?

CSS 渲染阻塞最正確的處理方式取決於這段 CSS 什麼時候被需要,不是一律 inline 或一律 defer。根據用途不同,有四條處理路徑。

我用的決策框架分四個路徑:

情境 處理方式 原因
首屏(above-fold)所需 CSS inline 到 <style> in <head> 消除阻塞,同時確保首次繪製樣式正確
捲動後才用到的 CSS <link rel="preload"> + onload swap 不阻塞首次繪製,但預先載入備用
特定 media 條件下才用的 CSS(列印、高解析度) <link media="print"> 或 media="(min-width:768px)"> 瀏覽器不會阻塞不符合條件的 media stylesheet
完全沒用到的 CSS 移除 Coverage 工具確認後直接刪

Inline critical CSS 的實作:

<!-- 首屏 CSS 直接 inline -->
<style>
  /* 只放 above-fold 需要的樣式,通常 5-15KB 以內 */
  body { font-family: system-ui; margin: 0; }
  header { background: #1a1a2e; color: #fff; padding: 1rem; }
  .hero { min-height: 60vh; display: flex; align-items: center; }
</style>

<!-- 其餘 CSS 用 preload 非阻塞載入 -->
<link rel="preload" href="/styles/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/styles/main.css"></noscript>

onload swap 的邏輯是:先用 rel="preload" 讓瀏覽器並行下載 CSS,但不阻塞渲染;下載完成後 onload 觸發,把 rel 改成 stylesheet,樣式才真的套用。<noscript> 是給停用 JS 的使用者的 fallback,不能省略。

Media query 分割的實作:

<!-- 這個只在列印時載入,不阻塞螢幕渲染 -->
<link rel="stylesheet" href="/styles/print.css" media="print">

<!-- 這個只在寬螢幕載入 -->
<link rel="stylesheet" href="/styles/wide.css" media="(min-width:1024px)">

要抽 critical CSS,手動做很痛苦。工具推薦:critical 這個 npm 套件可以自動幫你跑 headless Chrome、擷取首屏 CSS 並 inline。大型專案用 CI 跑更方便。

JavaScript 渲染阻塞消除:async、defer 和動態載入

JS 的阻塞主要來自沒有加 async 或 defer 屬性的 <script> 標籤。這三個屬性的差異要搞清楚,用錯了會出問題。

屬性 下載時機 執行時機 執行順序 適合用途
(無屬性) 遇到時立刻 下載完立刻 阻塞 HTML 解析 少用,只用於必須立刻執行的 inline 腳本
defer 並行下載 DOM 完成後,DOMContentLoaded 前 保持原始順序 大多數外部 JS 庫,依賴 DOM 的腳本
async 並行下載 下載完立刻(隨機順序) 不保證順序 獨立的第三方腳本(analytics、廣告)

我看過最常見的錯誤:把有相依關係的 JS 加了 async,結果執行順序不固定,A 依賴 B 但 B 還沒跑完,console 報錯。有相依關係的腳本用 defer,獨立的用 async。

<!-- 原本阻塞的寫法 -->
<script src="/js/vendor.js"></script>
<script src="/js/app.js"></script>

<!-- 修正後:defer 保持順序,不阻塞 -->
<script src="/js/vendor.js" defer></script>
<script src="/js/app.js" defer></script>

對於非常龐大的 JS bundle,還可以用動態 import 做 code splitting,只在需要時載入特定模組:

// 只有使用者點擊特定按鈕時才載入這個模組
button.addEventListener('click', async () => {
  const { init } = await import('./heavy-module.js');
  init();
});

這對 JavaScript 效能優化中的 Long Task 問題很有幫助,把不必要的初始化推遲到真正需要的時候。

第三方腳本的阻塞問題:最常被忽略的瓶頸

第三方腳本是最常被忽略的 render-blocking 來源,因為它們通常不是你的代碼,而且來自外部 domain,下載時間無法預測。

常見第三方腳本的阻塞風險分類:

腳本類型 預設阻塞風險 建議處理
Google Analytics 4 (gtag.js) 低(官方版已 async) 確認加載方式包含 async
Google Tag Manager 中(snippet 包含同步 dataLayer init) 移至 <body> 末尾;或用 defer
Hotjar / Clarity 高(預設同步載入) 改為 defer,或只在 DOMContentLoaded 後載入
Facebook Pixel 低(已 async) 確認使用官方 snippet 版本
Live chat 小工具(Intercom、Zendesk) 高(SDK 通常同步) 用 facade pattern,延遲至使用者互動時載入
A/B testing 腳本(Optimize 已停用,VWO 等) 極高(設計上就是要在首次渲染前執行) 評估是否真的需要;或用 server-side 方案

Facade pattern 是處理 chat widget 的好方法:先顯示一個靜態的假按鈕圖片,使用者點擊時才動態載入真正的 SDK。這讓頁面首次渲染完全不需要等 chat SDK,但使用者互動時功能一樣可用。

CSS @import 鏈:最容易被遺漏的阻塞來源

CSS @import 是一個很容易被忽略的阻塞問題,而且比一般的 render-blocking CSS 更糟糕,因為它會產生串聯阻塞鏈(blocking chain)。

問題在這裡:瀏覽器先下載並解析 main.css,然後才發現裡面有 @import 指向 fonts.css;然後再去下載 fonts.css,發現裡面又有 @import 指向 variables.css。這是串行的,每一個 @import 都要等前一個下載完才能開始。

/* main.css: 瀏覽器先下載這個 */
@import url('/css/fonts.css');    /* 等 main.css 解析後才開始下載 */
@import url('/css/variables.css'); /* 又等 fonts.css 解析後才開始 */

body { ... }

如果每個 CSS 下載需要 200ms,這個鏈就讓渲染延遲了至少 600ms。改成 <link> 標籤就可以並行下載:

<!-- 這樣三個 CSS 並行下載,只需要等最慢的那個 -->
<link rel="stylesheet" href="/css/main.css">
<link rel="stylesheet" href="/css/fonts.css">
<link rel="stylesheet" href="/css/variables.css">

診斷方式:在 DevTools Network 面板,按下載開始時間排序,如果你看到三個 CSS 請求的開始時間是階梯狀(一個比一個晚),就是 @import 鏈。如果是平行的(幾乎同時開始),就沒問題。

特別危險的情況:Google Fonts 也可以用 @import 引入。如果你的 CSS 裡有 @import url("https://fonts.googleapis.com/..."),這不只是一個 @import,還需要跨 domain 建立新連線(DNS + TCP + TLS),延遲更長。改用 <link rel="preconnect"> 搭配直接的 <link rel="stylesheet"> 才正確。

消除後 LCP 和 FCP 能改善多少?

效果取決於阻塞資源的數量和大小,但多數案例 FCP 和 LCP 都有明顯改善。這是我實際測試過的一個 Next.js 專案(行動裝置 Lighthouse,Fast 3G throttle):

指標 修改前 修改後 改善幅度
LCP 3.1s 1.8s -42%
FCP 2.4s 1.1s -54%
TBT(Total Blocking Time) 680ms 420ms -38%
Lighthouse Performance Score 54 81 +27 分

這個案例做了三件事:(1)把 2 個 render-blocking CSS 檔案改成 inline critical CSS + preload defer;(2)把 3 個同步 JS 加上 defer;(3)把 Hotjar 改成 DOMContentLoaded 後才載入。不是什麼黑魔法,每個步驟都有對應的工具和代碼模式。

要注意的是:Lighthouse 跑出來的改善是 lab data,在模擬環境。真實 CrUX field data 的改善通常要 28 天的 rolling window 才反映出來。這兩個數據的差距為什麼存在,我在 LCP 優化的四段時間模型裡有解釋。

常見錯誤:消除阻塞後 FOUC 出現怎麼處理?

FOUC(Flash of Unstyled Content,無樣式內容閃爍)是移除 render-blocking CSS 後最常見的副作用。原因很直接:你把某些 CSS defer 掉了,但那段 CSS 其實是首屏內容需要的,結果使用者看到一瞬間沒有樣式的頁面。

預防方法有兩個:

  1. Critical CSS 要抽得夠完整。用 Coverage 工具看,不只看「是否使用過」,要用 Slow 3G throttle 重新載入,確認所有首屏出現的元素樣式都有被標記為 critical。很多開發者在快速網路下測試,看不到 FOUC,但在慢速網路上就出現了。
  2. 用 JavaScript 偵測 CSS 載入完成。如果你用 rel="preload" + onload swap 方式 defer CSS,在 CSS 套用之前,頁面可能短暫呈現 inline critical CSS 的樣式。這通常問題不大,但如果你的 critical CSS 不夠完整,就會出現版面跳動。解法是確保 critical CSS 包含所有上方可見區域的版面定義(高度、寬度、字型大小、背景色)。

測試方式:在 DevTools Network 面板,把網路速度調到「Slow 3G」,清除快取後重新載入。如果你看到頁面先以無樣式(或樣式殘缺)狀態顯示,再突然套入完整樣式,FOUC 就存在。

Render Blocking 診斷到修復的決策流程

把上面所有步驟整合成一個可重複執行的診斷流程,從 Lighthouse 警告到 Slow 3G 驗證,每次修改新專案或幫客戶做效能優化時按這個走,不會漏掉任何阻塞來源。

Render Blocking 診斷到修復的決策流程圖:Lighthouse 警告到 CSS defer、JS async、@import 修復的完整步驟
從 Lighthouse 警告到實際修復的完整 render blocking 診斷流程
  1. 跑 Lighthouse,找「Eliminate render-blocking resources」,記下每個阻塞資源和估計節省時間
  2. 開 Network 面板,Slow 3G 重新載入,看 waterfall,確認阻塞資源是哪些,下載時間多長
  3. 分類:CSS 還是 JS?
    • CSS → 開 Coverage 面板,判斷 critical vs non-critical,按上述決策矩陣處理
    • JS → 確認是自己的還是第三方,加 defer 或 async,或用動態 import
  4. 特別檢查 CSS @import:在 Network 面板看 CSS 的「Initiator」欄,如果是由另一個 CSS 觸發(不是 HTML),就是 @import 鏈,改成並行的 <link> 標籤
  5. 確認第三方腳本:在 Network 面板按 Domain 篩選,看有沒有來自外部 domain 的 JS 或 CSS 出現在阻塞期
  6. 修改後 Slow 3G 重跑,觀察有沒有 FOUC,如果有就補充 critical CSS
  7. 再跑一次 Lighthouse,驗證 FCP 和 LCP 改善幅度,並用 CrUX 或 WebPageTest 跑 field data 確認

LCP 圖片的優先載入和 render blocking 修復是互補的,解決 render blocking 讓瀏覽器更快到達 LCP 圖片的下載時間點,然後再搭配 LCP 圖片的 fetchpriority 優化提升 LCP 圖片本身的載入順序,兩個一起做效果最明顯。

更多關於 TTFB 和 server 回應速度的影響,可以參考 TTFB 優化筆記,TTFB 慢通常是 render blocking 改完後下一個要解決的問題。


延伸閱讀:

陳

陳柏翰

網站效能工程師

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

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

想看更多?

探索所有效能實驗

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

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