webpack5升级指南(附打包性能优化大全)

9,098 阅读6分钟

目录

  • webpack5新特性
  • 升级到webpack5问题汇总
  • 着手优化项
  • 开始优化
    • 使用新增cache属性,缓存进行优化
    • resolve部分优化
    • module 部分优化
    • optimization部分
    • plugin部分
    • 多线程打包
    • 其他优化项
  • 最终结果

1. webpack5新特性

图片.png

  • 增加持久化存储能力,提升构建性能(核心)
  • 提升算法能力来改进长期缓存(降低产物资源的缓存失效率)
  • 提升 Tree Shaking 能力降低产物大小和代码生成逻辑
  • 提升 web 平台的兼容性能力
  • 清除了内部结构中,在 Webpack4 没有重大更新而引入一些新特性时所遗留下来的一些奇怪的 state
  • 通过引入一些重大的变更为未来的一些特性做准备,使得能够长期的稳定在 Webpack5 版本上

2. 升级到webpack5

问题1:命令行env环境对象传参格式改变

原因:在webpack4中使用--env.xx xxx对webpack.config文件进行环境变量传参,在webpack5中会报如下错误

图片.png

解决方案:修改--env.xx xxx 改为 --env xx=xxx可解决

问题2: 使用webpack-dev-server 报错:Cannot find module ‘webpack-cli/bin/config-yargs

解决方案:将webpack-dev-server 改写成 webpack serve 即可,或者修改webpack-cli版本改为3.x也可解决

问题3: 使用babel-loader?cacheDirectory报错

解决方案:

  1. 使用Webpack5新增的cache
  2. 使用Happypack打包cache

问题4:无法使用hard-source-webpack-plugin插件

使用插件会报 TypeError: Cannot read property 'tap' of undefined 解决方案:webpack5废弃了hard-source-webpack-plugin使用,可以使用webpack新增的cache属性替代

问题5:Conflicting values for 'process.env.NODE_ENV'报错

使用new webpack.DefinePlugin 定义NODE_ENV报错

问题6: Can't resolve '../../core-js/object/define-property报错

解决方案:需增module中增加一条rules解决。

module: {

    rules: [{
        test: /\.m?js/, // fix:issue: https://github.com/webpack/webpack/issues/11467
        resolve: {
            fullySpecified: false,
        },
    ...
    ]},

},

问题7:merge is not a function报错

解决方案:需要将const { merge } = require('webpack-merge') 改为const merge = require('webpack-merge')

3. 着手优化项

  • webpack作为前端主流打包工具,配置项错综复杂,刚接手优化就像以下场景,不知道从何下手:

图片.png

可以从以下几个思路进行优化:

思路1:

先确定哪些可以进行优化模块,webpack主要配置(entry、output、resolve、module、performance、externals、module、plugins,其他)进行优化

思路2:

使用包体积检测工具webpack-bundle-analyzer分析包大小,着手优化

图片.png

思路3:

使用打包速度及各个模块检测插件speed-measure-webpack-plugin,分析着手优化

图片.png

4. 开始优化

使用新增cache属性,缓存进行优化(推荐)

  • 在webpack5之前,一般会使用cache-loader将编译结构写入磁盘缓存,或者使用babel-loader?cacheDirectory=true,设置babel编译的结果写进磁盘缓存。

  • webpack5新增的cache属性,会默认开启磁盘缓存,默认将编译结果缓存在 node_modules/.cache/webpack目录下。

  • 当设置 cache.type: "filesystem" 时,webpack 会在内部以分层方式启用文件系统缓存和内存缓存。 从缓存读取时,会先查看内存缓存,如果内存缓存未找到,则降级到文件系统缓存。 写入缓存将同时写入内存缓存和文件系统缓存。

  • 文件系统缓存不会直接将对磁盘写入的请求进行序列化。它将等到编译过程完成且编译器处于空闲状态才会执行。 如此处理的原因是序列化和磁盘写入会占用资源,并且我们不想额外延迟编译过程。

