做webpack性能优化的一段经历|项目复盘

2,676 阅读6分钟

前言

回忆往昔岁月,我做前端开发至今已3年有余,做过的项目大部分都是业务需求迭代,技术优化经验相对较少。去年承蒙leader信任,给予机会开始做公司项目的技术优化。技术优化的首次任务,就是对整体前端项目进行性能优化(编译优化+构建体积优化)。

一、项目简介:

  1. 项目简介:我们团队主要做的是SaaS项目,有涉及B端的智能客服、在线客服、智能电销,C端聊天机器人等业务。
  2. 项目规模:产品:10人;前端:10人;后端:24人;测试:10人,和若干销售、运营人员等。
  3. 个人职责:我主要是负责私有化业务线的前端项目开发,然后就是在前端技术架构这块打打杂,做点项目优化的事情~~ 最近做项目复盘的时候,发现搞项目优化给我带来不少的收获和成长。今天我跟大家分享下做webpack性能优化的一段经历。

二、项目背景:

为什么要做webpack优化?

我司团队的项目使用的前端技术栈是vue + vuex + webpack,由于业务代码过于庞杂,组件数量过多,而且引入的第三方依赖较多,因而出现了webpack编译速度较慢、编译打包后的代码体积过大、页面加载速度较慢等问题。

三、实践过程:

做webpack性能优化前,首先调研了其构建过程。了解到webpack启动后会根据entry配置的入口出发,递归遍历解析所依赖的文件,然后进行转换输出dist文件。简单来讲,webpack构建过程可以分为解析编译、打包输出的2个过程。因此,性能优化就从这2个方面着手。

编译优化

1、首先进行编译速度分析

我使用了 speed-measure-webpack-plugin 插件分析每个loader和plugin执行耗时具体情况。使用配置如下:

const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();

const webpackConfig = smp.wrap({
  plugins: [new MyPlugin(), new MyOtherPlugin()],
});

image.png 由上图可知,编译总耗时81.56秒,其中vue-loader、css-loader、url-loader、babel-loader明显耗时较长。由此可以分析出webpack在文件搜索、解析时间过长,因为loader就是用来做文件解析转换的。

2、分析耗时情况,对症下药进行编译优化

文件搜索时间过久的优化措施

1)配置module.noParse,告诉webpack不必解析某些文件
比如 JQuery、Lodash 已经是可以直接运行在浏览器的文件,就不必再搜索解析。配置使用如下:

module.exports = {
  //...
  module: {
    noParse: /jquery|lodash/,
  },
};

2)配置loader,通过test、include、exclude缩小搜索范围,例如:

module.exports = {
  //...
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
        },
        include: [resolve('src')],
        exclude: /node_modules/,
      }
    ]
  }
}

文件解析过久的优化措施

1)解析loader开启多进程,使用thread-loader
thread-loader 使用起来也非常简单,只要把 thread-loader 放置在其他 loader 之前, 那 thread-loader 之后的 loader 就会在一个单独的 worker 池(worker pool)中运行。
官方上说每个 worker 大概都要花费 600ms ,所以官方为了防止启动 worker 时的高延迟,提供了对 worker 池的优化。使用方式如下:

const threadLoader = require('thread-loader');

const jsWorkerPool = {
  // 产生的 worker 的数量,默认是 (cpu 核心数 - 1)
  // 当 require('os').cpus() 是 undefined 时,则为 1
  workers: 2,
  
  // 闲置时定时删除 worker 进程
  // 默认为 500ms
  // 可以设置为无穷大, 这样在监视模式(--watch)下可以保持 worker 持续存在
  poolTimeout: 2000
};

threadLoader.warmup(jsWorkerPool, ['babel-loader']);

module.exports = {
  // ...
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'thread-loader',
            options: jsWorkerPool
          },
          'babel-loader'
        ]
      }
    ]
  }
}

注意:如果小项目,文件不多,无需使用thread-loader开启多进程,这样反而打包会变慢,因为开启进程也是需要额外耗时的。

