性能分析

1. 统计基本信息

使用webpack内置的stats

可以统计出构建时间、构建资源清单及资源大小等信息

使用方法:

1. cli

  1. webpack --env production --json > stats.json

2. node API

  1. webpack(config, (err, stats) => {
  2. console.log(stats);
  3. });

2. 速度分析

使用speek-measure-webpack-plugin

插件功能

  1. 分析出整个构建时间和每个loader和plugin的构建时间
  2. 时间过长的标红,较长的标黄

插件使用:包裹webpack的配置

  1. const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
  2. const smp = new SpeedMeasurePlugin();
  3. const webpackConfig = smp({
  4. // webpack配置
  5. });

3. 体积分析

使用webpack-bundle-analyzer

以可视化形式展示打包依赖模块的体积。

  1. const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
  2. module.exports = {
  3. plugins: [
  4. new BundleAnalyzerPlugin()
  5. ]
  6. };

构建完成后会启动本地服务,serve 8888端口,浏览器中访问就能看到分析结果。

提升构建速度

使用高版本的webpack和nodejs

高版本的webpack和nodejs构建速度更快

webpack4优化原因:

  • v8带来的优化(for of 代替forEach,Map、Set代替Object、includes代替indexOf)
  • 默认使用更快的md4 hash算法
  • webpack AST可以直接从loader传递给AST,减少解析时间
  • 使用字符串方法代替正则

多进程多实例构建

资源并行解析可选方案

  • HappyPack
    作者已经不维护,建议使用webpack官方提供的”thread-loader”
  • thread-loader
  1. {
  2. test: /\.js$/,
  3. use: [
  4. {
  5. loader: 'thread-loader',
  6. options: {
  7. workers: 3
  8. }
  9. },
  10. 'babel-loader'
  11. ]
  12. }
  • parallel-webpack

多进程多实例并行压缩

1. 方法一,使用parallel-uglify-plugin插件

  1. const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
  2. module.exports = {
  3. plugins: [
  4. new ParallelUglifyPlugin({
  5. // ...
  6. })
  7. ]
  8. };

2. 方法二,使用uglifyjf-webpack-plugin

目前webpack官方推荐使用terser-webpack-plugin

3. 方法三,使用terser-webpack-plugin,开启parallel参数

  1. const TerserPlugin = require('terser-webpack-plugin');
  2. module.exports = {
  3. optimization: {
  4. minimizer: [
  5. new TerserPlugin({
  6. parallel: 4
  7. })
  8. ]
  9. }
  10. };

进一步分包:预编译资源模块

1. 使用html-webpack-externals-plugin

将react、react-dom基础包通过cdn引入,不打入bundle中

使用方法:

  1. const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');
  2. plugins: [
  3. new HtmlWebpackExternalsPlugin({
  4. externals: [
  5. {
  6. module: 'react',
  7. entry: '//path/to/your/cdn-domain/react.min.js',
  8. global: 'React'
  9. },
  10. ...
  11. ]
  12. });
  13. ]

效果

  1. <script type="text/javascript" src="//path/to/your/cdn-domain/react.min.js"></script>

2. 预编译资源模块,使用DLLPlugin和DllReferencePlugin

通常来说,我们的代码都可以至少简单区分成业务代码第三方库。如果不做处理,每次构建时都需要把所有的代码重新构建一次,耗费大量的时间。然后大部分情况下,很多第三方库的代码并不会发生变更(除非是版本升级),这时就可以用到dll:把复用性较高的第三方模块打包到动态链接库中,在不升级这些库的情况下,动态库不需要重新打包,每次构建只重新打包业务代码。 使用dll时,构建过程分成dll构建过程和主构建过程,所以需要两个构建配置文件,例如叫做<font style="color:rgb(33, 37, 41);">webpack.config.js</font><font style="color:rgb(33, 37, 41);">webpack.dll.config.js</font>

步骤:

  1. 使用DLLPlugin进行分包,对第三方包打包,完成后打包结果保存在项目中,后面就不需要再构建第三方包了。
  2. 每次构建业务项目时候,使用DllReferencePlugin实现对构建好的第三方包dll的解析和处理。

示例:

