likes
comments
collection
share

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

作者站长头像
站长
· 阅读数 16

前言

笔者最近在整理关于 webpack 相关的知识点,一方面是因为自己掌握的知识点比较零碎、不够系统,有时候碰到问题不知从何下手,另外一方面 webpack5.0 已经在路上了,这的确是一个让人头秃的消息。

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

所以这就促使了我去系统性的回顾了一遍 webpack4.0 的所有知识点,包括 webpack 的由来,各种配置的使用、性能优化、Webpack 的底层原理、相关脚手架的配置分析,都回顾了一波,大致目录如下图:

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

笔者把系列的文章都扔在了这个仓库:webpack 学习整理文档,有兴趣的同学可以去看一波。

今天这篇文章也是笔者就学习文档中的性能优化这一块内容做的整理与回顾。

文章中使用到的案例代码链接放在了最底部,大家自取。

 

为什么要优化

先来说说为什么要优化?当然如果你的项目很小,构建很快,其实不需要特别关注性能方面的问题。

但是随着项目涉及到的页面越来越多,功能和业务代码也会越来越多,相应的 webpack 的构建时间也会越来越久,这个时候我们就不得不考虑性能优化的事情了。

因为这个构建时间与我们的日常开发是密切相关,当我们本地开发启动 devServer 或者 build 的时候,如果时间过长,会大大降低我们的工作效率。

试想一个场景,我们突然碰到一个紧急 bug,项目启动需要花费 3/4 分钟,改完后项目 build 上线也要 3/4 分钟,这个时候脑瓜是不是 duangduangduang...

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

那接下来我们看一下如何优化 webpack 的性能,提升 webpack 的构建速度。

 

分析工具

在动手优化之前,我们需要有一个量化的指标,得知道影响构建时间的问题究竟出在哪里,是某个 chunk 文件太大了,还是哪一个 loader 或者 plugin 耗时太久了等等。

我们可以对通过一些工具对项目进行相应的 体积速度 分析, 然后对症下药。

体积分析

初级分析

可以通过官方提供的 stat.json 文件帮助我们分析打包结果,stat.json 文件可以通过下面语句快速生成:

webpack --profile --json > stats.json

接着我们通过官网提供的 stats.json 分析工具 进行分析,上传 stats.json 文件之后,就可以得到如下图所示分析结果:

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

其中包括 webpack 的版本、打包时间、打包过程的 hash 值、模块数量( modules )、chunk 数量、打包生层的静态文件 assets 以及打包的警告和错误数。

我们可以分析其提供的内容,进行大致问题的定位。

第三方工具

webpack-bundle-analyzer 是打包分析神器,它的界面个人觉得很好看,而且能很直观的给出每一个打包出来的文件的大小以及各自的依赖,能够更加方便的帮助我们对项目进行分析。

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

使用如下:

// config/webpack.common.js
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

const commonConfig = {
  // ...
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerPort: 8889, // 指定端口号
      openAnalyzer: false,
    }),
  ]
  // ...
}

webpack-bundle-analyzer 其底层也是依赖 stat.json 文件的,通过对 stat.json 的分析,得出最后的分析页面

通过分析工具的分析,我们可以知道哪些文件耗时比较多,打包出来的体积比较大,从而对有问题的文件进行优化。

 

速度分析

我们可以通过 speed-measure-webpack-plugin 这个插件帮助我们分析整个打包的总耗时,以及每一个loader 和每一个 plugins 构建所耗费的时间,从而帮助我们快速定位到可以优化 Webpack 的配置。

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

如上图,耗时比较长的会以红色标出。

使用

引入此插件,创建一个 plugins 实例 smp 包裹 webpack 配置文件即可,我们修改一下 webpack 的公共配置文件 webpack.common.js

// config/webpack.common.js
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
// ...
module.exports = (production) => {
  if (production) {
    const endProdConfig = merge(commonConfig, prodConfig);
    return smp.wrap(endProdConfig);
  } else {
    const endDevConfig = merge(commonConfig, devConfig);
    return smp.wrap(endDevConfig);
  }
};

