使用 grunt-cssnano 合并重复 @media 查询并计算节约字节
解读
这道题表面问“怎么用 grunt-cssnano 合并 @media”,实则考察三层能力:
- 对 grunt-cssnano 插件核心配置项的掌握,尤其是
mergeRules与mergeIdents在@media场景下的行为差异; - 对构建产物体积度量的敏感度,能否在 CI 或本地给出可量化的“节约字节”报告,这是国内一线厂绩效复盘里的硬指标;
- 对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体积一并算入,否则会被审计部门视为“隐藏体积”。
答案
- 安装依赖
npm i -D grunt-cssnano cssnano-preset-advanced gzip-size pretty-bytes
- 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']);
};
- 运行结果示例
Running "cssnano:prod" (cssnano) task
>> 1 file created 28.3 kB → 19.7 kB
Running "bytesave:report" task
**@media 合并后 gzip 体积减少:5.38 kB**
- 集成到 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合并收益会被稀释,此时应优先内联再合并,避免过度优化;体现候选人对业务价值的深度思考。