使用DLLPlugin进行分包

  1. // webpack.dll.config.js
  2. const path = require('path');
  3. const webpack = require('webpack');
  4. module.exports = {
  5. context: process.cwd(),
  6. entry: {
  7. library: [
  8. 'react',
  9. 'react-dom',
  10. 'redux'
  11. ]
  12. },
  13. output: {
  14. filename: '[name].dll.js',
  15. path: path.resolve(__dirname, './build/library'),
  16. library: '[name]'
  17. },
  18. plugins: [
  19. new webpack.DllPlugin({
  20. name: '[name]',
  21. path: './build/library/[name].json'
  22. })
  23. ]
  24. };

使用DllReferencePlugin对manifest.json引用

  1. // webpack.config.js
  2. module.exports = {
  3. plugins: [
  4. new webpack.DllReferencePlugin({
  5. manifest: require('./build/library/manifest.json')
  6. })
  7. ]
  8. };

引用效果

  1. <script src="/build/library/library.dll.js"></script>

原理

使用DLLPlugin对第三方库打包时候,会生成打包结果和manifest.json文件到指定目录,文件中包含各第三方包的引用关系等信息。

使用DllReferencePlugin插件打包业务代码时候,我们通过配置告诉插件DLLPlugin打包的产物的目录,DllReferencePlugin会分析manifest文件,DLL的包不会参与打包构建过程,并且还生成相关的引用。

3. 使用externals选项

使用externals选项可以排除指定的第三方模块,在构建过程中忽略它们。

使用方法示例:

首先在html中引入第三方模块

  1. <script src="https://example-cdn.com/react.min.js"></script>

然后在externals选项里面配置要排除的模块和引用方式

  1. // webpack.config.js
  2. module.exports = {
  3. externals: {
  4. 'react': 'React'
  5. }
  6. };

在项目中引用

  1. import React from 'react';

充分利用缓存提升二次构建速度

缓存思路

  1. babel-loader开启缓存
  2. terser-webpack-plugin开启缓存
  3. 使用cache-loader或者hard-source-webpack-plugin

缩小构建目标

目的:尽可能少构建模块

比如babel-loader不解析node_modules

  1. module.exports = {
  2. rules: {
  3. test: /\.js$/,
  4. loader: 'happypack-loader',
  5. exclude: 'node_moudles'
  6. }
  7. };

减少文件搜索范围

  1. 优化resolve.modules配置(减少模块搜索层级)
  2. 优化resolve.mainFields配置(缩小模块入口搜索范围)
  3. 优化resolve.extensions配置(比如限定.js,其他引用时候补全后缀)
  4. 合理使用alias(缩小模块引用路径搜索范围)

使用oneOf

通常来讲,同一种类型的文件只能由一个loader处理,那么正常来讲的逻辑应该是,比如我是一个css文件,那么我匹配到test为css后缀的loader我就应该立即执行了,但是事实是,虽然匹配到了,但是还是会遍历完整一遍再进行解析,这样来讲效果明显就更低了。 而Oneof语法就是解决这个问题的,使文件一旦匹配上loader之后就立即解析,省去了全盘遍历这个不必要的过程。 如果对于需要多个loader共同解决的文件类型,比如js。那就需要把其中的loader放历Oneof之外,这样才能实现loader也能同时执行到。

提升加载和运行速度

使用Tree Shaking擦除无用的js和css

摇树js

Tree-Shaking原理

Tree-shaking的本质是消除没有用到的代码。主要的效果是,引用了但没有使用的模块,不会被打包到最终的bundle中。 Tree-shaking要求模块是ESM,ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,这就是tree-shaking的基础。 所谓静态分析就是不运行代码,从语法上对代码进行分析,ES6之前的模块化规范,比如我们可以动态require一个模块,只有执行后才知道是否要引用某个模块,引用的是什么模块(因为require是运行时调用,所以require理论上可以运用在代码的任何地方,而且require支持传入表达式作为参数,只能在运行时才知道引入的是哪个模块),这个就不能通过静态分析去做优化。

webpack默认支持Tree-shaking,如果mode为”production”webpack在构建会做Tree-shaking的操作。

摇树css