笔者文章演示的代码配置文件分为三个,分别为 开发环境配置文件生产环境配置文件,以及前二者共用的公共配置文件,如下:

  • webpack.dev.js:开发环境使用的配置文件
  • webpack.prod.js:生产环境使用的配置文件
  • webpack.common.js:公共配置文件

执行打包之后,可以看到如下效果图:

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

注意:speed-measure-webpack-plugin 对于 webpack 的升级还不够完善,暂时还无法与你自己编写的挂载在 html-webpack-plugin 提供的 hooks 上的自定义 Pluginadd-asset-html-webpack-plugin 就是此类)共存,有人已经在 github 上提了 issue 了,但是貌似还是没有解决。

 

优化策略

经过相应的体积分析和速度分析之后,我们便可以着手进行优化了。

使用新版本

这个是 webpack 性能优化的万能膏药,升级版本必定能带来性能提升,而且提升很明显。

我们可以看一张对比图:

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

从上图中我们可以看到,webpack4.0 的构建速度远远快于 webpack3.0,官方也说升级之后,升级版本之后,构建时间可以降低 60% - 98% 左右。

在每一个版本的更新,webpack 内部肯定会做很多优化,而 webpack 是依赖 Nodejs 运行环境,升级他们对应的版本,webpack 的速度肯定也能够获得提升。

说不定在 webpack5.0 出来之后,我们今天讲到的大部分性能优化方法都会被集成到 webpack 自身中去,我们只需要通过几个简单的配置就能完成性能配置。

同时新版本的包管理工具(NpmYarn)也可以更快的帮助我们分析一些包的依赖和引入,从而提高打包速度。

webpack4.0 带来的优化

  • v8 引擎带来的优化(for of 替代 forEachMapSet 替代 Objectincludes 替代 indexOf
  • 默认使用更快的 md4 hash 算法
  • webpack AST 可以直接从 loader 传递给 AST,减少解析时间
  • 使用字符串方法替代正则表达式

我们可以在 github 上的 webpack 库的 releases 版本迭代 页面中查看其带来的性能优化:

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

一个 v8 性能优化例子:

我们可以来看一个例子,比较使用 includes 替代 indexOf 之后带来的速度提升,创建 compare-includes-indexof.js 文件,在这个文件中建一个 10000000 长度的数组,记录两个函数分别消耗的时间:

const ARR_SIZE = 10000000;
const hugeArr = new Array(ARR_SIZE).fill(1);

// includes
const includesTest = () => {
  const arrCopy = [];
  console.time('includes')
  let i = 0;
  while (i < hugeArr.length) {
    arrCopy.includes(i++);
  }
  console.timeEnd('includes');
}

// indexOf
const indexOfTest = () => {
  const arrCopy = [];
  console.time('indexOf');
  for (let item of hugeArr) {
    arrCopy.indexOf(item);
  }
  console.timeEnd('indexOf');
}

includesTest();
indexOfTest();

可以发现 includes 的速度远远快于 indexOf

  • includes12.224ms
  • indexOf147.638ms

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

所以在项目上尽可能使用比较新的 webpackNodeNpmYarn 版本,是我们提升打包速度的第一步。

 

体积优化

webpack 是个项目打包工具,一般项目打完包以后,需要发布到服务器上供用户使用,为了用户体验,我们的项目体积需要越小越好,所以 webpack 中打包的体积是 webpack 中重要的一环。

js 压缩

webpack4.0 默认在生产环境的时候是支持代码压缩的,即 mode=production 模式下。

实际上 webpack4.0 默认是使用 terser-webpack-plugin 这个压缩插件,在此之前是使用 uglifyjs-webpack-plugin,两者的区别是后者对 ES6 的压缩不是很好,同时我们可以开启 parallel 参数,使用多进程压缩,加快压缩。

// config/webpack.common.js
const TerserPlugin = require('terser-webpack-plugin');
// ...
const commonConfig = {
  // ...
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: 4, // 开启几个进程来处理压缩,默认是 os.cpus().length - 1
      }),
    ],
  },
  // ...
}

