CSS 阻塞渲染的解法:Critical CSS 提取内联、字体与首屏图片的二级阻塞清理

数据能说明问题:很多独立博客的移动端 LCP 卡在 2.8 秒到 4 秒这个区间,而它们并不缺 CDN、不缺 HTTP/2,页面里也没有大图。用 PageSpeed Insights 一跑,报告会准确地把责任指向「移除阻塞渲染的资源」,但那几十 KB 的 CSS 就摆在那里,删不掉也合不了。这篇讲的是怎么真的把它挪出关键路径——Critical CSS 的提取、内联、异步加载,以及字体和高优先级图片这两块最常见的二次阻塞。

一、先搞清楚谁在阻塞渲染

浏览器解析 HTML 遇到 会暂停渲染,直到该样式表下载并解析完毕。这不是 bug,而是为了避免「先渲染无样式内容再重排」(FOUC)。所以问题不在于「有 CSS」,而在于「首屏需要的 CSS 和全站需要的 CSS 被混在一起,全都放在 head 里同步加载」。

# 用 Lighthouse 的 CLI 版拿一份结构化报告,方便对比前后
npx lighthouse https://example.com \
  --only-categories=performance \
  --form-factor=mobile --throttling-method=simulate \
  --output=json --output-path=./lh-before.json --chrome-flags="--headless"

# 看阻塞资源清单
node -e "const r=require('./lh-before.json');
const a=r.audits['render-blocking-resources'];
console.log(a.score, a.displayValue);
(a.details.items||[]).forEach(i=>console.log(i.url, i.totalBytes, i.wastedMs));"

同时要看 render-blocking-insight 之外的另一个指标:关键请求深度。一个 CSS 文件本身不大,但它引用了字体文件,字体又引用了另一份 CSS,链深度变成三层,移动端 RTT 高的时候光排队就要一秒多。

二、Critical CSS 到底是什么,不该是什么

Critical CSS 指「渲染首屏(above the fold)所必需的那部分规则」。做法是:把这段规则内联进 的

不占关键路径,也不需要 preload。代价是主 CSS 的下载起步更晚。实践中最平衡的做法是 A + C 混用:preload 主 CSS(让它尽早开始下载),同时用 script 在 DOM 就绪后把它变成 stylesheet。这样既早下载又不阻塞。

五、字体:第二个隐形阻塞点

即便 CSS 处理完了,自定义字体仍可能让首屏文字延迟显示(FOIT),或者从无衬线跳到自定义字体(FOUT 引起的 CLS)。三条规则:

@font-face {
  font-family: 'SiteSans';
  src: url('/fonts/sitesans.woff2') format('woff2');
  font-display: swap;          /* 先用系统字体显示,字体到了再换 */
  unicode-range: U+0000-00FF;  /* 只要拉丁字符,避免整包下载 */
  size-adjust: 104%;           /* 微调系统字体的度量,减少跳动 */
}
  • 只加载 woff2,老格式在现代浏览器已无必要。
  • 中文字体动辄 3–8MB,必须做子集化(按页面实际用到的字符)或者干脆用系统字体栈。font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; 在很多场景下比下载一个中文字体更快也更好看。
  • 首屏用到的字体文件加 ,注意 crossorigin 不能漏,字体请求是 CORS 请求,漏了会导致下载两次。

六、首屏图片与 LCP

LCP 元素如果是图片,它往往才是真正的瓶颈,CSS 优化只解决了一部分。做法:


...


...

常见的两个错误:给首屏大图加了 loading="lazy"(浏览器会推迟到布局后才请求,直接拖后 LCP);以及不给 width/height,导致图片加载完成后页面重排,CLS 飙升。

七、优化前后怎么验证

别只看一次 Lighthouse 分数,它有波动。用真实用户数据兜底:

指标目标数据来源
LCP< 2.5s (75分位)CrUX / GSC 核心网页指标
CLS< 0.1同上
INP< 200ms同上
阻塞资源浪费时间< 300msLighthouse

实操上建议按这个顺序:先在弱网模拟(4G、CPU 4x 降速)下跑三次 Lighthouse 取中位数作为基线;改完之后用完全相同的条件复测;最后等 7–28 天看 GSC 的核心网页指标分组变化。只有真实用户数据改善,才算真的优化成功——实验室数据只是必要非充分条件。

八、容易踩的三个坑

  1. 内联 CSS 与 CSP 冲突。如果你有 Content-Security-Policy 且没有 'unsafe-inline',内联样式块会被拦。解法是用 nonce 或 hash,把内联的

    不占关键路径,也不需要 preload。代价是主 CSS 的下载起步更晚。实践中最平衡的做法是 A + C 混用:preload 主 CSS(让它尽早开始下载),同时用 script 在 DOM 就绪后把它变成 stylesheet。这样既早下载又不阻塞。

    五、字体:第二个隐形阻塞点

    即便 CSS 处理完了,自定义字体仍可能让首屏文字延迟显示(FOIT),或者从无衬线跳到自定义字体(FOUT 引起的 CLS)。三条规则:

    @font-face {
      font-family: 'SiteSans';
      src: url('/fonts/sitesans.woff2') format('woff2');
      font-display: swap;          /* 先用系统字体显示,字体到了再换 */
      unicode-range: U+0000-00FF;  /* 只要拉丁字符,避免整包下载 */
      size-adjust: 104%;           /* 微调系统字体的度量,减少跳动 */
    }
    • 只加载 woff2,老格式在现代浏览器已无必要。
    • 中文字体动辄 3–8MB,必须做子集化(按页面实际用到的字符)或者干脆用系统字体栈。font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; 在很多场景下比下载一个中文字体更快也更好看。
    • 首屏用到的字体文件加 ,注意 crossorigin 不能漏,字体请求是 CORS 请求,漏了会导致下载两次。

    六、首屏图片与 LCP

    LCP 元素如果是图片,它往往才是真正的瓶颈,CSS 优化只解决了一部分。做法:

    
    ...
    
    
    ...

    常见的两个错误:给首屏大图加了 loading="lazy"(浏览器会推迟到布局后才请求,直接拖后 LCP);以及不给 width/height,导致图片加载完成后页面重排,CLS 飙升。

    七、优化前后怎么验证

    别只看一次 Lighthouse 分数,它有波动。用真实用户数据兜底:

    指标目标数据来源
    LCP< 2.5s (75分位)CrUX / GSC 核心网页指标
    CLS< 0.1同上
    INP< 200ms同上
    阻塞资源浪费时间< 300msLighthouse

    实操上建议按这个顺序:先在弱网模拟(4G、CPU 4x 降速)下跑三次 Lighthouse 取中位数作为基线;改完之后用完全相同的条件复测;最后等 7–28 天看 GSC 的核心网页指标分组变化。只有真实用户数据改善,才算真的优化成功——实验室数据只是必要非充分条件。

    八、容易踩的三个坑

    1. 内联 CSS 与 CSP 冲突。如果你有 Content-Security-Policy 且没有 'unsafe-inline',内联样式块会被拦。解法是用 nonce 或 hash,把内联的