摇树css的基本思路是,给定content和css,分析content中用到的选择器,然后分析css文件中没有用到的选择器,将其移除。

摇树css工具有:

  1. PurifyCSS
    使用purgecss-webpack-plugin
  2. uncss

scope-hoisting

Scope Hoisting使用和原理分析

webpack 的 scope hoisting 是什么?

scope hoisting 是 webpack3 的新功能,直译过来就是「作用域提升」。熟悉 JavaScript 都应该知道「函数提升」和「变量提升」,JavaScript 会把函数和变量声明提升到当前作用域的顶部。「作用域提升」也类似于此,webpack 会把引入的 js 文件“提升到”它的引入者顶部。
在之前版本中,webpack打包会把每个模块用闭包封装,通过webpack_require 引用。 这样会存在问题:
  1. ⼤量作用域包裹代码,导致体积增大(模块越多越明显)
  2. 运行代码时创建的函数作⽤域变多(每个模块引用都要创建一个函数作用域),内存开销变大
为了解决这两个问题,webpack启用scope hoisting,将每个模块都提升到引入者顶部,这样模块不会因为依赖链路较深而导致调用栈变深,它们都在同一层。 这样就解决了上面的两个问题:
  1. 代码量明显减少。
  2. 调用栈变浅,减少了创建作用域的内存和计算损耗,提升了运行速度。

使用webpack进行图片压缩

图片压缩实际是使用了基于node库的imagemin或者tinypng API

在webpack中配置image-webpack-loader,这个loader实际是使用了imagemin进行图片压缩

优化polyfill方案

polyfill的方案:

1. babel-polyfill

将babel-polyfill作为一个单独的入口打包
这样做的一个问题是会将所有polyfill代码都打包进去(200k左右),导致代码体积过大

  1. const path = require('path');
  2. module.exports = {
  3. entry: [
  4. 'babel-polyfill',
  5. path.resolve(__dirname, './src/index.js')
  6. ]
  7. };

2. babel-presets的选项中“useBuiltIns”

选项值为“false”时候,不加入polyfill。
选项值为“entry”时候,将所有polyfill打包进项目。
选项值为“usage”时候,按需加载,并且做了优化:将polyfill的工具方法提取成公共资源,而不会每个 polyfill代码都内联相同的工具方法 。
此外babel-presets中还支持根据支持的浏览器来选择polyfill,这通过”target”属性配置。

  1. // .babelrc
  2. {
  3. "presets": [
  4. "@babel/preset-env",
  5. {
  6. "useBuildIns": "useage"
  7. }
  8. ]
  9. }

3. @babel/runtime和@babel/plugin-transform-runtime

  1. `@babel/runtime`实现polyfill的功能,它分析代码,然后添加相关的polyfill,即实现了polyfill的按需加载。它和上述两种方法的区别是,它在添加polyfill代码时候,不会污染全局变量,而是定义局部方法来实现polyfill。**因为这个特点,它更适合用在第三方库中,而上面两种适合用在业务代码项目中**。
  2. 其缺点在于不支持实例方法的polyfill,如`arr.includes(1);`
  3. 由于 `@babel/runtime`也是使用内联代码实现polyfill,因此可能多个文件中会内联相同的工具方法。`@babel/plugin-transform-runtime`用来解决这个问题,它提取公共的工具方法,每个文件使用时候引入相关的工具方法,这样减少的代码体积。

4. polyfill-service,使用动态polyfill服务

根据浏览器userAgent选择相应的polyfill,有些浏览器支持的,就不再下发冗余polyfill。
可以使用官方的服务。
或者自建polyfill服务。
可能存在的问题:浏览器ua不准,有些国内浏览器修改ua导致polyfill判断错误,降级方案是下载所有polyfill

  1. https://cdn.polyfill.io/v2/polyfill.min.js

使用prerender-spa-plugin预渲染

prerender-spa-plugin

prerender-spa-plugin插件启用无头浏览器,加载项目的路由,并渲染出首屏页面(也可以配置其他路由),然后生成静态页面,保存在指定的目录。 我们的静态资源服务器就可以serve预渲染的页面了。 使用这个插件相当于在构建阶段就渲染好了首屏页面,极大地提升了首屏性能。