关于webpack:你可能不知道的9条Webpack优化策略

2次阅读

共计 12332 个字符,预计需要花费 31 分钟才能阅读完成。

引言

webpack的打包优化始终是个陈词滥调的话题,惯例的无非就分块、拆包、压缩等。

本文以我本人的教训向大家分享如何通过一些剖析工具、插件以及 webpack 新版本中的一些新个性来显著晋升 webpack 的打包速度和改善包体积,学会剖析打包的瓶颈以及问题所在。

本文演示代码,仓库地址

速度剖析 ????

webpack 有时候打包很慢,而咱们在我的项目中可能用了很多的 pluginloader,想晓得到底是哪个环节慢,上面这个插件能够计算 pluginloader 的耗时。

yarn add -D speed-measure-webpack-plugin

配置也很简略,把 webpack 配置对象包裹起来即可:

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

const smp = new SpeedMeasurePlugin();

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

来看下在我的项目中引入 speed-measure-webpack-plugin 后的打包状况:

从上图能够看出这个插件次要做了两件事件:

  • 计算整个打包总耗时
  • 剖析每个插件和 loader 的耗时状况
    晓得了具体 loaderplugin的耗时状况,咱们就能够“隔靴搔痒”了

体积剖析 ????

打包后的体积优化是一个能够着重优化的点,比方引入的一些第三方组件库过大,这时就要思考是否须要寻找替代品了。

这里采纳的是webpack-bundle-analyzer,也是我平时工作中用的最多的一款插件了。

它能够用交互式可缩放树形图显示 webpack 输入文件的大小。用起来十分的不便。

首先装置插件:

yarn add -D webpack-bundle-analyzer

装置完在 webpack.config.js 中简略的配置一下:

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

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
    //  能够是 `server`,`static` 或 `disabled`。//  在 `server` 模式下,分析器将启动 HTTP 服务器来显示软件包报告。//  在“动态”模式下,会生成带有报告的单个 HTML 文件。//  在 `disabled` 模式下,你能够应用这个插件来将 `generateStatsFile` 设置为 `true` 来生成 Webpack Stats JSON 文件。analyzerMode: "server",
    //  将在“服务器”模式下应用的主机启动 HTTP 服务器。analyzerHost: "127.0.0.1",
    //  将在“服务器”模式下应用的端口启动 HTTP 服务器。analyzerPort: 8866,
    //  门路捆绑,将在 `static` 模式下生成的报告文件。//  绝对于捆绑输入目录。reportFilename: "report.html",
    //  模块大小默认显示在报告中。//  应该是 `stat`,`parsed` 或者 `gzip` 中的一个。//  无关更多信息,请参见“定义”一节。defaultSizes: "parsed",
    //  在默认浏览器中主动关上报告
    openAnalyzer: true,
    //  如果为 true,则 Webpack Stats JSON 文件将在 bundle 输入目录中生成
    generateStatsFile: false,
    //  如果 `generateStatsFile` 为 `true`,将会生成 Webpack Stats JSON 文件的名字。//  绝对于捆绑输入目录。statsFilename: "stats.json",
    //  stats.toJson()办法的选项。//  例如,您能够应用 `source:false` 选项排除统计文件中模块的起源。//  在这里查看更多选项:https://github.com/webpack/webpack/blob/webpack-1/lib/Stats.js#L21
    statsOptions: null,
    logLevel: "info"
  )
  ]
}

而后在命令行工具中输出 npm run dev,它默认会起一个端口号为 8888 的本地服务器:

图中的每一块清晰的展现了组件、第三方库的代码体积。

有了它,咱们就能够针对体积偏大的模块进行相干优化了。

多过程 / 多实例构建 ????

大家都晓得 webpack 是运行在 node 环境中,而 node 是单线程的。webpack 的打包过程是 io 密集和计算密集型的操作,如果能同时 fork 多个过程并行处理各个工作,将会无效的缩短构建工夫。

平时用的比拟多的两个是 thread-loaderHappyPack

先来看下 thread-loader 吧,这个也是 webpack4 官网所举荐的。

thread-loader

装置

yarn add -D thread-loader

thread-loader 会将你的 loader 搁置在一个 worker 池外面运行,以达到多线程构建。

把这个 loader 搁置在其余 loader 之前(如上面示例的地位),搁置在这个 loader 之后的 loader 就会在一个独自的 worker 池 (worker pool) 中运行。

