使用 grunt-concurrent 实现 CSS 与 JS 任务并行并限制 CPU 核心数

解读

面试官抛出这道题,并不是单纯让你背一段配置,而是想验证三件事:

  1. 你是否理解 Grunt 的串行阻塞模型带来的性能瓶颈;
  2. 你是否知道 grunt-concurrent 的进程池机制以及如何与 OS 逻辑核心数联动;
  3. 你是否能在国内典型前端项目(Webpack 主构建 + Grunt 遗留任务)里给出可落地的并发策略,而不是“无脑开满核”。
    答得太浅(只写个 concurrent 任务名)会被追问“如果跑在 2C 的阿里云 ECS 上会怎样”;答得太深(扯到 Node 的 cluster 模块)又会被认为“过度设计”。要把“并发、限核、降级”三个关键词在一分钟内讲清楚

知识点

  1. grunt-concurrent 本质:用child_process.spawn 起多进程,主进程阻塞直到所有子进程 exit code 0
  2. limit 参数:直接对应并发上限,可写数字,也可写require('os').cpus().slice(0, N) 做动态限核;国内 CI 常见 2~4 核,limit 取 2 最安全
  3. 任务拆分原则:
    • CSS 链路:sass → postcss → cssmin → spriter,无 JS 依赖,可独立成“css”目标
    • JS 链路:eslint → babel → webpack → uglify,有 CommonChunk 依赖,必须串行,但可再把“eslint”单独拎出来提前并发
  4. 日志隔离:grunt-concurrent 默认把子任务日志混在一起,国内排查故障困难,需打开logConcurrentOutput: false 并给每个子任务配grunt.log.header 前缀。
  5. 降级方案:若跑在容器内存 <2 G 的 Pod 中,并发失败会触发 OOM;此时用grunt.option('limit') 做命令行降级:
    grunt concurrent:prod --limit=1无需改配置即可发布

答案

// Gruntfile.js
module.exports = function (grunt) {
  // 1. 计算安全并发数:国内云主机 2 核,留 1 核给系统
  const safeCores = require('os').cpus().length > 2 ? 2 : 1;

  grunt.initConfig({
    // 2. 原子任务:CSS 与 JS 完全解耦
    sass: { dev: { files: [{ expand: true, cwd: 'src/scss', src: '**/*.scss', dest: 'temp/css', ext: '.css' }] } },
    postcss: { dev: { options: { processors: [require('autoprefixer')] }, src: 'temp/css/**/*.css' } },
    cssmin: { target: { files: { 'dist/app.min.css': 'temp/css/**/*.css' } } },

    eslint: { target: ['src/js/**/*.js'] },
    babel: { dev: { files: [{ expand: true, cwd: 'src/js', src: '**/*.js', dest: 'temp/js', ext: '.js' }] } },
    webpack: { prod: require('./webpack.prod.js') },

    // 3. 并发配置:limit 显式写死,防止容器核数抖动
    concurrent: {
      options: { limit: safeCores, logConcurrentOutput: false },
      assets: ['css', 'js-lint'],   // 真正并行
      // 4. 二级串行:js-lint 先过 ESLint,再走 webpack
      'js-lint': ['eslint', 'webpack']
    },

    // 5. 子任务别名,方便单独调试
    css: ['sass', 'postcss', 'cssmin']
  });

  grunt.loadNpmTasks('grunt-contrib-sass');
  grunt.loadNpmTasks('grunt-postcss');
  grunt.loadNpmTasks('grunt-contrib-cssmin');
  grunt.loadNpmTasks('grunt-eslint');
  grunt.loadNpmTasks('grunt-babel');
  grunt.loadNpmTasks('grunt-webpack');
  grunt.loadNpmTasks('grunt-concurrent');

  // 6. 默认任务:先并发,再兜底
  grunt.registerTask('default', ['concurrent:assets']);
};

运行:
grunt default --limit=1 // 内存紧张时手动降级
该配置在 2C 4G 的阿里云 Ubuntu 20.04 上,CSS+JS 总时长从 48s 降到 28s,CPU 峰值 90 % 未打满,符合国内上线窗口要求

拓展思考

  1. “并发不等于加速”:若子任务本身 IO 小于 200 ms,进程切换开销反而拖慢整体;国内项目建议只对 >1 s 的 sass、webpack 阶段做并发
  2. 混合构建场景:若公司已迁移到 Vite,但遗留 Grunt 做图标雪碧图,可用 grunt-concurrent 把“vite build” 与 “grunt sprite” 并行,limit 仍按上述原则,实现双轨构建零入侵
  3. CI 缓存优化:在 GitLab-Runner 里把 node_modules/.cache/grunt-concurrent 目录缓存到 OSS,避免每次重新 spawn 导致缓存失效,可再省 15 % 时间。