一、为什么我的npm脚本跑得比蜗牛还慢?

每次运行npm脚本时,是不是总觉得等待时间长得能泡杯咖啡?特别是当项目逐渐变大时,那些原本秒级的任务突然变成了分钟级。这背后其实隐藏着几个常见原因:

首先,依赖安装可能是罪魁祸首。想象一下,当你的项目有几百个依赖时,每次npm install就像是在组装一台复杂的机器,每个零件都需要精确到位。例如:

// package.json片段
{
  "dependencies": {
    "lodash": "^4.17.21",  // 常用工具库
    "react": "^18.2.0",    // 前端框架
    "webpack": "^5.88.2"   // 构建工具
    // ...还有50+其他依赖
  }
}

其次,脚本本身的编写方式也很关键。比如下面这个常见的构建脚本:

// package.json中的脚本
{
  "scripts": {
    "build": "webpack --mode production && node build-helper.js"
  }
}

这种串行执行的方式意味着前一个任务必须完全结束后才会开始下一个,效率自然低下。

二、揪出拖慢性能的"元凶"

要解决问题,首先得知道问题出在哪。这里有几个实用的诊断方法:

  1. 使用time-prefix来测量脚本执行时间:
# 在package.json中添加时间测量
{
  "scripts": {
    "build": "node -e \"console.time('build')\" && webpack --mode production && node -e \"console.timeEnd('build')\""
  }
}
  1. 用--verbose标志获取详细日志:
npm run build --verbose
  1. 试试优秀的speed-measure-webpack-plugin:
// webpack.config.js
const SpeedMeasurePlugin = require("speed-measure-plugin");

module.exports = new SpeedMeasurePlugin().wrap({
  // 你原来的webpack配置
});

通过这些工具,你可能会发现:

  • 90%的时间花在了babel转译上
  • node_modules体积过大导致I/O瓶颈
  • 某些插件处理了不必要的文件

三、让npm脚本飞起来的优化技巧

3.1 并行化一切可能

把串行任务改为并行可以大幅提升速度。使用npm-run-all这个神器:

// 优化后的package.json
{
  "scripts": {
    "build": "run-p build:*",
    "build:js": "webpack --mode production",
    "build:css": "node build-css.js",
    "build:assets": "node copy-assets.js"
  }
}

3.2 缓存是性能的银弹

充分利用缓存机制:

// 使用hard-source-webpack-plugin
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');

module.exports = {
  plugins: [new HardSourceWebpackPlugin()]
};

// 或者配置babel-loader缓存
{
  test: /\.js$/,
  loader: 'babel-loader',
  options: {
    cacheDirectory: true
  }
}

3.3 按需加载依赖

检查你的依赖是否真的都需要:

# 使用depcheck找出无用依赖
npx depcheck

# 输出示例:
Unused dependencies
* jquery
* outdated-pkg

3.4 升级到最新工具链

比较一下不同包管理器的安装速度:

# 使用npm
time npm install

# 使用yarn
time yarn install

# 使用pnpm
time pnpm install

在我的测试中,pnpm通常比npm快2-3倍,因为它采用了内容寻址存储。

四、高级玩家必备的终极优化

4.1 自定义解析器加速require

// 使用@cspotcode/source-map-support加速sourcemap加载
require('@cspotcode/source-map-support/register');

// 自定义模块解析缓存
const Module = require('module');
const originalRequire = Module.prototype.require;
const requireCache = new Map();

Module.prototype.require = function(id) {
  if (requireCache.has(id)) {
    return requireCache.get(id);
  }
  const result = originalRequire.call(this, id);
  requireCache.set(id, result);
  return result;
};

4.2 利用worker_threads并行处理

// worker-utils.js
const { Worker } = require('worker_threads');

function runService(workerData) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./worker.js', { workerData });
    worker.on('message', resolve);
    worker.on('error', reject);
    worker.on('exit', (code) => {
      if (code !== 0) reject(new Error(`Worker stopped with exit code ${code}`));
    });
  });
}

4.3 使用ESBuild作为转换器

// webpack.config.js
const { ESBuildMinifyPlugin } = require('esbuild-loader');

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: 'esbuild-loader',
        options: {
          loader: 'jsx',  // 支持JSX
          target: 'es2015'
        }
      }
    ]
  },
  optimization: {
    minimizer: [
      new ESBuildMinifyPlugin({
        target: 'es2015'
      })
    ]
  }
};

五、不同场景下的优化策略

5.1 小型项目

  • 使用npm script的基础优化即可
  • 关注依赖数量控制
  • 简单的缓存策略

5.2 中型项目

  • 必须引入并行处理
  • 需要构建缓存
  • 考虑使用更快的打包工具如vite

5.3 大型企业级项目

  • 需要分布式构建
  • 可能需要自定义解析器
  • 应该考虑增量构建策略
  • 可能需要拆分子项目

六、避坑指南与注意事项

  1. 缓存一致性问题:
// 确保缓存键包含环境变量
new HardSourceWebpackPlugin({
  environmentHash: {
    root: process.cwd(),
    directories: ['config'],
    files: ['package.json', '.env'],
  }
})
  1. 并行任务资源竞争:
# 限制并行任务数量
run-p --max-parallel 4 build:*
  1. 版本锁定很重要:
{
  "resolutions": {
    "babel-loader": "8.1.0"
  }
}
  1. CI环境特殊处理:
# .gitlab-ci.yml示例
cache:
  key: ${CI_COMMIT_REF_SLUG}
  paths:
    - node_modules/
    - .cache/

七、总结与未来展望

通过上述方法,我们能够将原本需要5分钟的构建过程缩短到1分钟以内。但性能优化是永无止境的旅程,未来还可以考虑:

  • 探索Rust编写的构建工具如swc
  • 尝试基于ESM的构建流程
  • 评估是否真的需要这么多polyfill
  • 考虑服务端渲染的按需编译

记住,最快的代码是永远不会执行的代码。在优化前,先问问:这个步骤真的有必要吗?