使用 grunt-cssnano 合并重复 @media 查询并计算节约字节

解读

这道题表面问“怎么用 grunt-cssnano 合并 @media”,实则考察三层能力:

  1. 对 grunt-cssnano 插件核心配置项的掌握,尤其是 mergeRulesmergeIdents@media 场景下的行为差异;
  2. 构建产物体积度量的敏感度,能否在 CI 或本地给出可量化的“节约字节”报告,这是国内一线厂绩效复盘里的硬指标;
  3. Grunt 生态链路的理解:从源文件 → 中间产物 → 压缩后 → gzip 后,分别如何取快照、做 diff,最终把结果回写到 MR 评论或邮件。

面试官常通过追问“字节到底怎么算”来筛掉只背文档的候选人,因此答案必须给出可落地、可复现、可集成到 Jenkins/GitLab CI 的脚本级方案

知识点

  • cssnano 预设default 预设已带 cssnano-preset-default,其中 mergeRules 默认 true,但 mergeIdents@media 不生效;需显式关闭可能导致拆分的优化(如 colormin 在旧版 WebKit 下会拆媒体查询)。
  • Grunt 多目标机制grunt.initConfig 中给同一任务配 dev/prod 双目标,利用 options.compare 开关决定是否走“度量”逻辑,避免本地开发阶段拖慢速度。
  • 字节计算策略
    原始体积grunt.file.read(original).length
    压缩后体积grunt.file.read(min).length
    gzip 体积:借助 zlib.gzipSync(buf).length,国内 CDN 按 gzip 计费,此值最贴近真实节省成本;
    diff 报告:用 pretty-bytes 把字节转人类可读,再输出 Markdown 表格供 GitLab 机器人抓取。
  • 缓存与增量:通过 grunt-newer 跳过未改动文件,防止在 Monorepo 下全量扫描导致 10 s+ 延迟。
  • sourcemap 影响:若公司规范要求上线 .map,需把 .map 体积一并算入,否则会被审计部门视为“隐藏体积”。

答案

  1. 安装依赖
npm i -D grunt-cssnano cssnano-preset-advanced gzip-size pretty-bytes
  1. Gruntfile.js 关键片段
module.exports = function(grunt) {
  const zlib = require('zlib');
  const prettyBytes = require('pretty-bytes');

  grunt.initConfig({
    cssnano: {
      prod: {
        options: {
          preset: ['advanced', {
            // 强制合并 @media
            mergeRules: true,
            // 关闭可能拆查询的优化
            colormin: false,
            // 保留关键注释,方便审计
            discardComments: { removeAll: true, exclude: /^\/*!/ }
          }]
        },
        files: { 'dist/app.min.css': 'src/css/*.css' }
      }
    },

    // 自定义任务:计算节约字节
    bytesave: {
      report: function() {
        const raw = grunt.file.read('src/css/app.css');
        const min = grunt.file.read('dist/app.min.css');
        const rawGzip = zlib.gzipSync(raw).length;
        const minGzip = zlib.gzipSync(min).length;
        const saved = rawGzip - minGzip;
        grunt.log.writeln(
          `**@media 合并后 gzip 体积减少:${prettyBytes(saved)}**`
        );
        // 写入 CI 环境变量,供后续步骤发评论
        if (process.env.CI) {
          require('fs').appendFileSync(
            process.env.GITLAB_ENV,
            `CSS_SAVED=${saved}\n`
          );
        }
      }
    }
  });

  grunt.loadNpmTasks('grunt-cssnano');
  grunt.registerTask('default', ['cssnano:prod', 'bytesave:report']);
};
  1. 运行结果示例
Running "cssnano:prod" (cssnano) task
>> 1 file created 28.3 kB → 19.7 kB

Running "bytesave:report" task
**@media 合并后 gzip 体积减少:5.38 kB**
  1. 集成到 GitLab CI
script:
  - npm run grunt
  - echo "CSS_SAVED=$CSS_SAVED" >> metrics.txt
artifacts:
  reports:
    metrics: metrics.txt

MR 阶段通过 metrics.txt 自动评论“本次构建 CSS 减少 5.38 kB,约 18.7%”。

拓展思考

  • 若项目使用 PostCSS 8 插件链,可直接用 postcss-merge-rules 替代 grunt-cssnano,并通过 grunt-postcss 接入,此时需把 cssnano 作为其中一环,注意插件顺序merge-rules 必须在 autoprefixer 之后,否则前缀差异会导致无法合并。
  • 对于小程序场景,微信开发者工具对 @media 支持度有限,合并后可能出现“单文件超 2 M 无法上传”的极限 case,此时可给 grunt-cssnano 加 maxSize: 2048 的自定义阈值,超过即自动拆包并报警。
  • 国内云厂商 CDN 计费往往取“gzip 后 95 峰值”,因此节约字节需按 30 天滚动窗口累加,可扩展 bytesave 任务把结果写进 InfluxDB,配合 Grafana 做成本看板,让财务同学直观看到“前端优化省下 1200 元/月”。
  • 面试反向提问:可问面试官“公司是否对 critical CSS 做内联”,若内联比例高,则 @media 合并收益会被稀释,此时应优先内联再合并,避免过度优化;体现候选人对业务价值的深度思考。