webpack性能优化

182 阅读5分钟

分析影响打包速度

打包就是从入口文件开始将所有的依赖模块打包到一个文件中的过程。

在webpack构建过程中,实际上耗费时间大多数用在 loader 解析转换以及代码的压缩中。

搜索时间

开始打包时,我们需要获取所有的依赖模块。搜索所有的依赖项,这需要占用一定的时间,即搜索时间。

1. 优化 loader 配置

目的:筛选资源,符合条件的资源让这项规则中的loader处理,减少搜索时间。

配置:

  • Rule.test:筛选资源
  • Rule.include:符合条件的资源
  • Rule.exclude:需要排除在外的资源
  • Rule.issuer:匹配引入的资源
module.exports = {
    module: {
        rules: [
            {
                test:/\.css$/, // 字符串
                // test: function (path) { return path.indexOf('.css') > -1 }, // 函数
                include:/\.css$/,
                //include: path.resolve(__dirname, 'src/css'),
                //include:function (content) { //return content.indexOf('src/css') > -1 //},
                exclude:/node_modules/, 
                //exclude: path.resolve(__dirname, 'node_modules'), 
                //exclude:function (content) { //return content.indexOf('node_modules') > -1 //},
                issuer: /\main\.js$/, 
                //issuer: path.resolve(__dirname, 'main.js'),
                //issuer:function (content) { //return content.indexOf('main.js') > -1 //},
                use: ['style-loader', 'css-loader']
            },
        ],
    },
}

2.优化 resolve配置

  • resolve.module 配置: 设置模块导入规则,import/require时会直接在这些目录找文件。默认值是 当前目录['node_modules'],如果没找到就去上一级目录 ../node_modules 中找,以此类推。
module.exports = { 
    //... 
    resolve: { 
        modules: [path.resolve(__dirname, 'src'), 'node_modules',path.join(__dirname, 'src')] 
    }
};
  • resolve.alias 配置: resolve.alias 配置项通过别名来把原导入路径映射成一个新的导入路径,减少耗时的递归解析操作。
module.exports = { 
    //... 
    resolve: { 
        alias: { 
            '@/components': path.resolve(__dirname, 'src/components/'), 
            '@/servers': path.resolve(__dirname, 'src/servers/')
        }
    } 
};

// 传统模式导入组件
import BaseTable from '../../../componets/BaseTable'
// 使用别名导入
import BaseTable from '@/components/BaseTable'
  • resolve.extensions 配置:
    • resolve.extensions 列表要尽可能的小。
    • 频率出现最高的文件后缀要优先放在最前面,可以尽快退出寻找过程。
    • 在源码中写导入语句时,要尽可能的带上后缀,从而可以避免寻找过程。 在导入语句可以省略文件后缀,webpack 会根据 resolve.extension 自动带上后缀后去尝试询问文件是否存在。
module.exports = {
    //... 
    resolve: { 
        extensions: ['.jsx', '.js', '.json'] 
    } 
};
  • resolve.mainFields 配置

有一些第三方模块会针对不同环境提供几分代码,webpack 会根据 mainFields 的配置去决定优先采用那份代码,根据配置数组顺序只采用第一个。

// 分别提供采用 ES5 和 ES6 的2份代码,这2份代码的位置写在 `package.json` 文件里
{ 
    "jsnext:main": "es/index.js",// 采用 ES6 语法的代码入口文件
    "main": "lib/index.js" // 采用 ES5 语法的代码入口文件 
}
// webpack.config.js
module.exports = { 
    //... 
    resolve: {
        // mainFields: ['browser', 'main'], // 默认配置
        mainFields: ['jsnext:main', 'browser', 'main'] // 优先采用ES6 的那份代码 
    }
};

3.添加 module.noParse 配置

module.noParse 配置让 Webpack 忽略对部分没采用模块化的文件的递归解析处理,不进行转化和解析,从而提升 webpack 的构建性能。