2)合理利用缓存

  • loader启用缓存,比如使用loader本身的缓存或使用cache-loader
    • loader本身缓存
    loader: 'babel-loader?cacheDirectory=ture'
    
    • cache-loader 在一些性能开销较大的 loader 之前添加此 loader,以将结果缓存到磁盘里。
    module.exports = {
      module: {
        rules: [
          {
            test: /\.ext$/,
            use: [
              'cache-loader',
              ...loaders
            ],
            include: path.resolve('src')
          }
        ]
      }
    }
    

注意:保存和读取这些缓存文件会有一些时间开销,所以最好只对性能开销较大的 loader 使用loader缓存。

  • 利用模块缓存提升二次构建的速度
    使用 hard-source-webpack-plugin 为模块提供中间缓存,快速提升二次构建的速度。
module.exports = {
  //...
  plugin: [
      new HardSourceWebpackPlugin({
        cachePrune: {
          maxAge: Infinity,
          sizeThreshold: Infinity
        }
      })
  ]
}

代码压缩打包时间过久的优化方式

1)压缩代码插件开启多进程并行、缓存模式

  • terser-webpack-plugin (webpack4推荐使用,支持es6语法)
const TerserJSPlugin = require('terser-webpack-plugin');
 
module.exports = {
  optimization: {
    minimizer: [
     new TerserJSPlugin({
         cache: true,// 启用缓存
         parallel: true// 开启多进程
     })
    ],
  },
};

如果你使用的是 webpack v5 或以上版本,你不需要安装这个插件。webpack v5 自带最新的 terser-webpack-plugin。如果使用 webpack v4,则必须安装 terser-webpack-plugin v4 的版本。

  • uglifyjs-webpack-plugin(webpack3、4都可以使用)
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
 
module.exports = {
  optimization: {
    minimizer: [
     new UglifyJsPlugin({
         cache: true,// 启用缓存
         parallel: true// 开启多进程
     })
    ],
  },
};

采用上述优化手段,可以将总体编译速度提升了80%左右。我们项目优化后,编译时间从原来的81.56秒加快为13.268秒,编译速度提升了83%。
上述优化效果最明显的是 hard-source-webpack,虽然首次构建时间变化不大,但是第二次开始,编译构建速度提升了60-70%。

构建体积优化

1、对bundle体积大小进行分析

使用 webpack-bundle-analyzer 分析打包后生成Bundle的每个模块体积大小。配置如下:

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

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

bundle体积总览图

主文件体积图 由上图可知,bundle文件里包含了很多较大的第三方依赖包、业务代码js/css、以及图片资源等。因此打包体积优化可以从这几个方面考虑优化。

2、因地制宜进行构建体积优化

bundle打包文件去除第三方依赖包

配置externals,防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)。

module.exports = {
  //...
  externals: {
    vue: 'Vue',
    vuex: 'Vuex',
    'vue-router': 'VueRouter',
    axios: 'axios',
    ...
  },
};

index.html再引入需要的第三方依赖js:

<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.12/vue.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/vuex/3.0.1/vuex.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/vue-router/3.0.1/vue-router.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.19.0/axios.min.js"></script>

通过配置externals,大幅度减少了打包生成bundle的体积,因为配置的这些第三方依赖都没打包进来。 我司项目使用了该方法后,体积文件大幅度减少,index.js体积从4.34M减少到2.74M,变小了36%,如图所示:

image.png Tree Shaking剔除无用的JavaScript

// package.json中添加
{
    "sideEffects": ["*.css", "babel-polyfill"]
}

通过配置sideEffects,Tree Shaking便开启了,webpack打包时会自动剔除没有引用的js文件。对于业务文件冗余,但又不敢轻易删除的项目特别适合开启Tree Shaking,可以大幅度减少打包体积。

四、总结思考:

在做webpack优化过程中,我也遇到过一些问题。给大家总结一下,webpack优化主要需要注意以下几点:

  • webpack配置要区分版本,webpack3.x和webpack4.x使用差异挺大的,需要参照官方文档来修改配置。没区分版本用一样的配置会报错哦~~
  • 善于利用可视化工具进行分析,因地制宜制定优化方案。只有适合自己项目的优化方案才是最好的。 (荀子曰:假舆马者,非利足也,而致千里;假舟楫者,非能水也,而绝江河。君子生非异也,善假于物也。)

分析工具可参考:五种可视化方案分析 webpack 打包性能瓶颈

本文正在参与「掘金 2021 春招闯关活动」, 点击查看 活动详情