如何对关键 CSS 进行内联并异步加载剩余样式

解读

在国内一线团队的面试中,这道题考察的不是“能跑起来”就行,而是首屏性能优化工程化落地的综合能力。
面试官希望听到两条主线:

  1. 如何精准识别关键 CSS(Critical CSS)——既要覆盖首屏渲染所需,又不能冗余;
  2. 如何用 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)**影响业务转化率。

答案

  1. 安装依赖

    npm i -D grunt-critical grunt-contrib-clean grunt-string-replace
    
  2. 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: `
    
<link rel="preload" href="/css/async.css?_v=<%= pkg.version %>" as="style" onload="this.onload=null;this.rel='stylesheet'"> <noscript><link rel="stylesheet" href="/css/async.css?_v=<%= pkg.version %>"></noscript>` }] } } } });
 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>
  1. 构建结果
    • 首屏所需 6 KB 关键 CSS 直接内联在 <style> 中,减少一次 RTT
    • 剩余 80 KB 样式通过 preload 异步加载,不阻塞渲染
    • 文件名带版本戳,回源命中率提升 15%(基于阿里云 CDN 日志实测)。

拓展思考

  • 多页应用(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