// 例如 jquery 、ChartJS, 它们庞大又没有采用模块化标准
module.exports = { 
    //... 
    module:{
        noParse: /jquery|lodash/,  //接收参数  正则表达式 或函数
        // noParse:function(contentPath){
            return /jquery|lodash/.test(contentPath);
        }
    }
};

解析时间

webpack 根据我们配置的 loader 解析相应的文件。日常开发中我们需要使用 loader 对 js ,css ,图片,字体等文件做转换操作。

webpack 是单线程模式的,也就是说,webpack 打包只能逐个文件处理,当 webpack 需要打包大量文件时,打包时间就会比较漫长。

1. 多进程打包 thread-loader

官方:把这个 loader 放置在其他 loader 之前, 放置在这个 loader 之后的 loader 就会在一个单独的 worker 池(worker pool)中运行。在 worker 池(worker pool)中运行的 loader 是受到限制的:

  • 这些 loader 不能产生新的文件。
  • 这些 loader 不能使用定制的 loader API(也就是说,通过插件)。
  • 这些 loader 无法获取 webpack 的选项设置。 每个 worker 都是一个单独的有 600ms 限制的 node.js 进程。同时跨进程的数据交换也会被限制。

总结:如果是小项目,文件不多,无需开启多进程打包,反而会变慢,因为开启进程是需要花费时间的,还有仅在耗时的 loader 上使用。

// 1.安装 npm install thread-loader -D
// 2.配置:webpack.config.js
module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.css$/,
        exclude: /node_modules/,
        // 创建一个 css worker 池
        use: [
          'style-loader',
          'thread-loader',
          {
            loader: 'css-loader',
            options: {
              modules: true,
              localIdentName: '[name]__[local]--[hash:base64:5]',
              importLoaders: 1
            }
          },
        ]
      }
    ]
  }
}

2. happypack

仅做了解,不具体说明,因为happypack 作者 Ahmad Amireh推荐使用 thread-loader,并宣布将不再继续维护 happypack,所以不推荐使用它。

压缩时间

JS 压缩是发布编译的最后阶段,通常 webpack 需要卡好一会,这是因为压缩 JS 需要先将代码解析成 AST 语法树,然后需要根据复杂的规则去分析和处理 AST,最后将 AST 还原成 JS,这个过程涉及到大量计算,因此比较耗时,打包就容易卡住。

1. terser-webpack-plugin

terser-webpack-plugin 内部封装了 terser 库,用于处理 js 的压缩和混淆,通过 webpack plugin 的方式对代码进行处理(老牌工具uglify不支持es6,且uglify-es不再更新) 。

注意webpack4默认使用了terser-webpack-plugin插件,并默认开启了多进程和缓存。 另外还提供如下参数:

  • test: 匹配文件
  • include:要包含的文件
  • exclude: 要排除的文件
  • cache: 启用/禁用文件缓存,默认值为true,缓存目录的默认路径是node_modules/.cache/terser-webpack-plugin
  • parallel:并行运行来提高构建速度,默认值为true。(并行化可以显著提高构建速度,因此强烈建议使用。)
  • sourceMap:使用源映射将错误消息位置映射到模块,默认值false,建议仅在开发环境开启。
// 1.安装 npm install terser-webpack-plugin --save-dev
// 2.配置 webpack.config.js
const TerserPlugin = require('terser-webpack-plugin'); module.exports = { 
    //....
    optimization: { 
        minimize:true, 
        minimizer: [
            new TerserPlugin({
                // ...options
            })
        ], 
    },
};

延伸了解:多进程压缩插件ParallelUglifyPlugin,项目基本处于没人维护的阶段,不推荐;webpack3中使用的UglifyJsPlugin,在webpack4中已被废弃;

二次打包时间

当项目中有文件更改时,我们需要重新打包,所有的文件都必须要重新打包,需要花费同初次打包相同的时间,但项目中大部分文件都没有变更,尤其是第三方库。所以可以合理利用缓存。