resolve部分优化:

  • externals(推荐):对第三方包进行公共包CDN引用,降低包大小
// 在index.html添加react及jquery的cdn地址,配置外部cdn引用
module.exports = {
  ...
    externals: {
        react: 'React',
        jquery: 'jQuery'
    }
  ...
};
  • resolve.alias:使用别名缩短引用模块路径,降低文件解析成本。(webpack使用enhanced-resolve解析文件路径,如果是相对路径,会先进行文件路径拼接,再进行模块引用,使用alias将不会进行文件路径拼接,并且引用后直接进行缓存:webpack.docschina.org/concepts/mo…)
module.exports = {
    resolve: {
        alias: {
          '@': path.resolve('src'), // @ 代表 src 路径
        },
    }
}
  • resolve.mainFields:减少第三方模块搜索步骤。webpack.target默认为browserslistweb,当targetweb或者webworker时,值是["browser", "module", "main"],这就会导致不必要的检索。由于大多数第三方模块都采用main字段去描述入口文件的位置, 可以直接设置为:
module.exports = {
  resolve: {
    // 只采用 main 字段作为入口文件描述字段,以减少搜索步骤
    mainFields: ['main'],
  },
};
  • 合理配置resolve.extensions检索文件类型
    • 列表值尽量少
    • 频率高的文件类型的后缀写在前面
    • 源码中的导入语句尽可能的写上文件后缀,如require(./data)要写成require(./data.json)
module.exports = {
...
    resolve:{
        extensions: ['.js', '.jsx'],
    }
}

module 部分优化

  • includeexclude:排除不需要处理loader文件(exclude优先include)
  • cache-loader(推荐):对loader解析过的文件进行缓存,默认保存在 node_modueles/.cache/cache-loader 目录下(与cache结合使用减少10%左右速度)
rules: [
      {
        test: /.ext$/,
        use: ['cache-loader', ...loaders],
        include: path.resolve('src'),
      },
    ],
  • 设置noPrase(不推荐):与external类似,但无法共存。主要作用模块跳过编译环节,区别在于是否需要提取到cdn进行异步加载
module.exports = {
    module:{ noParse:[/jquery|chartjs/, /react.min.js$/] }
}

optimization部分

  • 去除uglifyjs-webpack-plugin改用terser-webpack-plugin做代码压缩(uglifyjs-webpack-plugin社区已失去维护,都是2年前的代码,虽然也已支持多进程,但实测效果明显不如terser-webpack-plugin
module.exports = {
  ...
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        parallel: true,
      }),
    ],
  },
  ...
};
  • optimize-css-assets-webpack-plugin(推荐):对进行css压缩
module.exports = {
  ...
  optimization: {
    minimize: true,
    minimizer: [
      new OptimizeCSSAssetsPlugin({
        assetNameRegExp: /\.css$/,
        safe: true,
        cache: true,
        parallel: true,
        discardComments: {
            removeAll: true,
        },

       }),
    ],
  },
  ...
};

  • splitChunks代码分割 (推荐): 主要作用是提取公共代码,防止代码被重复打包,拆分过大的js文件,合并零散的js文件)
