浏览器渲染页面需要构建CSSOM,外部样式表会阻塞渲染。如果样式文件体积较大,首屏白屏时间就会明显变长。Critical CSS技术的做法是提取首屏可见区域对应的少量CSS规则,内联到HTML的<style>标签中,让浏览器在下载大体积外部样式之前先完成首次绘制,同时将完整样式改为异步加载。在Webpack 5构建流程中,这一提取与注入过程可以借助专门插件自动完成。

Webpack 5本身并没有内置Critical CSS提取器,但它在资源模块、缓存以及插件体系上的改进,让关键CSS方案更容易落地。理解浏览器阻塞机制,再结合适合的Webpack插件,就能显著提升首屏性能指标。
为什么首屏渲染会被CSS阻塞?
浏览器解析HTML时会构建DOM树,解析CSS时会构建CSSOM树。渲染树必须同时依赖DOM和CSSOM,因此任意一个外部样式表尚未加载完成,页面就不会进入绘制阶段。对移动端或弱网环境来说,一个几百KB的CSS文件可能需要几百毫秒甚至数秒才能完成传输,这段时间用户看到的只有白屏。
传统HTML入口通常直接引入完整样式文件,例如:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>页面标题</title> <link rel="stylesheet" href="/assets/main.css"> </head> <body> <header class="hero">首屏内容</header> </body> </html>
优化思路是:只把首屏可见区域真正用到的样式抽取出来,直接内联到HTML的<style>标签中。浏览器读取HTML时就能立即获得样式并完成首帧绘制,完整CSS则通过preload或延迟方式加载,避免阻塞首次渲染。优化后的结构大致如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>页面标题</title>
<style>
.hero { display: flex; height: 60vh; background: #f5f5f5; }
</style>
<link rel="preload" href="/assets/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/assets/main.css"></noscript>
</head>
<body>
<header class="hero">首屏内容</header>
</body>
</html>
这里的关键在于,<link rel="preload">会提前下载完整CSS,但不会阻塞渲染,下载完成后通过onload切换为普通样式表。浏览器在解析到内联的<style>时已经可以绘制首屏,用户感知的白屏时间大幅缩短。
Webpack 5 下提取关键CSS的两种实战方案
要在Webpack构建中自动完成关键CSS提取,目前主流方案有两种:一种是使用基于静态分析的critters-webpack-plugin,另一种是使用真实浏览器渲染的html-critical-webpack-plugin。
方案一:critters-webpack-plugin
Critters是Google推出的关键CSS提取库,它不依赖完整浏览器,通过解析HTML和CSS选择器来匹配DOM节点,构建速度非常快。Webpack插件版本安装简单:
npm install critters-webpack-plugin --save-dev
在Webpack配置中,它通常与MiniCssExtractPlugin和HtmlWebpackPlugin配合使用:
const Critters = require('critters-webpack-plugin');
module.exports = {
plugins: [
new Critters({
preload: 'swap',
inlineThreshold: 5000,
pruneSource: true,
}),
],
};
其中preload: 'swap'表示用preload方式加载剩余CSS,inlineThreshold控制内联CSS体积上限,超过该大小就不会内联。pruneSource为true时,已经从完整CSS中提取并内联的规则会被移除,避免重复加载。
方案二:html-critical-webpack-plugin
该插件基于Puppeteer无头浏览器,会在构建时打开页面,计算首屏实际使用的CSS选择器,然后注入内联样式。安装命令如下:
npm install html-critical-webpack-plugin puppeteer --save-dev
配置示例:
const HtmlCriticalWebpackPlugin = require('html-critical-webpack-plugin');
module.exports = {
plugins: [
new HtmlCriticalWebpackPlugin({
base: 'dist/',
src: 'index.html',
dest: 'index.html',
inline: true,
minify: true,
width: 1300,
height: 900,
}),
],
};
两者各有适用场景,下面用表格对比:
| 能力维度 | critters-webpack-plugin | html-critical-webpack-plugin |
|---|---|---|
| 分析方式 | 静态DOM与CSS解析 | 真实浏览器渲染 |
| 构建速度 | 快 | 较慢 |
| 依赖体积 | 轻量 | 需下载Chromium |
| 动态JS首屏效果 | 较差 | 准确 |
| 适用项目 | 静态模板、SSR输出 | React/Vue等动态渲染页面 |
如果首屏内容主要来自静态HTML或服务端渲染,Critters就能满足需求;如果首屏完全由JavaScript异步生成,则必须借助无头浏览器渲染才能得到准确的关键CSS。
关键CSS提取的实现原理与优化细节
Critters的工作流程大致分为四步:读取HtmlWebpackPlugin生成的HTML,解析出所有CSS文件;将HTML解析为DOM树,记录所有节点对应的选择器;遍历CSS规则的AST,检查每条规则是否匹配任何DOM节点;把匹配的规则注入到HTML的<style>标签中,同时根据配置处理剩余CSS。
这种静态分析方式对@font-face、@keyframes和媒体查询有特殊处理。例如首屏如果使用了自定义字体,对应的@font-face规则会被保留并内联;如果某个动画只在首屏出现,那么关键帧也会被保留。但静态分析无法感知JavaScript动态插入的DOM,因此对React、Vue等客户端渲染框架来说,构建时HTML里通常没有真实节点,Critters提取到的关键样式可能很少甚至为空。
对于动态渲染页面,有两条路可走:一是开启SSR或预渲染,让构建时HTML包含首屏节点;二是改用html-critical-webpack-plugin,在真实浏览器中执行完JS后再提取。此外,即使使用真实浏览器方案,也需要考虑视口尺寸、设备类型等参数,移动端与桌面端的关键CSS可能不同,通常以移动端优先提取,再通过媒体查询补充桌面规则。
内联后的CSS如果体积过大,反而会拖慢HTML解析。一般建议内联CSS不超过15KB,否则应只提取最重要的首屏组件样式,剩余部分继续使用外部文件。可以先用Lighthouse等工具审计首屏渲染阻塞的资源,确认最佳内联阈值。
生产环境配置与常见问题排查
一个完整的Webpack 5生产配置通常包含CSS提取、HTML模板和关键CSS插件。下面是一个可直接用于生产构建的配置示例:
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const Critters = require('critters-webpack-plugin');
module.exports = {
mode: 'production',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].[contenthash:8].js',
clean: true,
},
module: {
rules: [
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
new Critters({
preload: 'swap',
inlineThreshold: 3000,
pruneSource: true,
}),
],
};
这个配置中,css-loader解析CSS依赖,MiniCssExtractPlugin将CSS抽离为独立文件,HtmlWebpackPlugin生成HTML入口,最后Critters负责提取并内联关键CSS。构建完成后,打开HTML文件就能看到内联的<style>标签以及带preload属性的完整样式链接。
常见问题之一是样式重复:如果pruneSource没有开启,或者CSS中存在作用范围很广的选择器,会导致完整样式文件里依然包含已经内联的规则,增加不必要的传输体积。另一个问题是构建时间变长,尤其是在使用Puppeteer方案时,每个页面都需要启动浏览器渲染,可以通过只对首页关键模板运行提取、其他页面跳过插件来优化。
排查时可以先查看HTML中内联<style>的实际体积,再打开浏览器开发者工具的网络面板,确认完整CSS是否为preload加载且没有阻塞首屏。如果发现首屏绘制完成但页面跳动明显,通常是异步样式加载后布局发生变化,这时需要把容易产生位移的属性尽量放入首屏关键CSS中,保证首帧布局稳定。
Critical CSS并不是所有项目都需要的优化手段。对于样式非常简单或者CSS总量本身很小的页面,提取收益可能不明显;但对于包含大量组件库、全局主题样式的中大型前端工程,配合Webpack 5的长期缓存策略和Critters插件,可以稳定降低首屏渲染时间,改善Core Web Vitals中的FCP和LCP指标。
Webpack 5Critical CSS关键CSS修改时间:2026-08-22 13:33:51