CSS 压缩

压缩 CSS

我们可以借助 optimize-css-assets-webpack-plugin 插件来压缩 css,其默认使用的压缩引擎是 cssnano。 具体使用如下:

// config/webpack.prod.js
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
// ...
const prodConfig = {
  // ...
  optimization: {
    minimizer: [
      new OptimizeCSSAssetsPlugin({
        assetNameRegExp: /\.optimize\.css$/g,
        cssProcessor: require('cssnano'),
        cssProcessorPluginOptions: {
          preset: ['default', { discardComments: { removeAll: true } }],
        },
        canPrint: true,
      })
    ]
  },
}
擦除无用的 CSS

使用 PurgeCSS 来完成对无用 css 的擦除,它需要和 mini-css-extract-plugin 配合使用。

// config/webpack.common.js
const PurgecssPlugin = require('purgecss-webpack-plugin');
// ...
const PATHS = {
  src: path.join(__dirname, './src')
};

const commonConfig = {
  // ...
  plugins: [
    // ...
    new PurgecssPlugin({
      paths: glob.sync(`${PATHS.src}/**/*`,  { nodir: true }),
    }),
  ]
  // ...
}

在未使用此插件之前,比如我们只用到了 navcontact 这个类,其他的都没有用到,我们在未引入之前打包一下,发现未用到的 css 还是会被打包进去:

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

引入插件后,重新进行打包,发现没有用到的 css 都被擦除了:

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

更多使用大家可参考 PurgeCSS 文档

图片压缩

一般来说在打包之后,一些图片文件的大小是远远要比 js 或者 css 文件要来的大,所以我们首先要做的就是对于图片的优化,我们可以手动的去通过线上的图片压缩工具,如 tiny png 帮我们来压缩图片。

但是这个比较繁琐,在项目中我们希望能够更加自动化一点,自动帮我们做好图片压缩,这个时候我们就可以借助 image-webpack-loader 帮助我们来实现。它是基于 imagemin 这个 Node 库来实现图片压缩的。

使用很简单,我们只要在 file-loader 之后加入 image-webpack-loader 即可:

// config/webpack.common.js
// ...
module: {
  rules: [
    {
      test: /\.(png|jpg|gif)$/,
      use: [
        {
          loader: 'file-loader',
          options: {
            name: '[name]_[hash].[ext]',
            outputPath: 'images/',
          }
        },
        {
          loader: 'image-webpack-loader',
          options: {
            // 压缩 jpeg 的配置
            mozjpeg: {
              progressive: true,
              quality: 65
            },
            // 使用 imagemin**-optipng 压缩 png,enable: false 为关闭
            optipng: {
              enabled: false,
            },
            // 使用 imagemin-pngquant 压缩 png
            pngquant: {
              quality: '65-90',
              speed: 4
            },
            // 压缩 gif 的配置
            gifsicle: {
              interlaced: false,
            },
            // 开启 webp,会把 jpg 和 png 图片压缩为 webp 格式
            webp: {
              quality: 75
            }
          }
        }
      ]
    },
  ]
}         
// ...

我们先不使用这个 loader 打包一下,图片大小是 2.1MB

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

使用 image-webpack-loader 之后,图片大小是 666KB

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

压缩的效果还是很明显的。

拆分代码

有时候我们写的某些模块根本没有使用,但是还是被打包了,这样实际上会拖累 webpack 的打包速度,而且也会增加打包文件的体积,所以我们可以使用 tree-shaking 将这些代码剔除掉。

或者也可以使用 splitChunksPlugin 把一个大的文件分割成几个小的文件,这样也可以有效的提升 webpack 的打包速度,详细的配置介绍大家可以看笔者写的 配置 SplitChunksPlugin,里面详细介绍了怎么配置 splitChunks,以及各参数的用法与意义,在这里就不展开讲了。

 

速度优化

讲完打包体积的优化,我们来看一下在速度方面的优化。

分离两套配置