例如:

  • cache-loader
  • HardSourceWebpackPlugin

所有这些缓存方法都有启动的开销。重新运行期间在本地节省的时间很大,但是初始(冷)运行实际上会更慢。

1. cache-loader

配置很简单,仅仅需要在一些性能开销较大的 loader 之前添加此 loader,显著提升二次构建速度。因为保存和读取这些缓存文件会有一些时间开销,所以只对性能开销较大的 loader 使用此 loader。

缓存默认的存放路径是: node_modueles/.cache/cache-loader 目录下。

// 1.安装:npm install cache-loader --save-dev
// 2.配置
module.exports = {
  module: {
    rules: [
      {
        test: /\.jsx$/,
        use: ['cache-loader', ...loaders],
      },
    ],
  },
};

2. hard-source-webpack-plugin

  • 第一次构建:不能提升速度
  • 第二次构建:将显着加快(大概提升90%的构建速度)。

(webpack v5 实现了此功能) 注意:每当程序包依赖性发生变化时,请记住清除缓存

缓存默认的存放路径是: node_modules/.cache/hard-source

// 1.安装:npm install hard-source-webpack-plugin --save-dev
// 2.配置
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
module.exports = {
  // ......  
  plugins: [
    new HardSourceWebpackPlugin()  
  ]
}

体积优化

1. 分离公共资源-SplitChunks

SplitChunks插件将公共依赖项提取出来,单独打包,这样公共代码只需要下载一次就缓存起来了,避免了重复下载。

webpack4内置了SplitChunksPlugin,通过optimization.splitChunks配置来实现。

  • chunks:表示从哪些chunks里面抽取代码,值为initial、async、all或者函数
  • cacheGroups:缓存组(关键配置项)
  • priority:优先级配置项
  • minSize:默认30KB,表示抽取出来的文件在压缩前的最小size。
  • maxSize:默认为0,表示抽取出来的文件在压缩前的最大size。
  • minChunks:默认为1,表示被引用次数。
  • name:抽取出来文件的名字,默认为true,表示自动生成文件名。

minSize默认最小是30KB时,如果公共代码小于30KB的话,就不会被分割成一个单独的文件,因为如果分割的话并不会带来性能的提升,反而使浏览器多了一次请求。

// 抽离出react代码
module.exports = {
  // ......  
  optimization: {
     splitChunks:{
        chunks:'all',
        cacheGroups:{
         react:{
            test:/(react)/,
            chunks:'initial',
            name:'reactBase',
            priority:10,
         },
         //default:{
         //   minChunks:2,
         //   priority:-10,
         //}
        }
     }
  }
}

延伸了解,webpack 4.x以前使用CommonsChunkPlugin

2. Tree-shaking

tree-shaking技术帮我们把一些没有使用到的代码去除,这些代码不会包含在最终生成的包里面,可以优化包体积。

名词解释 DCE ( dead-code elimination):

去除不影响执行结果的代码,包括不会执行到的代码和未使用的变量等。

// 处理前
var a = 1;
function b(){
    return 'b';
    return 'b2';
}
var c = b();
// 处理后
function b(){
    return 'b';
}
b();

webpack中实现DCE依靠的是代码压缩工具terser(webapck4之前用uglify-es)。

tree-shaking配置:

// 下面`./math.js`模块导出的函数`square`并没有被使用,因此打包的时候会将其删除。

// ./index.js(入口文件)
import { cube, square } from './math';
console.log(cube(2));

// ./math.js
export function square(x) {
  return x * x;
}
export function cube(x) {
  return x * x * x;
}

除了webpack的默认配置,还可以通过package.json的sideEffects字段来配置。在打包阶段,webpack无法准确判断某个文件是否有副作用,所以默认认为所有文件都是有副作用的。

sideEffects可选值如下:

  • true(默认),都有副作用
  • false,都没有副作用
  • 文件列表,列表中的文件有副作用,其他没有。