示例

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve("src"),
        use: [
          "thread-loader",
          // your expensive loader (e.g babel-loader)
        ]
      }
    ]
  }
}

HappyPack

装置

yarn add -D happypack

HappyPack 能够让 Webpack 同一时间解决多个工作,施展多核 CPU 的能力,将工作合成给多个子过程去并发的执行,子过程解决完后,再把后果发送给主过程。通过多过程模型,来减速代码构建。

示例

// webpack.config.js
const HappyPack = require('happypack');

exports.module = {
  rules: [
    {
      test: /.js$/,
      // 1) replace your original list of loaders with "happypack/loader":
      // loaders: ['babel-loader?presets[]=es2015' ],
      use: 'happypack/loader',
      include: [/* ... */],
      exclude: [/* ... */]
    }
  ]
};

exports.plugins = [// 2) create the plugin:
  new HappyPack({// 3) re-add the loaders you replaced above in #1:
    loaders: ['babel-loader?presets[]=es2015' ]
  })
];

这里有一点须要阐明的是,HappyPack的作者示意已不再保护此我的项目,这个能够在 github 仓库看到:

作者也是举荐应用 webpack 官网提供的thread-loader

thread-loaderhappypack 对于小型我的项目来说打包速度简直没有影响,甚至可能会减少开销,所以倡议尽量在大我的项目中采纳。

多过程并行压缩代码 ????

通常咱们在开发环境,代码构建工夫比拟快,而构建用于公布到线上的代码时会增加压缩代码这一流程,则会导致计算量大耗时多。

webpack默认提供了 UglifyJS 插件来压缩 JS 代码,然而它应用的是单线程压缩代码,也就是说多个 js 文件须要被压缩,它须要一个个文件进行压缩。所以说在正式环境打包压缩代码速度十分慢 (因为压缩JS 代码须要先把代码解析成用 Object 形象示意的 AST 语法树,再利用各种规定剖析和解决AST,导致这个过程耗时十分大)。

所以咱们要对压缩代码这一步骤进行优化,罕用的做法就是多过程并行压缩。

目前有三种支流的压缩计划:

  • parallel-uglify-plugin
  • uglifyjs-webpack-plugin
  • terser-webpack-plugin

parallel-uglify-plugin

下面介绍的 HappyPack 的思维是应用多个子过程去解析和编译 JS,CSS 等,这样就能够并行处理多个子工作,多个子工作实现后,再将后果发到主过程中,有了这个思维后,ParallelUglifyPlugin 插件就产生了。

webpack 有多个 JS 文件须要输入和压缩时,原来会应用 UglifyJS 去一个个压缩并且输入,而 ParallelUglifyPlugin 插件则会开启多个子过程,把对多个文件压缩的工作分给多个子过程去实现,然而每个子过程还是通过 UglifyJS 去压缩代码。并行压缩能够显著的晋升效率。

装置

yarn add -D webpack-parallel-uglify-plugin

示例

import ParallelUglifyPlugin from 'webpack-parallel-uglify-plugin';

module.exports = {
  plugins: [
    new ParallelUglifyPlugin({
      // Optional regex, or array of regex to match file against. Only matching files get minified.
      // Defaults to /.js$/, any file ending in .js.
      test,
      include, // Optional regex, or array of regex to include in minification. Only matching files get minified.
      exclude, // Optional regex, or array of regex to exclude from minification. Matching files are not minified.
      cacheDir, // Optional absolute path to use as a cache. If not provided, caching will not be used.
      workerCount, // Optional int. Number of workers to run uglify. Defaults to num of cpus - 1 or asset count (whichever is smaller)
      sourceMap, // Optional Boolean. This slows down the compilation. Defaults to false.
      uglifyJS: {
        // These pass straight through to uglify-js@3.
        // Cannot be used with uglifyES.
        // Defaults to {} if not neither uglifyJS or uglifyES are provided.
        // You should use this option if you need to ensure es5 support. uglify-js will produce an error message
        // if it comes across any es6 code that it can't parse.
      },
      uglifyES: {
        // These pass straight through to uglify-es.
        // Cannot be used with uglifyJS.
        // uglify-es is a version of uglify that understands newer es6 syntax. You should use this option if the
        // files that you're minifying do not need to run in older browsers/versions of node.
      }
    }),
  ],
};

webpack-parallel-uglify-plugin已不再保护,这里不举荐应用

uglifyjs-webpack-plugin

装置

yarn add -D uglifyjs-webpack-plugin

示例

const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

