在线工具集

Critical CSS 实战:解锁首屏 1s

外部 CSS 文件是首屏渲染的隐形拦路虎。一份 80KB 的 main.css 在 4G 网络下需要 300 到 600 毫秒下载与解析,期间浏览器拒绝绘制任何内容(CSSOM 阻塞渲染)。Critical CSS(关键 CSS)通过内联首屏所需样式、异步加载剩余样式,把 First Contentful Paint 与 Largest Contentful Paint 各砍 200 到 800 毫秒。本文从原理到实战完整讲透:什么是关键 CSS、为什么有效、怎么用 critical / penthouse / criters 提取、异步加载剩余 CSS 的两种工程化方案、FOUC 防护、Astro / Next.js 等元框架的内置支持、Lighthouse 验证流程。读完即能在自家项目把首屏 1 秒达成。

1. 渲染阻塞与关键渲染路径

浏览器渲染一个页面要走「关键渲染路径」(Critical Rendering Path):下载 HTML、构建 DOM;下载 CSS、构建 CSSOM;合并 DOM 与 CSSOM 生成渲染树;计算布局;绘制像素。其中 CSS 是默认阻塞的——浏览器必须等所有 link rel=stylesheet 引用的 CSS 下载并解析完成,才会开始合并渲染树。这是为了避免 FOUC(无样式闪烁),但代价是首屏延迟。

具体数字:一份 100KB 的 CSS 文件在 4G 网络(约 1.5Mbps 有效带宽)下下载 500ms 左右,加上解析 50 到 150ms,总共 600 到 700ms 浏览器拒绝绘制。这意味着即使 HTML 已到达浏览器,用户仍盯着白屏。Lighthouse 报告里 Eliminate render-blocking resources 项就是抓这个。

Critical CSS 的核心思路:把首屏内容(above-the-fold,视口内可见部分)所需的最小样式抽出来,直接 inline 到 HTML head 的 style 标签内。HTML 一到达浏览器立刻能用这套样式开始绘制,无需任何额外 CSS 网络请求。剩余非关键样式(below-the-fold 内容、交互组件、动画细节)异步加载,下载完成后追加到页面但不阻塞首屏。

2. 关键 CSS 的边界:哪些算关键

边界判断有三个层次。第一层是空间边界:视口内(above-the-fold)可见元素所需样式都是关键。这是最常见定义。但视口大小因设备而异,移动端 375x667、桌面端 1280x720 是常用基准;多视口都需提取并合并。

第二层是时间边界:页面加载后立即可见的元素,包括动画初始状态。例如骨架屏、loading 转圈、首屏 hero 区的淡入动画起点都算关键。下拉菜单的 hover 态不算关键(用户交互后才出现)。

第三层是依赖边界:关键样式依赖的全局变量、reset、字体声明都要包含。比如 :root 的 CSS 变量、box-sizing 全局规则、normalize.css、@font-face 声明(特别是首屏字体)都要 inline,否则关键 CSS 失效。

典型一份关键 CSS 大小在 8 到 20KB 之间(gzip 后 2 到 5KB)。超过 20KB 说明首屏太复杂或工具配置错误,应审视;低于 5KB 可能漏掉了关键依赖。

3. 提取工具对比:critical / penthouse / criters

三大主流工具。critical 是 Addy Osmani(Google Chrome 团队)维护的 npm 包,最早期最成熟。原理是用 Puppeteer 驱动 headless Chrome 加载页面,按指定视口截屏,遍历所有 CSS 规则判断是否影响首屏元素,输出关键 CSS 字符串。优势:精准、支持复杂 SPA、可同时处理多视口合并。劣势:需要 Puppeteer 与 Chrome,CI 时启动慢、内存占用高。适合 SSR / SSG 构建的中后期处理或独立批量处理。

penthouse 是底层提取引擎,critical 内部也用它。功能更轻量,要自己组装 HTML 注入与异步加载流程。研究或定制场景才直接用 penthouse。

criters(critters,名字来自小动物 critter)是 GoogleChromeLabs 出品的现代方案。工作时机是构建产物 HTML 阶段:扫描已生成的 HTML 与外部 CSS,模拟首屏匹配规则(不启动 Chrome 而是用 cheerio 与 css-select 静态分析),inline 关键部分,把外部 CSS 改成异步加载链接。优势:无 Puppeteer 依赖、构建时极快(10 倍于 critical)、与 Webpack / Vite / Next.js 集成完善。劣势:纯静态分析对 JS 动态生成的 DOM 不敏感,重客户端渲染场景不准确。是元框架与 SSG 项目的首选。

选型规则:构建时 SSG/SSR 已输出完整 HTML 用 critters;纯客户端 SPA(CRA、Vite SPA)用 critical;元框架自带(Next.js / Astro / Nuxt)用框架内置。