module.exports = {
  ...
  optimization: {
    ...
    splitChunks: { 
        chunks: "all", //指定打包同步加载还是异步加载 
        minSize: 30000, //构建出来的chunk大于30000才会被分割 
        minRemainingSize: 0, 
        maxSize: 0, //会尝试根据这个大小进行代码分割 
        minChunks: 1, //制定用了几次才进行代码分割 
        maxAsyncRequests: 6, 
        maxInitialRequests: 4, 
        automaticNameDelimiter: "~", //文件生成的连接符 
        cacheGroups: { 
            defaultVendors: { test: /[\\/]node_modules[\\/]/, //符合组的要求就给构建venders 
            priority: -10, //优先级用来判断打包到哪个里面去 
            filename: "vendors", //指定chunks名称 }, 
            default: { 
                minChunks: 2, //被引用两次就提取出来 
                priority: -20, 
                reuseExistingChunk: true, //检查之前是否被引用过有的话就不被打包了 
             }, 
         }, 
     }
     ...
}

runtimeChunk:创建一个额外的文件或chunk,减少 entry chunk 体积,提高性能。

module.exports = {
    ```
 optimization: {
     runtimeChunk: true
   }
}

Plugin部分

const ESLintPlugin = require('eslint-webpack-plugin');

module.exports = {
  // ...
  plugins: [new ESLintPlugin()],
  // ...
};
  • mini-css-extract-plugin (推荐) :抽离css文件,可用于上传cdn
  • DllPlugin (不推荐):主要作用还是缓存, webpack5推荐使用cache(把网页依赖的基础模块抽离出来打包到dll文件中,当需要导入的模块存在于某个dll中时,这个模块不再被打包,而是去dll中获取)
  • Hardsourcewebpackplugin (webpack5报错,推荐改用cache :issue: github.com/mzgoddard/h…

多线程打包

  • 使用Happypack:打包构建时,Happypack会创建一个线程池,将构建任务模块进行拆分及分配线程,这些线程会各自去处理其中的模块以及它的依赖。处理完成之后会有一个通信的过程,会将处理好的资源传输给HappyPack的一个主进程,完成整个的一个构建过程。

图片.png

  • 使用方法
    • id: 在配置文件中设置的与 loader 关联的 id 首先会设置到实例上,为了后续 loader 与 plugin 能进行一对一匹配

    • name: 标识插件类型为 HappyPack,方便快速在 loader 中定位对应 plugin,同时也可以避免其他插件中存在 id 属性引起错误的风险

const happyThreadPool = HappyPack.ThreadPool({ size: 3 });

module: {
    rules: [{
        test: /\.js[x]?$/,
        use: [ 'happypack/loader?id=jsx']
    }]

}

plugins: [
    new HappyPack({
    id: 'jsx',
    threadPool: happyThreadPool,
    loaders: [
        {
            loader: 'babel-loader',
    },
    verbose: true,
],
...

}),
  • 使用thread-loader
    • 注意:thread-loader不可以和 mini-css-extract-plugin 结合使用
    • 原理:与HappyPack类似,每次webpack解析一个模块,thread-loader会将它及它的依赖分配给worker进程中
    • 使用:
rules: [ 
{ 
    test: /\.js[x]?$/, //对所有js后缀的文件进行编译 
    include: path.resolve('src'), //表示在src目录下的.js文件都要进行一下使用的loader 
    use: [ 'babel-loader', { 
        loader: 'thread-loader', 
        options: { workers: 3, }, 
    }, // 'happypack/loader', ]
    }
]

其他优化项

  • 使用mode属性
  • JS-Tree-Shaking: package.json设置side-effect:false,删除死代码(效果不理想,开发过程eslint本身会提示,无需设置)
    • Tips: 也可以对第三方包进行优化,如lodash。方式是使用 import { throttle } from 'lodash-es' 代替 import { throttle } from 'lodash'lodash-es Lodash 库导出为 ES 模块,支持基于 ES modules 的 tree shaking,实现按需引入)
  • purgecss-webpack-plugin : 进行CSS文件进行Tree-Shaking (推荐,css文件可能会有大幅降低)
  • Module Federation:webpack5新增的微前端解决方案,公用npm,及发布消费组件代码(示例:webpack.docschina.org/concepts/mo…
  • esbuild-loader: 相比babel-loader速度更快

5. 最终结果

  • 优化前打包速度:

图片.png

  • 优化前打包体积: 图片.png

  • 优化后打包速度:

图片.png

  • 优化后打包体积: 图片.png