一般来说在项目开发中,我们会区分开发和生产环境两套配置,各司其职。

在开发阶段:我们需要 webpack-dev-server 来帮我们进行快速的开发,同时需要 HMR 热更新 帮我们进行页面的无刷新改动,而这些在 生产环境 中都是不需要的。

在生产阶段:我们需要进行 代码压缩目录清理计算 hash提取 CSS 等等;

实现起来很简单,我们前面也提到过,就新建三个 webpack 的配置文件就行:

  • webpack.dev.js:开发环境的配置文件
  • webpack.prod.js:生产环境的配置文件
  • webpack.common.js:公共配置文件

通过 webpack-merge 来整合两个配置文件共同的配置 webpack.common.js,具体可以参照源码。

 

减少查找过程

webpackresolve 参数进行合理配置,使用 resolve 字段告诉 webpack 怎么去搜索文件。

合理使用 resolve.extensions

在导入语句没带文件后缀时,webpack 会自动带上后缀后去尝试询问文件是否存在,查询的顺序是按照我们配置 的 resolve.extensions 顺序从前到后查找,webpack 默认支持的后缀是 jsjson

举个🌰:如果我们配置 resolve.extensions= ['js', 'json'],那么 webpack 会先找 xxx.js

如果没有则再查找 xxx.json,所以我们应该把常用到的文件后缀写在前面,或者 我们导入模块时,尽量带上文件后缀名。

虽然 extensions 会优先查找数组内的值,但是我们不要一股脑儿的把所有后缀都往里面塞,这会调用多次文件的查找,这样就会减慢打包速度。

优化 resolve.modules

这个属性告诉 webpack 解析模块时应该搜索的目录,绝对路径和相对路径都能使用。使用绝对路径之后,将只在给定目录中搜索,从而减少模块的搜索层级:

// config/webpack.common.js
// ...

const commonConfig = {
  // ...
  resolve: {
    extensions: ['.js', '.jsx'],
    mainFiles: ['index', 'list'],
    alias: {
      alias: path.resolve(__dirname, '../src/alias'),
    },
    modules: [
      path.resolve(__dirname, 'node_modules'), // 指定当前目录下的 node_modules 优先查找
      'node_modules', // 将默认写法放在后面
    ]
  },
  // ...
}
// ...
使用 resolve.alias 减少查找过程

alias 的意思为 别名,能把原导入路径映射成一个新的导入路径。

比如我们项目中可能会有一些相对路径的写法,就可以使用 alias 配置来减少查找过程;

还比如我们经常使用的 react 库,其实我们可以直接使用其 dist 目录下打包好的 react.min.js,这样就能跳过耗时的模块解析,具体示例配置如下:

// config/webpack.common.js
// ...
const commonConfig = {
  // ...
  resolve: {
    // ...
    alias: {
      react: path.resolve(__dirname, './node_modules/react/dist/react.min.js'),
      @alias: path.resolve(__dirname, '../src/alias'),
    },
  },
  // ...
}
// ...

这个笔者在实际项目中没有尝试过,不过也是一个思路,大家有机会可以尝试一波。

 

缩小构建目标

排除 Webpack 不需要解析的模块,即使用 loader 的时候,在尽量少的模块中去使用。

我们可以借助 includeexclude 这两个参数,规定 loader 只在那些模块应用和在哪些模块不应用。

我们修改公共配置文件 webpack.common.js

// config/webpack.common.js
// ...
const commonConfig = {
  // ...
  module: {
    rules: [
      { 
        test: /\.js|jsx$/, 
        exclude: /node_modules/,
        include: path.resolve(__dirname, '../src'),
        use: ['babel-loader']
      },
      // ...
    ]
  },
}
// ...

首先我们不加 excludeinclude 两个参数,打包一下 npm run build,打包时间 3350ms 左右:

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

接着我们加上这两个参数,意思分别是:

  • exclude: /node_modules/:排除 node_modules 下面的文件
  • include: path.resolve(__dirname, '../src'):只对 src 下面的文件使用