4. 内联策略:style 标签 vs 内嵌 link

关键 CSS 提取出来后要 inline 到 HTML head。最直接的做法是在 head 里加 style 标签直接放 CSS 文本:<style>关键 CSS 字符串</style>。HTML 多几 KB 但避免一个网络请求,对首屏速度净收益。

大小取舍:关键 CSS 越短越好,控制在 14KB 以内(一个 TCP 慢启动 cwnd 初始窗口约 14KB),保证首个 RTT 就能把 HTML 与关键 CSS 一起送到。超过 14KB 时考虑减少首屏样式复杂度,不要为了 inline 多塞。

Astro 内置每个页面专属 CSS scoped 处理,构建时把 island 边界外的 CSS 自动 inline 到 head。Next.js 的 experimental.optimizeCss 开启后也自动 inline。Hugo、Jekyll 等纯静态站点要用 critters 后处理。

HTTP/2 之后曾有声音质疑 inline 的必要性(多路复用让外部 CSS 没那么贵),但实测在弱网与移动端 inline 仍快 100 到 300ms,CWV 实测仍胜出。Critical CSS 仍是 2026 年标准实践。

5. 异步加载剩余 CSS:两种主流模式

关键 CSS 内联后,原本的 link rel=stylesheet 必须改成异步加载,否则又回到阻塞老路。两种主流模式。

模式 A:preload + onload swap。link rel=preload as=style href=full.css onload="this.rel='stylesheet'"。preload 让浏览器以低优先级在后台下载 CSS,下载完触发 onload 把 rel 改为 stylesheet 激活样式。配 noscript link rel=stylesheet href=full.css 兜底禁用 JS 用户。优势:现代浏览器原生支持、语义清晰。劣势:onload 在某些旧浏览器(Safari 历史版本)不触发,需 polyfill loadCSS(Filament Group 出品的小库)。

模式 B:media=print swap。link rel=stylesheet href=full.css media=print onload="this.media='all'"。media=print 告诉浏览器这个 CSS 仅打印时用,因此不阻塞屏幕渲染但仍下载;onload 把 media 改回 all 激活。优势:所有浏览器支持,无需 polyfill。劣势:稍 hack,需注释解释意图。

实战推荐模式 B(兼容性赢)。两种模式效果相同:首屏用 inline 关键 CSS 立即绘制,后台拉完整 CSS,下载完应用。整个过程对用户无感。

6. FOUC 防护与边界情况

FOUC(Flash of Unstyled Content)是 Critical CSS 用错时的反噬:首屏有元素的样式没进 inline,渲染时显示无样式版本,几百毫秒后 full.css 到达突然切回正确样式。视觉断层差。

预防策略五条。第一,提取时设置正确的视口(mobile 375x667 + desktop 1280x720 都跑一遍并合并)。第二,扫描首屏所有可能元素,包括第三方 widget、动画初始态。第三,把首屏字体的 @font-face 也内联,避免字体切换引起布局跳变(详见 Web 字体加载策略)。第四,对依赖 JS 才出现的关键内容(client-side rendered hero)显式标记 hidden 直到 hydration 完成。第五,noscript 标签内放完整 link rel=stylesheet href=full.css 给禁用 JS 用户兜底。

边界情况处理。Cookie 同意横幅常在首屏顶部 push 内容,CLS 影响大;用 fixed 或 sticky 定位避免 push。响应式断点跨越视口的样式都要进关键集。Web 字体首字宽差异引起的偏移用 size-adjust 调整 fallback 度量。

7. 元框架内置:Next.js / Astro / Nuxt

2026 年的元框架基本都内置 Critical CSS 处理。新项目优先靠它们,不需手动接 critical/critters。

Astro:默认对每个页面生成专属 CSS,根据 island 边界自动拆分。页面只引用对应页面 CSS(通常已经是「关键 CSS」级别)。无 island 的纯静态页面 CSS 全 inline 到 head。Tailwind 配合 Astro 默认 purge 后单页 CSS 常 5 到 15KB,无需再做关键 CSS 提取。

Next.js Pages Router:next.config.js 设 experimental.optimizeCss: true 启用 critters 自动处理。App Router:默认按需注入 RSC 边界 CSS,关键 CSS 自动 inline。Server Components 输出时已经携带关键 CSS。

Nuxt 3:vite-plugin-css 处理,关键 CSS 默认 inline。配置 experimental.inlineSSRStyles 开启更激进的内联模式。

Remix:默认所有 route 独立 link rel=stylesheet,不自动做关键 CSS。需要手动加 remix-image 或 critters 插件。Sveltekit 类似 Astro 默认每页 scoped CSS。

判断是否还需手动处理:看 Lighthouse Performance 是否提示 Eliminate render-blocking resources,提示就要动手;不提示说明框架已处理好。

8. 验证与监控:Lighthouse + Field 数据双轨

Critical CSS 上线后必须验证效果,否则可能配置错或被新代码意外破坏。验证流程三步。

第一步,本地 Lighthouse。Chrome DevTools 打开 Performance Insights 面板录制一次首屏加载,查看 Critical Request Chain 与 Render-Blocking Resources。优化前 main.css 通常是 render-blocking;优化后只剩 HTML 一个关键请求,CSS 标记为 Async。Performance 评分通常上升 10 到 25 分。

第二步,PageSpeed Insights。输入页面 URL 看 Lab 分数与 Field 数据(CrUX)。Field 数据反映真实用户的 28 天首字节、FCP、LCP。Critical CSS 优化后 LCP 在 2 到 4 周内 Field 数据明显改善。

第三步,CI 长期监控。Lighthouse CI 集成到 GitHub Actions,每个 PR 跑一次性能预算检查(FCP < 1.8s、LCP < 2.5s、Total CSS < 50KB),超标 PR 失败。配合 web-vitals 库前端埋点上报真实 FCP/LCP/CLS 到自家分析平台,长期跟踪。

Critical CSS 是首屏 1 秒达成的临门一脚,配合 Core Web Vitals 优化图片优化深度教程JS bundle 体积优化、字体策略,整套体系下来一个普通项目首屏 1 秒达成是常态。

常见问题

Critical CSS 到底是什么?

Critical CSS(关键 CSS)是渲染首屏内容(above-the-fold)所需的最小样式集合。把它内联到 HTML 的 head style 标签内,浏览器拿到 HTML 即可立即绘制首屏,无需等待外部 CSS 文件下载与解析。剩余非关键 CSS 通过 link rel=preload onload swap 或 media=print 切换技巧异步加载,不阻塞渲染。这套技术能把首次内容绘制(FCP)与最大内容绘制(LCP)各砍 200 到 800 毫秒。

critical 与 penthouse 与 criters 怎么选?

critical 是 Addy Osmani 团队维护的 npm 包,最成熟,Puppeteer 驱动 headless Chrome 提取首屏 CSS,支持多视口、自动 inline 与外链分离。penthouse 是底层引擎,更轻量但要自己组装 inline 流程。criters(critters)是 GoogleChromeLabs 出品,工作时机是构建产物 HTML 阶段,速度快、与 Webpack/Vite 集成好,是现代项目首选。三者输出本质相同,区别在集成方式:要 SSR 中后期处理用 critical;要构建时静态处理且速度快选 critters。

为什么要用 media=print 异步加载剩余 CSS?

传统 link rel=stylesheet 是阻塞渲染的——HTML 解析到这一行会暂停 DOM 构造直到 CSS 下载并解析完成。把它改成 link rel=stylesheet href=full.css media=print onload="this.media=all" 是个聪明 hack:media=print 让浏览器认为这个 CSS 仅打印时使用,因此不阻塞屏幕渲染,但浏览器仍会下载(优先级降低);onload 回调把 media 改回 all,激活样式。这是无 JS 依赖的异步加载,效果接近 preload 但兼容性更好。现代浏览器也可用 link rel=preload as=style onload swap 模式。

Critical CSS 会引起 FOUC 吗?

会,如果首屏依赖的样式没全部包进 inline。FOUC(Flash of Unstyled Content)是用户看到无样式或部分样式的内容、然后样式突然加载完成切换的视觉断层。预防:用工具时设置正确的视口尺寸(移动 + 桌面)、扫描所有首屏元素(包括 hover 的 menu、动画初始态)、把 Web 字体相关 @font-face 也内联(否则字体切换会引发布局跳变)、复杂动态组件首次渲染加 hidden/opacity:0 直到关键 CSS 应用。配合 noscript fallback link 兼容禁用 JS 用户。

Astro / Next.js 是不是就不用手动做了?

基本不用,但仍要理解原理排查异常。Astro 默认对每个页面生成专属 CSS 并按 island 边界拆分,单页 CSS 通常已小到不需要再做关键 CSS 提取。Next.js Pages Router 自动 inline 关键 CSS(next.config.js 的 experimental.optimizeCss);App Router 默认按需注入。Nuxt 用 vite-plugin-css 优化。但当页面包含巨大第三方 UI 库(antd、Tailwind 全量未 purge)时仍可能 100KB+ CSS 阻塞,需要手动加 critters 插件。Lighthouse Opportunities 里出现 Eliminate render-blocking resources 提示就要动手。

相关工具