Webpack打包优化

20213.13 星期五

构建流程

1、初始化参数:配置文件和shell语句合并参数,得到最终参数
2、开始编译:初始化Compiler编译对象,加载插件,执行run开始编译
3、确定入口:根据entry找到入口文件
4、编译模块:用loader进行翻译后,找出对应依赖模块
5、完成编译:确定了翻译的内容和依赖关系
6、输出准备:根据入口和模块的依赖关系,组装成包含多个模块的chunk,每个chunk转成一个文件加载到输出列表。
7、执行输出:根据output路径和文件名,写入文件系统。

结果分析

有针对性优化手段。
虽然一些基础的手段确实必要,但是应该清楚项目的瓶颈在哪里,影响多大。是否必要。
要对项目有清晰的认识,才可以对症下药。

performance: 如何展示性能提示。
performance.hints: 在生产环境构建时推荐使用 “error”

stats: 在统计输出里指定你想看到的信息。
webpack --profile --json > compilation-stats.json

ProgressBarPlugin
SpeedMeasurePlugin
DashboardPlugin
BundleAnalyzerPlugin
DuplicatePackageCheckerPlugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
constrequire('progress-bar-webpack-plugin');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const DashboardPlugin = require('webpack-dashboard/plugin');

module.exports = {
plugins: [
new ProgressBarPlugin({
format: 'build [:bar]' + chalk.green.bold(':percent') + ' (:elapsed seconds)',      
clear: false    
})
new DashboardPlugin(),
new BundleAnalyzerPlugin({ analyzerMode'static', openAnalyzertrue, logLevel'info' })
],
};

const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();
smp.wrap(webpackConfig);

其他:webpack-chart,webpack-visualizer,webpack bundle optimize helper,bundle-stats
注意:不分析结果的时候,上面插件都不用设置。可以通过参数去配置是否启动。

构建速度优化

构建往往会有不同的结果,比如生产环境,开发环境。或者node端,es module。
从方式上分为:构建过程/速度优化;构建结果(质量/大小)优化。

有些基础优化手段,在各个结果上都适用。最后有些选项适用不同的环境。

最新版本

更新webpack到最新版本,包括node.js, 不论什么环境都有助于性能提升。

Mode

提供 mode 配置选项,告知 webpack 使用相应模式的内置优化。

从 webpack 4 开始,会根据你选择的 mode 来执行不同的优化, 不过所有的优化还是可以手动配置和重写。

我们可以对比mode中默认配置,做一些优化。

entry

构建之源头。
相当于大厦的地基,后续的所有操作都取决于入口文件。如果可以在入口处解决,后续的优化手段完全没有必要。

所以,开始很重要。

最简单的就是一个项目有很多个入口,有些页面由于历史原因现在已经很少维护了。
如果你打算对项目做优化,把这些入口从项目中移出去是最快最简单的方法。

还有就是多项目。很多个项目共用一个打包构建,单独修改其中一个项目,会重新构建所有项目。
这种情况可以只对修改的项目单独构建。
(这里可以有些思考)

同样在开发时也适用。某次的修改/feature,只会出现在一个入口里。
如果只启动本次相关的入口,至少每次的启动时间会减少。

方法论已经清楚了。具体操作就不解释了

Module

Module选项决定了如何处理项目中的不同类型的模块。
通过以下设置将 loader 应用于最少数量的必要模块,自然提升了速度(模块入口)。

module.noParse: 忽略大型的 library 可以提高构建性能。
module.rules:test, exclude, include。
test 尽量保证项目中出现的条件。用include引入符合条件的模块。或者exclude排除,比如使用babel是可以排除es5的文件。

比如图片处理都在某一个文件目录下,就可以用include:path.resolve(__dirname, 'src/images')。或者通过exclude排除不参与的node_modules
比如没有写typescript,而多余的引入ts的条件,包括loader

Resolve

设置模块如何被解析,优化模块查找路径。

resolve.alias:使模块引入变得更简单。引用的时候可以不关注路径
resolve.extensions: 尝试按顺序解析这些后缀名。把常用的文件放在前面
resolve.modules: 可以使用绝对路径,将只在给定目录中搜索。比如设置node_modules为绝对路径,再加上开发目录。
resolve.cacheWithContext: 如果启用了不安全缓存可以设置。

externals

某些依赖从脚本引入不参与构建。比如jQuery,editor,videojs

缓存

cache

缓存生成的 webpack 模块和 chunk,来改善构建速度。缓存默认在观察模式(watch mode)启用。

cache 会在开发 模式被设置成 type: 'memory' 而且在 生产 模式 中被禁用。
或许生产环境也可以利用cache:type: 'filesystem'

cache-loader

在一些性能开销较大的 loader 之前添加 cache-loader。

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

1
2
3
4
5
6
7
rules: [
{
test: /\.ext$/,
use: ['cache-loader', ...loaders],
include: path.resolve('src'),
},
]

插件和loader

有的插件和loader 也是可以开启缓存的。比如:babel-loader, thread-loader/HappyPack, HtmlWebpackPlugin,TerserPlugin等。
可以注意一下。

babel-loader 选项cacheDirectory:默认值为 false。

多线程

parallelism: 限制并行处理的模块数量。默认100。可以根据项目的实际情况调整以充分利用电脑性能。

thread-loader,happypack, parallel-webpack

不要使用太多的 worker,因为 Node.js 的 runtime 和 loader 都有启动开销。

有些插件也可以配置多线程。比如TerserPlugin。

1
2
3
4
5
6
7
8
9
10
11
12
const threadLoader = require('thread-loader');

rules: [
{
test: /\.js$/,
include: path.resolve('src'),
use: [
"thread-loader",
// 耗时的 loader (例如 babel-loader)
],
},
],

1
2
3
4
5
6
7
8
9
10
const happyThreadPool = HappyPack.ThreadPool({ size: require('os').cpus().length - 1});

new HappyPack({
id: 'js',
threadPool: happyThreadPool,
loaders: [{
loader: 'babel-loader',
options: Object.assign({}, babelrc, { cacheDirectory: true })
}]
}),

devtool/source map

控制是否生成,以及如何生成 source map。
不同的值会明显影响到构建(build)和重新构建(rebuild)的速度,包括构建质量。

模式是: [inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map.

inline映射文件生成到输出文件中
eval是打包最快的方式。会映射到转换后的代码,所以不能正确的显示行数。
cheap方式定位到行不精确到列;
module方式不止是业务层,各个模块如loader都负责
source-map - 整个 source map 作为一个单独的文件生成

开发环境: eval, eval-source-map, eval-cheap-source-map, eval-cheap-module-source-map(大多数情况下最佳)
对于生产环境:none,source-map,nosources-source-map

—- 以下区分不同环境: 开发和生产,及其他。—-

Optimization

从 webpack 4 开始,会根据你选择的 mode 来执行不同的优化, 不过所有的优化还是可以手动配置和重写。

optimization.emitOnErrors
optimization.removeAvailableModules
optimization.removeEmptyChunks
optimization.mergeDuplicateChunks

构建质量优化

大概率适用于生产环境提升网页性能。

输出包大小

TerserPlugin, uglifyjs-webpack-plugin:压缩代码。去除空白,注释log等。
CompressionWebpackPlugin: 提供gz 压缩包。
IgnorePlugin, (ContextReplacementPlugin, ): 忽略多语言等文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
const TerserPlugin = require('terser-webpack-plugin');
const CompressionWebpackPlugin = require("compression-webpack-plugin");
module.exports = {
optimization: {
// emitOnErrors: true, // 默认false。设为ture 关键错误会被 emit 到生成的代码中
minimizer: [
new TerserPlugin({
parallel: true,
sourceMap: true, // 如果在生产环境中使用 source-maps,必须设置为 true
terserOptions: {
// https://github.com/webpack-contrib/terser-webpack-plugin#terseroptions
},
}),
],
splitChunks: {
chunks: 'async',
minSize: 20000,
minRemainingSize: 0,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 30,
maxInitialRequests: 30,
enforceSizeThreshold: 50000,
cacheGroups: {
libs: {
name: "chunk-libs",
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: "initial" // only package third parties that are initially dependent
},
elementUI: {
name: "chunk-elementUI", // split elementUI into a single package
priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
},
commons: {
name: "chunk-commons",
test: resolve("src/components"), // can customize your rules
minChunks: 3, // minimum common number
priority: 5,
reuseExistingChunk: true
}
},
},
},
plugins: [
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
CompressionWebpackPlugin({
filename: "[path].gz[query]", // 压缩后的文件策略
algorithm: "gzip", // 压缩方式
test: new RegExp(`\\.(${productionGzipExtensions.join("|")})$`), // 可设置需要压缩的文件类型
threshold: 8192, // 大于8kb的文件启用压缩
minRatio: 0.8 // 压缩比率大于等于0.8时不进行压缩
})
]
};

ignoreWarnings:忽略掉特定的警告。
NoEmitOnErrorsPlugin: production mode默认。

MinChunkSizePlugin:保持包的大小。

ImageMinimizerWebpackPlugin: 图片压缩。
可以提前压缩,或者上传到cdn服务器。
svg,json 等文件也都可以压缩。

有些loader和插件 也可以设置压缩。
比如html-loader 的minimize:在生产模式下为 true,其他情况为 false。

ui组件,utils, polyfill等按需加载。

babel.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
"presets": ["@babel/preset-env"],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
],
[
'import',
{
libraryName: 'vant',
libraryDirectory: 'es',
style: true
},
'vant'
],
[
'@xxx/babel-plugin-utils',
{
library: '@xxxx/xxxx-utils'
}
],
]
}

代码/文件抽离

入口起点:最简单直观的分离代码的方式。但是会chunk 之间包含一些重复的模块
防止重复:这样可以在多个 chunk 之间共享模块。entry.index.dependOn: shared

SplitChunksPlugin:将公共的依赖模块提取到已有的入口/新生成 chunk 中。

从 webpack v4 开始,移除了 CommonsChunkPlugin,取而代之的是 optimization.splitChunks。

MiniCssExtractPlugin 与 extract-text-webpack-plugin 相比:
异步加载;没有重复的编译(性能);更容易使用;特别针对 CSS 开发

动态导入import() 和requirec.ensure
PrefetchPlugin 和 PreloadPlugin
import(/* webpackPrefetch: true */ './path/to/LoginModal.js');

懒加载和按需加载。

常用不经常改动的可以用cdn(+ externals),或者dllPlugin。
图片等资源也可以考虑从构建中移出。

DllPlugin 和 DllReferencePlugin 用某种方法实现了拆分 bundles,同时还大幅度提升了构建的速度。

注意:某个依赖只在少数或一个模块中使用,可以动态加载。不需要DllPlugin。

source map: devtool。包括loader和插件。
比如:css-loader,默认情况下取决于 devtool 选项。还有TerserPlugin。

未完待续。

Tree Shaking

optimization.providedExports: 默认启用。

optimization.usedExports:

optimization.sideEffects: 只使用手动 flag,并且不对源码进行分析.在非生产环境默认使用。

es module

构建现代浏览器的module。

PWA

开发环境

充分利用电脑性能,或减少滥用。

增量编译: watch
在内存中编译: webpack-dev-server, webpack-hot-middleware,..
stats.toJson 加速
Devtool: 在大多数情况下,最佳选择是 eval-cheap-module-source-map。
避免在生产环境下才会用到的工具。
最小化 entry chunk : optimization.runtimeChunk: true.
避免额外的优化步骤。
输出结果不携带路径信息。

DevServer

devServer.compress: 为每个静态文件开启 gzip compression
devServer.writeToDisk
devServer.watchOptions: 见下

watchOptions

watchOptions.aggregateTimeout:当第一个文件更改,会在重新构建前增加延迟。
watchOptions.ignored:['**/files/**/*.js', '**/node_modules']
watchOptions.poll: 1500

lib

node端

通常不需要打包。
如果确实需要,同上生产环境。
需要注意避开内置模块。如有用到不属于cmd模块规范的依赖,注意引入方式。

SMTC

附一份vue.config.js配置文件。

webpack 配置项很多。

knowledge is no pay,reward is kindness
0%