重新打包一下,打包时间变成了 1400ms 左右:

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

 

利用多线程提升构建速度

由于运行在 Node.js 之上的 webpack 是单线程模型的,所以 webpack 需要处理的事情需要一件一件的做,不能多件事一起做。

如果 webpack 能同一时间处理多个任务,发挥多核 CPU 电脑的威力,那么对其打包速度的提升肯定是有很大的作用的。

HappyPack

原理:每次 webapck 解析一个模块,HappyPack 会将它及它的依赖分配给 worker 线程中。处理完成之后,再将处理好的资源返回给 HappyPack 的主进程,从而加快打包速度。

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

webpack4.0 中使用 happypack 需要使用其 5.0 版本。

我们将 HappyPack 引入公共配置文件,他的用法就是将相应的 loader 替换成 happypack/loader,同时将替换的 loader 放入其插件的 loaders 选项,我们暂且替换一下 babel-loader

// config/webpack.common.js
// ...
const makePlugins = (configs) => {
  const plugins = [
    // ...
    new HappyPack({
      loaders: ['babel-loader']
    }),
  ];
  // ...
  return plugins;
}
// ...

const commonConfig = {
  entry: {
    main: "./src/index.js",
    entry2: "./src/entry2.js",
    entry3: "./src/entry3.js",
    entry4: "./src/entry4.js",
    entry5: "./src/entry5.js",
    entry6: "./src/entry6.js",
  },
  // ...
  module: {
    rules: [{ 
      test: /\.jsx?$/, 
      // exclude: /node_modules/,
      // include: path.resolve(__dirname, '../src'), 
      use: [
        'happypack/loader'
        // 'babel-loader'
      ]
    }]
  },
  // ...
}
// ...

为了让效果更加明显一点,我们在项目下多增加几个入口文件,在不使用 happypack 的情况下,进行一次打包,时间差不多是 8s 多:

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

开启 happypack 之后,我们可以从控制台中看到,happypack 默认帮我们开启了 3 个进程,打包时间变成了6.5s 左右:

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

注意:HappyPack 的作者现在基本上也不维护这个插件了,因为作者对此项目的兴趣正在减弱。他也推荐我们使用 webpack 官方 thread-loader

更多参数大家可以参考 HappyPack 官网

thread-loader

webpack 官方推出的一个多进程方案,用来替代 HappyPack

原理和 HappyPack 类似,webpack 每次解析一个模块,thread-loader 会将它及它的依赖分配给 worker 线程中,从而达到多进程打包的目的。

使用很简单,直接在我们使用的 loader 之前加上 thread-loader 就行,我们需要先注释掉 HappyPack 代码:

// config/webpack.common.js
// ...
const commonConfig = {
  // ...
  module: {
    rules: [{ 
      test: /\.jsx?$/, 
      // exclude: /node_modules/,
      // include: path.resolve(__dirname, '../src'), 
      use: [
        {
          loader: 'thread-loader',
          options: {
            workers: 3, // 开启几个 worker 进程来处理打包,默认是 os.cpus().length - 1
          }
        },
        'babel-loader'
      ]
    }]
  },
  // ...
}
// ...

我们重新运行一下,也是差不多 6.5s 左右:

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

预先编译资源模块(DllPlugin)

我们在打包的时候,一般来说第三方模块是不会变化的,所以我们想只要在第一次打包的时候去打包一下第三方模块,并将第三方模块打包到一个特定的文件中,当第二次 webpack 进行打包的时候,就不需要去 node_modules 中去引入第三方模块,而是直接使用我们第一次打包的第三方模块的文件就行。

webpack.DllPlugin 就是来解决这个问题的插件,使用它可以在第一次编译打包后就生成一份不变的代码供其他模块引用,这样下一次构建的时候就可以节省开发时编译打包的时间。

添加配置文件

我们在配置文件目录 config 下新建一个 webpack.dll.js,此文件用于将我们的第三方包文件打包到 dll 文件夹中去:

// config/webpack.dll.js
const path = require('path');
const webpack = require('webpack');

