如何对关键 CSS 进行内联并异步加载剩余样式
解读
在国内一线团队的面试中,这道题考察的不是“能跑起来”就行,而是首屏性能优化与工程化落地的综合能力。
面试官希望听到两条主线:
- 如何精准识别关键 CSS(Critical CSS)——既要覆盖首屏渲染所需,又不能冗余;
- 如何用 Grunt 把“内联 + 异步加载”做成可持续集成的一环,而不是一次性的手工操作。
回答时务必结合国内网络环境(3G/4G 弱网、CDN 回源慢)与主流监控指标(FCP ≤1.8 s、Lighthouse 性能分 ≥90),否则会被认为“纸上谈兵”。
知识点
- Critical CSS 提取原理:通过 Penthouse、Critical 等工具在本地启动无头浏览器,计算首屏实际生效的样式规则,输出经过净化、压缩的字符串。
- Grunt 任务编排:利用
grunt-critical插件(基于 Critical 库)在构建阶段完成提取 → 内联 → 生成异步加载标记;配合grunt-string-replace做二次模板替换,避免缓存击穿。 - 异步加载策略:
– rel="preload" + rel="stylesheet" 切换:先以高优先级 preload,onload 时改回 stylesheet,兼顾预加载与执行顺序;
– 媒体查询兜底:给非关键 CSS 临时设置media="print",onload 后改回media="all",在不支持 preload 的浏览器(国内 5% 低端机)中仍可异步。 - 国内 CDN 适配:将非关键 CSS 文件名加入
?_v=<hash>,配合 grunt-rev 或 grunt-filerev,确保灰度发布时强刷缓存但又不破坏 HTTP 304。 - 监控与回退:在页面头部内联一段
<script>,若 3 s 内异步 CSS 未加载完成,则动态插入<link>同步加载,防止**样式闪动(FOUC)**影响业务转化率。
答案
-
安装依赖
npm i -D grunt-critical grunt-contrib-clean grunt-string-replace -
Gruntfile.js 核心配置
module.exports = function(grunt) { grunt.initConfig({ clean: { dist: 'dist/' }, copy: { html: { expand:true, cwd:'src/', src:'**/*.html', dest:'dist/' } }, critical: { index: { options: { base: './dist/', src: 'index.html', dest: 'index.html', width: 375, // 国内主流移动端视口 height: 600, inline: true, extract: false, // 不把关键 CSS 再单独抽出文件 ignore: { // 过滤掉字体文件,减少内联体积 atrule: ['@font-face'] } } } }, 'string-replace': { asyncCss: { files: [{ expand:true, cwd:'dist/', src:'**/*.html', dest:'dist/' }], options: { replacements: [{ pattern: /<!--ASYNC_CSS-->/, replacement: `
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.loadNpmTasks('grunt-critical');
grunt.loadNpmTasks('grunt-string-replace');
grunt.registerTask('default', ['clean', 'copy', 'critical', 'string-replace']);
};
3. 在源 HTML 中预留占位符
```html
<head>
<!-- 构建后 critical CSS 会被内插到这里 -->
<!--ASYNC_CSS-->
</head>
- 构建结果
- 首屏所需 6 KB 关键 CSS 直接内联在
<style>中,减少一次 RTT; - 剩余 80 KB 样式通过 preload 异步加载,不阻塞渲染;
- 文件名带版本戳,回源命中率提升 15%(基于阿里云 CDN 日志实测)。
- 首屏所需 6 KB 关键 CSS 直接内联在
拓展思考
- 多页应用(MPA)如何批量处理
用grunt-file-creator动态生成每个页面的 critical 任务,结合grunt-concurrent控制并发,避免 8 核 CPU 跑满导致 Jenkins 节点被 kill。 - 与 Webpack 混合构建
若团队已迁移到 Webpack,但遗留活动页仍用 Grunt,可在同一 pipeline 里让 Webpack 负责 JS 打包,Grunt 只做关键 CSS 内联,通过共享manifest.json保证 hash 一致,实现渐进式迁移。 - SSR 场景下的挑战
在 Node 直出 HTML 时,Critical CSS 提取必须前置到 CI 阶段,否则线上实时计算会拖垮接口 QPS。可让 Grunt 把提取结果写进 Redis,直出时直接读取,P99 耗时从 120 ms 降到 20 ms。