module.exports = {
  plugins: [
    new UglifyJsPlugin({
      uglifyOptions: {
        warnings: false,
        parse: {},
        compress: {},
        ie8: false
      },
      parallel: true
    })
  ]
};

其实它和下面的 parallel-uglify-plugin 相似,也可通过设置 parallel: true 开启多过程压缩。

terser-webpack-plugin

不晓得你有没有发现:webpack4 曾经默认反对 ES6语法的压缩。

而这离不开terser-webpack-plugin

装置

yarn add -D terser-webpack-plugin

示例

const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({parallel: 4,}),
    ],
  },
};

预编译资源模块 ????

什么是预编译资源模块?

在应用 webpack 进行打包时候,对于依赖的第三方库,比方 vuevuex 等这些不会批改的依赖,咱们能够让它和咱们本人编写的代码离开打包,这样做的益处是每次更改我本地代码的文件的时候,webpack只须要打包我我的项目自身的文件代码,而不会再去编译第三方库。

那么第三方库在第一次打包的时候只打包一次,当前只有咱们不降级第三方包的时候,那么 webpack 就不会对这些库去打包,这样的能够疾速的进步打包的速度。其实也就是 预编译资源模块

webpack中,咱们能够联合 DllPluginDllReferencePlugin 插件来实现。

DllPlugin是什么?

它能把第三方库代码分来到,并且每次文件更改的时候,它只会打包该我的项目本身的代码。所以打包速度会更快。

DLLPlugin 插件是在一个额定独立的 webpack 设置中创立一个只有 dllbundle,也就是说咱们在我的项目根目录下除了有 webpack.config.js,还会新建一个webpack.dll.js 文件。

webpack.dll.js的作用是把所有的第三方库依赖打包到一个 bundledll文件外面,还会生成一个名为 manifest.json文件。该 manifest.json 的作用是用来让 DllReferencePlugin 映射到相干的依赖下来的。

DllReferencePlugin又是什么?

这个插件是在 webpack.config.js 中应用的,该插件的作用是把刚刚在 webpack.dll.js 中打包生成的 dll 文件援用到须要的预编译的依赖上来。

什么意思呢?就是说在 webpack.dll.js 中打包后比方会生成 vendor.dll.js文件和 vendor-manifest.json 文件,vendor.dll.js文件蕴含了所有的第三方库文件,vendor-manifest.json文件会蕴含所有库代码的一个索引,当在应用 webpack.config.js 文件打包 DllReferencePlugin 插件的时候,会应用该 DllReferencePlugin 插件读取 vendor-manifest.json 文件,看看是否有该第三方库。

vendor-manifest.json文件就是一个第三方库的映射而已。

怎么在我的项目中应用?

下面说了这么多,次要是为了不便大家对于 预编译资源模块 DllPlugin 和、DllReferencePlugin插件作用的了解(我第一次应用看了良久才明确~~)

先来看下实现的我的项目目录构造:

次要在两块配置,别离是 webpack.dll.jswebpack.config.js(对应这里我是webpack.base.js

webpack.dll.js

const path = require('path');
const webpack = require('webpack');

module.exports = {
  mode: 'production',
  entry: {vendors: ['lodash', 'jquery'],
    react: ['react', 'react-dom']
  },
  output: {filename: '[name].dll.js',
    path: path.resolve(__dirname, './dll'),
    library: '[name]'
  },
  plugins: [
    new webpack.DllPlugin({name: '[name]',
      path: path.resolve(__dirname, './dll/[name].manifest.json')
    })
  ]
}

这里我拆了两局部:vendors(寄存了 lodashjquery 等)和 react(寄存了 react 相干的库,reactreact-dom 等)

webpack.config.js(对应我这里就是webpack.base.js)

const path = require("path");
const fs = require('fs');
// ...
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin');
const webpack = require('webpack');

const plugins = [// ...];

const files = fs.readdirSync(path.resolve(__dirname, './dll'));
files.forEach(file => {if(/.*\.dll.js/.test(file)) {
    plugins.push(new AddAssetHtmlWebpackPlugin({filepath: path.resolve(__dirname, './dll', file)
    }))
  }
  if(/.*\.manifest.json/.test(file)) {
    plugins.push(new webpack.DllReferencePlugin({manifest: path.resolve(__dirname, './dll', file)
    }))
  }
})

module.exports = {
  entry: {main: "./src/index.js"},
  module: {rules: []
  },
  plugins,

  output: {
    // publicPath: "./",
    path: path.resolve(__dirname, "dist")
  }
}

这里为了演示省略了很多代码,我的项目残缺代码在这里

因为下面我把第三方库做了一个拆分,所以对应生成也就会是多个文件,这里读取了一下文件,做了一层遍历。

最初在 package.json 外面再增加一条脚本就能够了:

"scripts": {"build:dll": "webpack --config ./webpack.dll.js",},

运行 yarn build:dll 就会生成本大节结尾贴的那张我的项目结构图了~

利用缓存晋升二次构建速度 ????

一般来说,对于动态资源,咱们都心愿浏览器可能进行缓存,那样当前进入页面就能够间接应用缓存资源,页面关上速度会显著放慢,既进步了用户的体验也节俭了宽带资源。

当然浏览器缓存办法有很多种,这里只简略探讨下在 webpack 中如何利用缓存来晋升二次构建速度。

webpack 中利用缓存个别有以下几种思路:

  • babel-loader开启缓存
  • 应用cache-loader
  • 应用hard-source-webpack-plugin

babel-loader

babel-loader在执行的时候,可能会产生一些运行期间反复的公共文件,造成代码体积冗余,同时也会减慢编译效率。

能够加上 cacheDirectory 参数开启缓存:

 {
    test: /\.js$/,
    exclude: /node_modules/,
    use: [{
      loader: "babel-loader",
      options: {cacheDirectory: true}
    }],
  },

cache-loader

在一些性能开销较大的 loader 之前增加此 loader,以将后果缓存到磁盘里。

装置

yarn add -D cache-loader

应用

cache-loader 的配置很简略,放在其余 loader 之前即可。批改Webpack 的配置如下:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.ext$/,
        use: [
          'cache-loader',
          ...loaders
        ],
        include: path.resolve('src')
      }
    ]
  }
}

请留神,保留和读取这些缓存文件会有一些工夫开销,所以请只对性能开销较大的 loader 应用此 loader

hard-source-webpack-plugin

HardSourceWebpackPlugin 为模块提供了两头缓存,缓存默认的寄存门路是: node_modules/.cache/hard-source

配置 hard-source-webpack-plugin后,首次构建工夫并不会有太大的变动,然而从第二次开始,构建工夫大概能够缩小 80%左右。

装置

yarn add -D hard-source-webpack-plugin

应用

// webpack.config.js
var HardSourceWebpackPlugin = require('hard-source-webpack-plugin');

module.exports = {
  entry: // ...
  output: // ...
  plugins: [new HardSourceWebpackPlugin()
  ]
}

webpack5中会内置hard-source-webpack-plugin

放大构建指标 / 缩小文件搜寻范畴 ????

有时候咱们的我的项目中会用到很多模块,但有些模块其实是不须要被解析的。这时咱们就能够通过放大构建指标或者缩小文件搜寻范畴的形式来对构建做适当的优化。

放大构建指标

次要是 excludeinclude 的应用:

  • exclude: 不须要被解析的模块
  • include: 须要被解析的模块
// webpack.config.js
const path = require('path');
module.exports = {
  ...
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        // include: path.resolve('src'),
        use: ['babel-loader']
      }
    ]
  }

这里 babel-loader 就会排除对 node_modules 下对应 js 的解析,晋升构建速度。

缩小文件搜寻范畴

这个次要是 resolve 相干的配置,用来设置模块如何被解析。通过 resolve 的配置,能够帮忙 Webpack 疾速查找依赖,也能够替换对应的依赖。

  • resolve.modules:通知 webpack 解析模块时应该搜寻的目录
  • resolve.mainFields:当从 npm 包中导入模块时(例如,import * as React from 'react'),此选项将决定在 package.json 中应用哪个字段导入模块。依据 webpack 配置中指定的 target 不同,默认值也会有所不同
  • resolve.mainFiles:解析目录时要应用的文件名,默认是index
  • resolve.extensions:文件扩展名
// webpack.config.js
const path = require('path');
module.exports = {
  ...
  resolve: {
    alias: {react: path.resolve(__dirname, './node_modules/react/umd/react.production.min.js')
    }, // 间接指定 react 搜寻模块,不设置默认会一层层的搜查
    modules: [path.resolve(__dirname, 'node_modules')], // 限定模块门路
    extensions: ['.js'], // 限定文件扩展名
    mainFields: ['main'] // 限定模块入口文件名

动静 Polyfill 服务 ????

介绍 动静 Polyfill前,咱们先来看下什么是babel-polyfill

什么是 babel-polyfill?

babel只负责语法转换,比方将 ES6 的语法转换成ES5。但如果有些对象、办法,浏览器自身不反对,比方:

  • 全局对象:PromiseWeakMap 等。
  • 全局动态函数:Array.fromObject.assign 等。
  • 实例办法:比方 Array.prototype.includes 等。

此时,须要引入 babel-polyfill 来模仿实现这些对象、办法。

这种个别也称为 垫片

怎么应用babel-polyfill

应用也非常简单,在 webpack.config.js 文件作如下配置就能够了:

module.exports = {entry: ["@babel/polyfill", "./app/js"],
};

为什么还要用 动静 Polyfill

babel-polyfill因为是一次性全副导入整个polyfill,所以用起来很不便,但与此同时也带来了一个大问题:文件很大,所以后续的计划都是针对这个问题做的优化。

来看下打包后 babel-polyfill 的占比:

占比 29.6%,有点太大了!

介于上述起因,动静 Polyfill服务诞生了。
通过一张图来理解下 Polyfill Service 的原理:

每次关上页面,浏览器都会向 Polyfill Service 发送申请,Polyfill Service辨认 User Agent,下发不同的 Polyfill,做到按需加载 Polyfill 的成果。

怎么应用 动静 Polyfill服务?

采纳官网提供的服务地址即可:

// 拜访 url,依据 User Agent 间接返回浏览器所需的 polyfills
https://polyfill.io/v3/polyfill.min.js

Scope Hoisting ????

什么是Scope Hoisting

Scope hoisting 直译过去就是「作用域晋升」。相熟 JavaScript 都应该晓得「函数晋升」和「变量晋升」,JavaScript 会把函数和变量申明晋升到以后作用域的顶部。「作用域晋升」也相似于此,webpack 会把引入的 js 文件“晋升到”它的引入者顶部。

Scope Hoisting 能够让 Webpack 打包进去的代码文件更小、运行的更快。

启用Scope Hoisting

要在 Webpack 中应用 Scope Hoisting 非常简单,因为这是 Webpack 内置的性能,只须要配置一个插件,相干代码如下:

// webpack.config.js
const webpack = require('webpack')

module.exports = mode => {if (mode === 'production') {return {}
  }

  return {
    devtool: 'source-map',
    plugins: [new webpack.optimize.ModuleConcatenationPlugin()],
  }
}

启用 Scope Hoisting 后的比照

让咱们先来看看在没有 Scope Hoisting 之前 Webpack 的打包形式。

如果当初有两个文件别离是

  • constant.js:
export default 'Hello,Jack-cool';
  • 入口文件 main.js:
import str from './constant.js';
console.log(str);

以上源码用 Webpack 打包后的局部代码如下:

[(function (module, __webpack_exports__, __webpack_require__) {var __WEBPACK_IMPORTED_MODULE_0__constant_js__ = __webpack_require__(1);
    console.log(__WEBPACK_IMPORTED_MODULE_0__constant_js__["a"]);
  }),
  (function (module, __webpack_exports__, __webpack_require__) {__webpack_exports__["a"] = ('Hello,Jack-cool');
  })
]

在开启 Scope Hoisting 后,同样的源码输入的局部代码如下:

[(function (module, __webpack_exports__, __webpack_require__) {var constant = ('Hello,Jack-cool');
    console.log(constant);
  })
]

从中能够看出开启 Scope Hoisting 后,函数申明由两个变成了一个,constant.js 中定义的内容被间接注入到了 main.js 对应的模块中。这样做的益处是:

  • 代码体积更小,因为函数申明语句会产生大量代码;
  • 代码在运行时因为创立的函数作用域更少了,内存开销也随之变小。

Scope Hoisting 的实现原理其实很简略:剖析出模块之间的依赖关系,尽可能的把打散的模块合并到一个函数中去,但前提是不能造成代码冗余。因而只有那些被援用了一次的模块能力被合并。

因为 Scope Hoisting 须要剖析出模块之间的依赖关系,因而源码必须采纳 ES6 模块化语句,不然它将无奈失效。

参考

极客工夫【玩转 webpack】

❤️ 爱心三连击

1. 如果感觉这篇文章还不错,就帮忙 点赞、分享 一下吧,让更多的人也看到~

2. 关注公众号 前端森林,定期为你推送陈腐干货好文。

3. 非凡阶段,带好口罩,做好集体防护。

4. 增加微信fs1263215592,拉你进技术交换群一起学习 ????

正文完
 0