module.exports = {
  mode: 'production', // 环境
  entry: {
    vendors: ['lodash'], // 将 lodash 打包到 vendors.js 下
    react: ['react', 'react-dom'], // 将 react 和 react-dom 打包到 react.js 下
  },
  output: {
    filename: '[name].dll.js', // 输出的名字
    path: path.resolve(__dirname, '../dll'), // 输出的文件目录
    library: '[name]' // 将我们打包出来的文件以全部变量的形式暴露,可以在浏览器变量的名字进行访问
  },
  plugins: [
    // 对生成的库文件进行分析,生成库文件与业务文件的映射关系,将结果放在 mainfest.json 文件中
    new webpack.DllPlugin({
      name: '[name]', // 和上面的 library 输出的名字要相同
      path: path.resolve(__dirname, '../dll/[name].manifest.json'),
    })
  ]
}
  • 上面的 library 的意思其实就是将 dll 文件以一个全局变量的形式导出出去,便于接下来引用,如下图:
  • mainfest.json 文件是一个映射关系,它的作用就是帮助 webpack 使用我们之前打包好的 ***.dll.js 文件,而不是重新再去 node_modules 中去寻找。

我们在命令行中打包一下 dll 文件,可以看到根目录生成了一个 dll 文件夹,并且在下面生成了相应的文件,并且 loader 打包到了 vendor.dll.js 中,reactreact-dom 打包到了 react.dll.js 中了:

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

接着我们需要去修改公共配置文件 webpack.common.js,将我们之前生成的 dll 文件导入到 html 中去,如果我们不想自己手动向 html 文件去添加 dll 文件的时候,我们可以借助一个插件 add-asset-html-webpack-plugin,此插件顾名思义,就是将一些文件加到 html 中去。

同时我们需要使用 webpack 自带的 DllReferencePlugin 插件对 mainfest.json 映射文件进行分析。

// config/webpack.common.js
const webpack = require('webpack');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');

// ...

const commonConfig = {
  // ...
  plugins: [
    // ...
    new AddAssetHtmlWebpackPlugin({
      filepath: path.resolve(__dirname, '../dll/vendors.dll.js')
    }),
    new AddAssetHtmlWebpackPlugin({
      filepath: path.resolve(__dirname, '../dll/react.dll.js')
    }),
    new webpack.DllReferencePlugin({
      manifest: require(path.resolve(__dirname, '../dll/vendors.dll.mainfest.json'))
    }),
    new webpack.DllReferencePlugin({
      manifest: require(path.resolve(__dirname, '../dll/react.dll.mainfest.json'))
    }),
  ],
  // ...
}
// ...

这里的代码还可以优化,具体大家可以参考笔者整理的笔记中 dll优化 这一节。

我们进行一次打包,可以看到打包耗时为 1450ms 左右,同时可以看到库文件打包到的 vendors.chunk.js1.22MB

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

我们注释掉对 dll 的引用分析之后,重新打包,打包耗时为 1950ms 左右,同时可以看到 vendors.chunk.js5.28MB

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

缓存 Cache 相关

我们可以开启相应 loader 或者 plugin 的缓存,来提升二次构建的速度。一般我们可以通过下面几项来完成:

如果项目中有缓存的话,在 node_modules 下会有相应的 .cache 目录来存放相应的缓存。

babel-loader

首先我们开启 babel-loader 的缓存,我们修改 babel-loader 的参数,将参数 cacheDirectory 设置为 true

// config/webpack.common.js
// ...
module: {
  rules: [
    { 
      test: /\.jsx?$/, 
      // exclude: /node_modules/,
      // include: path.resolve(__dirname, '../src'), 
      use: [
        {
          loader: 'babel-loader',
          options: {
            cacheDirectory: true,
          }
        },
      ]
    },
  ]
}         
// ...

首次打包时间为 8.5s 左右,打包完成之后,我们可以发现在 node_modules 下生成了一个 .cache 目录,里面存放了 babel 的缓存文件:

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

我们重新打包一次,会发现时间变成了 6s 左右:

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

TerserPlugin

我们通过将 TerserPlugin 中的 cache 设为 true,就可以开启缓存:

// config/webpack.common.js
const TerserPlugin = require('terser-webpack-plugin');
// ...
const commonConfig = {
  // ...
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: 4, // 开启几个进程来处理压缩,默认是 os.cpus().length - 1
        cache: true,
      }),
    ],
  },
  // ...
}

首次打包时间为 8-9s 左右,同时在 .cache 目录下生成了 terser-webpack-plugin 缓存目录:

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

我们重新打包一次,会发现时间变成了 5s 左右:

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

HardSourceWebpackPlugin

这个插件其实就是用于给模块提供一个中间的缓存。

使用如下,我们直接在插件中引入就 ok 了:

// config/webpack.common.js
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
// ...
const plugins = [
  // ...
  new HardSourceWebpackPlugin(),
];
// ...

我们打包一下,可以看到在第一次打包的时候 HardSourceWebpackPlugin 就帮我们开始生成打包文件了,同时在 .cache 目录生成了 hard-source 目录,第一次打包耗时 6.6s 左右:

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

我们重新打包一次,会发现时间变成了 2.7s 左右:

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

合理使用 sourceMap

之前我们有讲过,之前我们打包生成 sourceMap 的时候,如果信息越详细,打包速度就会越慢

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

所以我们要在代码打包过程中的时候,在对应的环境使用对应的 sourceMap 很重要。

 

其他

除了上述我们提到的一些常用方法,还有其他的一些方法,比如:

  • 使用 ES6 Modules 语法,以保证 Tree-Shaking 起作用

因为 tree-shaking 只对 ES6 Modules 静态引入生效,对于类似于 CommonJs 的动态引入方式是无效的

  • 合理使用 Ployfill

如果我们对于引入的 polyfill 不做处理的话,Webpack 会把所有的 Polyfill 都加载进来,导致产出文件过大。推荐使用 @babel/preset-envuseBuiltIns='usage' 方案,此配置项会根据浏览器的兼容性帮助我们按需引入所需的垫片;此外我们也可以使用动态 polyfill 服务,每次根据浏览器的 User Agent,下发不同的 Polyfill,具体可以参考 polyfill.io

  • 预加载资源 webpackPrefetch

使用 webpackPrefetch 来提前预加载一些资源,意思就是 将来可能需要一些模块资源,在核心代码加载完成之后带宽空闲的时候再去加载需要用到的模块代码。

  • icon 类图片使用 css Sprite 来合并图片

如果 icon 类图片太多的话,就使用雪碧图合成一张图片,减少网络请求,或者使用字体文件。

  • html-webpack-externals-plugin

此插件可以将一些公用包提取出来使用 cdn 引入,不打入 bundle 中,从而减少打包文件大小,加快打包速度。

  • 合理配置 chunk 的哈希值

在生产环境打包,一定要配置文件的 hash,这样有助于浏览器缓存我们的文件,当我们的代码文件没变化的时候,用户就只需要读取浏览器缓存的文件即可。一般来说 javascript 文件使用 [chunkhash]css 文件使用 [contenthash]、其他资源(例如图片、字体等)使用 [hash]

更多性能优化方法笔者就不再一一列举了,因为关于 webpack 性能优化的方法实在是太多了,大家可以根据实际遇到的问题制定相关的优化方案。

 

小结

今天这篇文章介绍了 webpack 打包的一些优化方案,从项目体积再到对项目速度,我们提出了一些优化方案,大家可以在具体的项目中去进行实践。

当然我还要提一嘴,如果你的项目本身构建就比较快,那么你其实就不需要使用文章中提到的方法去对项目进行优化,可能效果会适得其反。

对于文章中有些一笔带过的内容,大家可以在我的 webpack 学习整理文档 找到相应的介绍。

实不相瞒,想要个赞!

浅谈 webpack 性能优化(内附巨详细 webpack 学习笔记)

 

相关链接

 

示例代码

示例代码可以看这里: