一、前言
从 0 到 1 学习的敌人可参考前置学习文章:
- 学习 Webpack5 之路(根底篇)
- 学习 Webpack5 之路(实际篇)
前置文章 学习 Webpack5 之路(根底篇)对 webpack 的概念做了简略介绍,学习 Webpack5 之路(实际篇)则从配置着手,用 webpack 搭建了一个 SASS + TS + React 的我的项目。
本篇将从优化开发体验、放慢编译速度、减小打包体积、放慢加载速度 4 个角度登程,介绍如何对 webpack 我的项目进行优化。
本文依赖的 webpack 版本信息如下:
- webpack-cli@4.7.2
- webpack@5.46.0
二、优化效率工具
在优化开始之前,须要做一些筹备工作。
装置以下 webpack 插件,帮忙咱们剖析优化效率:
- progress-bar-webpack-plugin:查看编译进度;
- speed-measure-webpack-plugin:查看编译速度;
- webpack-bundle-analyzer:打包体积剖析。
1. 编译进度条
一般来说,中型我的项目的首次编译工夫为 5-20s,没个进度条等得多焦急,通过 progress-bar-webpack-plugin 插件查看编译进度,不便咱们把握编译状况。
装置:
npm i -D progress-bar-webpack-plugin
webpack.common.js
配置形式如下:
const chalk = require("chalk");
const ProgressBarPlugin = require("progress-bar-webpack-plugin");
module.exports = {
plugins: [
// 进度条
new ProgressBarPlugin({format: ` :msg [:bar] ${chalk.green.bold(":percent")} (:elapsed s)`,
}),
],
};
贴心的为进度百分比增加了加粗和绿色高亮态款式。
蕴含内容、进度条、进度百分比、耗费工夫,进度条成果如下:
2. 编译速度剖析
优化 webpack 构建速度,首先须要晓得是哪些插件、哪些 loader 耗时长,不便咱们针对性的优化。
通过 speed-measure-webpack-plugin 插件进行构建速度剖析,能够看到各个 loader、plugin 的构建时长,后续可针对耗时 loader、plugin 进行优化。
装置:
npm i -D speed-measure-webpack-plugin
webpack.dev.js
配置形式如下:
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap({// ...webpack config...});
蕴含各工具的构建耗时,成果如下:
留神:这些灰色文字的款式,是因为我在 vscode 终端运行的,导致有色彩的字体都显示为灰色,换个终端就好了,如 iTerm2。
3. 打包体积剖析
同样,优化打包体积,也须要先剖析各个 bundle 文件的占比大小,来进行针对优化。
应用 webpack-bundle-analyzer 查看打包后生成的 bundle 体积剖析,将 bundle 内容展现为一个便捷的、交互式、可缩放的树状图模式。帮忙咱们剖析输入后果来查看模块在何处完结。
装置:
npm i -D webpack-bundle-analyzer
webpack.prod.js
配置形式如下:
const BundleAnalyzerPlugin =
require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
module.exports = {
plugins: [
// 打包体积剖析
new BundleAnalyzerPlugin(),],
};
蕴含各个 bundle 的体积剖析,成果如下:
三、优化开发体验
1. 自动更新
自动更新 指的是,在开发过程中,批改代码后,无需手动再次编译,能够主动编译代码更新编译后代码的性能。
webpack 提供了以下几种可选形式,实现自动更新性能:
- webpack’s Watch Mode
- webpack-dev-server
- webpack-dev-middleware
webpack 官网举荐的形式是 webpack-dev-server
,在 学习 Webpack5 之路(实际篇)– DevServer 章节 曾经介绍了 webpack-dev-server 帮忙咱们在代码发生变化后主动编译代码实现 自动更新 的用法,在这里不反复赘述。
这是针对开发环境的优化,批改
webpack.dev.js
配置。
2. 热更新
热更新 指的是,在开发过程中,批改代码后,仅更新批改局部的内容,无需刷新整个页面。
2.1 批改 webpack-dev-server 配置
应用 webpack 内置的 HMR 插件,更新 webpack-dev-server 配置。
webpack.dev.js
配置形式如下:
module.export = {
devServer: {
contentBase: "./dist",
hot: true, // 热更新
},
};
2.2 引入 react-refresh-webpack-plugin
应用 react-refresh-webpack-plugin 热更新 react 组件。
装置:
npm install -D @pmmmwh/react-refresh-webpack-plugin react-refresh
webpack.dev.js
配置形式如下:
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
module.exports = {
plugins: [new webpack.HotModuleReplacementPlugin(),
new ReactRefreshWebpackPlugin(),],
};
遇到的问题:
配置了 SpeedMeasurePlugin 后,热更新就有效了,会提醒 runtime is undefined
。
解决方案:
仅在剖析构建速度时关上 SpeedMeasurePlugin 插件,这里咱们先敞开 SpeedMeasurePlugin 的应用,来查看热更新成果。
最终成果:
更新 react 组件代码时,无需刷新页面,仅更新组件局部。
四、放慢构建速度
1. 更新版本
1.1 webpack 版本
应用最新的 webpack 版本,通过 webpack 本身的迭代优化,来放慢构建速度。
这一点是十分无效的,如 webpack5 较于 webpack4,新增了长久化缓存、改良缓存算法等优化,webpack5 新个性可查看 参考资料。
1.2 包管理工具版本
将 Node.js、package 管理工具(例如 npm
或者 yarn
)更新到最新版本,也有助于进步性能。较新的版本可能建设更高效的模块树以及进步解析速度。
本文依赖的版本信息如下:
webpack@5.46.0
node@14.15.0
npm@6.14.8
2. 缓存
2.1 cache
通过配置 webpack 长久化缓存 cache: filesystem
,来缓存生成的 webpack 模块和 chunk,改善构建速度。
简略来说,通过 cache: filesystem
能够将构建过程的 webpack 模板进行缓存,大幅晋升二次构建速度、打包速度,当构建忽然中断,二次进行构建时,能够间接从缓存中拉取,可提速 90% 左右。
webpack.common.js
配置形式如下:
module.exports = {
cache: {type: "filesystem", // 应用文件缓存},
};
引入缓存后,首次构建工夫将减少 15%,二次构建工夫将缩小 90%,成果如下:
2.2 dll ❌
在 webpack 官网构建性能 中看到对于 dll 的介绍:
dll 能够为更改不频繁的代码生成独自的编译后果。能够进步应用程序的编译速度。
我灰溜溜的开始寻找 dll 的相干配置阐明,太简单了,接着找到了一个辅助配置 dll 的插件 autodll-webpack-plugin,后果下面间接写了 webpack5 开箱即用的长久缓存是比 dll 更优的解决方案。
所以,不必再配置 dll 了,下面介绍的 cache 显著更香。
2.3 cache-loader ❌
没错,cache-loader 也不须要引入了,下面的 cache 曾经帮忙咱们缓存了。
3. 缩小 loader、plugins
每个的 loader、plugin 都有其启动工夫。尽量少地应用工具,将非必须的 loader、plugins 删除。
3.1 指定 include
为 loader 指定 include,缩小 loader 利用范畴,仅利用于起码数量的必要模块,。
webpack 构建性能文档
rule.exclude 能够排除模块范畴,也可用于缩小 loader 利用范畴.
webpack.common.js
配置形式如下:
module.exports = {
rules: [
{test: /\.(js|ts|jsx|tsx)$/,
include: paths.appSrc,
use: [
{
loader: "esbuild-loader",
options: {
loader: "tsx",
target: "es2015",
},
},
],
},
],
};
定义 loader 的 include 后,构建工夫将缩小 12%,成果如下:
3.2 治理资源
应用 webpack 资源模块 (asset module) 代替旧的 assets loader(如 file-loader
/url-loader
/raw-loader
等),缩小 loader 配置数量。
配置形式如下:
module.exports = {
rules: [
{test: /\.(png|svg|jpg|jpeg|gif)$/i,
include: [paths.appSrc],
type: "asset/resource",
},
],
};
引入资源模块后,构建工夫将缩小 7%,成果如下:
4. 优化 resolve 配置
resolve 用来配置 webpack 如何解析模块,可通过优化 resolve 配置来笼罩默认配置项,缩小解析范畴。
4.1 alias
alias 能够创立 import
或 require
的别名,用来简化模块引入。
webpack.common.js
配置形式如下:
module.exports = {
resolve: {
alias: {"@": paths.appSrc, // @ 代表 src 门路},
},
};
4.2 extensions
extensions 示意须要解析的文件类型列表。
依据我的项目中的文件类型,定义 extensions,以笼罩 webpack 默认的 extensions,放慢解析速度。
因为 webpack 的解析程序是从左到右,因而要将应用频率高的文件类型放在左侧,如下我将 tsx
放在最左侧。
webpack.common.js
配置形式如下:
module.exports = {
resolve: {extensions: [".tsx", ".js"], // 因为我的我的项目只有这两种类型的文件,如果有其余类型,须要增加进去。},
};
4.3 modules
modules 示意 webpack 解析模块时须要解析的目录。
指定目录可放大 webpack 解析范畴,放慢构建速度。
webpack.common.js
配置形式如下:
module.exports = {modules: ["node_modules", paths.appSrc],
};
4.4 symlinks
如果我的项目不应用 symlinks(例如 npm link
或者 yarn link
),能够设置 resolve.symlinks: false
,缩小解析工作量。
webpack.common.js
配置形式如下:
module.exports = {
resolve: {symlinks: false,},
}
优化 resolve 配置后,构建工夫将缩小 1.5%,成果如下:
5. 多过程
上述能够看到 sass-loader 的构建工夫有 1.56s,占据了整个构建过程的 60%,那么有没有办法来放慢 sass-loader 的构建速度呢?
能够通过多过程来实现,试想将 sass-loader 放在一个独立的 worker 池中运行,就不会妨碍其余 loader 的构建了,能够大大放慢构建速度。
5.1 thread-loader
通过 thread-loader 将耗时的 loader 放在一个独立的 worker 池中运行,放慢 loader 构建速度。
装置:
npm i -D thread-loader
webpack.common.js
配置形式如下:
module.exports = {
rules: [
{test: /\.module\.(scss|sass)$/,
include: paths.appSrc,
use: [
"style-loader",
{
loader: "css-loader",
options: {
modules: true,
importLoaders: 2,
},
},
{
loader: "postcss-loader",
options: {
postcssOptions: {plugins: [["postcss-preset-env"]],
},
},
},
{
loader: "thread-loader",
options: {workerParallelJobs: 2,},
},
"sass-loader",
].filter(Boolean),
},
],
};
webpack 官网 提到
node-sass
中有个来自 Node.js 线程池的阻塞线程的 bug。当应用thread-loader
时,须要设置workerParallelJobs: 2
。
因为 thread-loader 引入后,须要 0.6s 左右的工夫开启新的 node 过程,本我的项目代码量小,可见引入 thread-loader 后,构建工夫反而减少了 0.19s。
因而,咱们应该仅在十分耗时的 loader 前引入 thread-loader。
成果如下:
5.2 happypack ❌
happypack 同样是用来设置多线程,然而在 webpack5 就不要再应用 happypack 了,官网也曾经不再保护了,举荐应用上文介绍的 thread-loader。
6. 辨别环境
在 学习 Webpack5 之路(实际篇)– 模式(mode)章节 曾经介绍了 webpack 的不同模式的内置优化。
在开发过程中,切忌在开发环境应用生产环境才会用到的工具,如在开发环境下,应该排除 [fullhash]
/[chunkhash]
/[contenthash]
等工具。
同样,在生产环境,也应该防止应用开发环境才会用到的工具,如 webpack-dev-server 等插件。
7. 其余
7.1 devtool
不同的 devtool
设置,会导致性能差别。
在大多数状况下,最佳抉择是 eval-cheap-module-source-map
。
具体辨别可至 webpack devtool 查看。
webpack.dev.js
配置形式如下:
export.module = {devtool: 'eval-cheap-module-source-map',}
7.2 输入后果不携带门路信息
默认 webpack 会在输入的 bundle 中生成门路信息,将门路信息删除可小幅晋升构建速度。
module.exports = {
output: {pathinfo: false,},
};
}
四、减小打包体积
1. 代码压缩
体积优化第一步是压缩代码,通过 webpack 插件,将 JS、CSS 等文件进行压缩。
1.1 JS 压缩
应用 TerserWebpackPlugin 来压缩 JavaScript。
webpack5 自带最新的 terser-webpack-plugin
,无需手动装置。
terser-webpack-plugin
默认开启了 parallel: true
配置,并发运行的默认数量:os.cpus().length - 1
,本文配置的 parallel 数量为 4,应用多过程并发运行压缩以进步构建速度。
webpack.prod.js
配置形式如下:
const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
optimization: {
minimizer: [
new TerserPlugin({
parallel: 4,
terserOptions: {
parse: {ecma: 8,},
compress: {
ecma: 5,
warnings: false,
comparisons: false,
inline: 2,
},
mangle: {safari10: true,},
output: {
ecma: 5,
comments: false,
ascii_only: true,
},
},
}),
],
},
};
体积减小 10%,成果如下:
1.1 ParallelUglifyPlugin ❌
你可能有听过 ParallelUglifyPlugin 插件,它能够帮忙咱们多过程压缩 JS,webpack5 的 TerserWebpackPlugin 默认就开启了多过程和缓存,无需再引入 ParallelUglifyPlugin。
1.2 CSS 压缩
应用 CssMinimizerWebpackPlugin 压缩 CSS 文件。
和 optimize-css-assets-webpack-plugin 相比,css-minimizer-webpack-plugin 在 source maps 和 assets 中应用查问字符串会更加精确,而且反对缓存和并发模式下运行。
CssMinimizerWebpackPlugin
将在 Webpack 构建期间搜寻 CSS 文件,优化、压缩 CSS。
装置:
npm install -D css-minimizer-webpack-plugin
webpack.prod.js
配置形式如下:
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
optimization: {
minimizer: [
new CssMinimizerPlugin({parallel: 4,}),
],
},
};
因为 CSS 默认是放在 JS 文件中,因而本示例是基于下章节将 CSS 代码拆散后的成果。
2. 代码拆散
代码拆散可能把代码拆散到不同的 bundle 中,而后能够按需加载或并行加载这些文件。代码拆散能够用于获取更小的 bundle,以及管制资源加载优先级,能够缩短页面加载工夫。
2.1 抽离反复代码
SplitChunksPlugin 插件开箱即用,能够将公共的依赖模块提取到已有的入口 chunk 中,或者提取到一个新生成的 chunk。
webpack 将依据以下条件主动拆分 chunks:
- 新的 chunk 能够被共享,或者模块来自于
node_modules
文件夹; - 新的 chunk 体积大于 20kb(在进行 min+gz 之前的体积);
- 当按需加载 chunks 时,并行申请的最大数量小于或等于 30;
- 当加载初始化页面时,并发申请的最大数量小于或等于 30;
通过 splitChunks 把 react 等公共库抽离进去,不反复引入占用体积。
留神:切记不要为 cacheGroups 定义固定的 name,因为 cacheGroups.name 指定字符串或始终返回雷同字符串的函数时,会将所有常见模块和 vendor 合并为一个 chunk。这会导致更大的初始下载量并减慢页面加载速度。
webpack.prod.js
配置形式如下:
module.exports = {
splitChunks: {
// include all types of chunks
chunks: "all",
// 反复打包问题
cacheGroups: {
vendors: {
// node_modules 里的代码
test: /[\\/]node_modules[\\/]/,
chunks: "all",
// name: 'vendors', 肯定不要定义固定的 name
priority: 10, // 优先级
enforce: true,
},
},
},
};
将公共的模块独自打包,不再反复引入,成果如下:
2.2 CSS 文件拆散
MiniCssExtractPlugin 插件将 CSS 提取到独自的文件中,为每个蕴含 CSS 的 JS 文件创建一个 CSS 文件,并且反对 CSS 和 SourceMaps 的按需加载。
装置:
npm install -D mini-css-extract-plugin
webpack.common.js
配置形式如下:
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {plugins: [new MiniCssExtractPlugin()],
module: {
rules: [
{test: /\.module\.(scss|sass)$/,
include: paths.appSrc,
use: [
"style-loader",
isEnvProduction && MiniCssExtractPlugin.loader, // 仅生产环境
{
loader: "css-loader",
options: {
modules: true,
importLoaders: 2,
},
},
{
loader: "postcss-loader",
options: {
postcssOptions: {plugins: [["postcss-preset-env"]],
},
},
},
{
loader: "thread-loader",
options: {workerParallelJobs: 2,},
},
"sass-loader",
].filter(Boolean),
},
],
},
};
留神:MiniCssExtractPlugin.loader 要放在 style-loader 前面。
成果如下:
2.3 最小化 entry chunk
通过配置 optimization.runtimeChunk = true
,为运行时代码创立一个额定的 chunk,缩小 entry chunk 体积,进步性能。
webpack.prod.js
配置形式如下:
module.exports = {
optimization: {runtimeChunk: true,},
};
}
成果如下:
3. Tree Shaking(摇树)
摇树,顾名思义,就是将枯黄的落叶摇下来,只留下树上活的叶子。枯黄的落叶代表我的项目中未援用的无用代码,活的树叶代表我的项目中理论用到的源码。
3.1 JS
JS Tree Shaking 将 JavaScript 上下文中的未援用代码(Dead Code)移除,通过 package.json
的 "sideEffects"
属性作为标记,向 compiler 提供提醒,表明我的项目中的哪些文件是 “pure(纯正 ES2015 模块)”,由此能够平安地删除文件中未应用的局部。
Dead Code 个别具备以下几个特色:
- 代码不会被执行,不可达到;
- 代码执行的后果不会被用到;
- 代码只会影响死变量(只写不读)。
3.1.1 webpack5 sideEffects
通过 package.json 的 "sideEffects"
属性,来实现这种形式。
{
"name": "your-project",
"sideEffects": false
}
需注意的是,当代码有副作用时,须要将 sideEffects
改为提供一个数组,增加有副作用代码的文件门路:
{
"name": "your-project",
"sideEffects": ["./src/some-side-effectful-file.js"]
}
增加 TreeShaking 后,未援用的代码,将不会被打包,成果如下:
3.1.2 对组件库援用的优化
webpack5 sideEffects 只能革除无副作用的援用,而有副作用的援用则只能通过优化援用形式来进行 Tree Shaking
。
1. lodash
相似 import {throttle} from 'lodash'
就属于有副作用的援用,会将整个 lodash 文件进行打包。
优化形式是应用 import {throttle} from 'lodash-es'
代替 import {throttle} from 'lodash'
,lodash-es 将 Lodash 库导出为 ES 模块,反对基于 ES modules 的 tree shaking,实现按需引入。
2. ant-design
ant-design 默认反对基于 ES modules 的 tree shaking,对于 js 局部,间接引入 import {Button} from 'antd'
就会有按需加载的成果。
如果我的项目中仅引入少部分组件,import {Button} from 'antd'
也属于有副作用,webpack 不能把其余组件进行 tree-shaking。这时能够 放大援用范畴,将引入形式批改为 import {Button} from 'antd/lib/button'
来进一步优化。
3.2 CSS
上述对 JS 代码做了 Tree Shaking 操作,同样,CSS 代码也须要摇摇树,打包时把没有用的 CSS 代码摇走,能够大幅缩小打包后的 CSS 文件大小。
应用 purgecss-webpack-plugin 对 CSS Tree Shaking。
装置:
npm i purgecss-webpack-plugin -D
因为打包时 CSS 默认放在 JS 文件内,因而要联合 webpack 拆散 CSS 文件插件 mini-css-extract-plugin
一起应用,先将 CSS 文件拆散,再进行 CSS Tree Shaking。
webpack.prod.js
配置形式如下:
const glob = require("glob");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const PurgeCSSPlugin = require("purgecss-webpack-plugin");
const paths = require("paths");
module.exports = {
plugins: [
// 打包体积剖析
new BundleAnalyzerPlugin(),
// 提取 CSS
new MiniCssExtractPlugin({filename: "[name].css",
}),
// CSS Tree Shaking
new PurgeCSSPlugin({paths: glob.sync(`${paths.appSrc}/**/*`, {nodir: true}),
}),
],
};
下面为了测试 CSS 压缩成果,我引入了大量有效 CSS 代码,因而 Tree Shaking 成果也非常明显,成果如下:
3. CDN
上述是对 webpack 配置的优化,另一方面还能够通过 CDN 来减小打包体积。
这里引入 CDN 的首要目标为了缩小打包体积,因而仅仅将一部分大的动态资源手动上传至 CDN,并批改本地引入门路。下文的放慢加载速度,将介绍另一种 CDN 优化伎俩。
将大的动态资源上传至 CDN:
- 字体:压缩并上传至 CDN;
- 图片:压缩并上传至 CDN。
五、放慢加载速度
1. 按需加载
通过 webpack 提供的 import() 语法 动静导入 性能进行代码拆散,通过按需加载,大大晋升网页加载速度。
应用形式如下:
export default function App() {
return (
<div>
hello react 111
<Hello />
<button onClick={() => import("lodash")}> 加载 lodash</button>
</div>
);
}
成果如下:
2. 浏览器缓存
浏览器缓存,就是进入某个网站后,加载的动态资源被浏览器缓存,再次进入该网站后,将间接拉取缓存资源,放慢加载速度。
webpack 反对依据资源内容,创立 hash id,当资源内容发生变化时,将会创立新的 hash id。
配置 JS bundle hash,webpack.common.js
配置形式如下:
module.exports = {
// 输入
output: {
// 仅在生产环境增加 hash
filename: ctx.isEnvProduction
? "[name].[contenthash].bundle.js"
: "[name].bundle.js",
},
};
配置 CSS bundle hash,webpack.prod.js
配置形式如下:
module.exports = {
plugins: [
// 提取 CSS
new MiniCssExtractPlugin({filename: "[hash].[name].css",
}),
],
};
配置 optimization.moduleIds,让公共包 splitChunks 的 hash 不因为新的依赖而扭转,缩小非必要的 hash 变动,webpack.prod.js
配置形式如下:
module.exports = {
optimization: {moduleIds: "deterministic",},
};
通过配置 contenthash/hash,浏览器缓存了未改变的文件,仅从新加载有改变的文件,大大放慢加载速度。
3. CDN
将所有的动态资源,上传至 CDN,通过 CDN 减速来晋升加载速度。
webpack.common.js
配置形式如下:
export.modules = {
output: {publicPath: ctx.isEnvProduction ? 'https://xxx.com' : '', // CDN 域名},
}
六、优化前后比照
在仓库代码仅 webpack 配置不同的状况下,查看优化前后比照。
- [优化前 github 地址]()
- [优化后 github 地址]()
1. 构建速度
类型 | 首次构建 | 未修改内容二次构建 | 批改内容二次构建 |
---|---|---|---|
优化前 | 2.7s | 2.7s | 2.7s |
优化后 | 2.7s | 0.5s | 0.3s |
2. 打包体积
类型 | 体积大小 |
---|---|
优化前 | 250 kb |
优化后 | 231 kb |
七、总结
从上章节 [优化前后比照] 可知,在小型我的项目中,增加过多的优化配置,作用不大,反而会因为额定的 loader、plugin 减少构建工夫。
在放慢构建工夫方面,作用最大的是配置 cache,可大大放慢二次构建速度。
在减小打包体积方面,作用最大的是压缩代码、拆散反复代码、Tree Shaking,可最大幅度减小打包体积。
在放慢加载速度方面,按需加载、浏览器缓存、CDN 成果都很显著。
本篇就介绍到这儿啦,有更好的 webpack 优化形式欢送评论区通知我哦~
本文源码:
- webpack Demo2 优化前
- webpack Demo2 优化后
心愿能对你有所帮忙,感激浏览~
别忘了点个赞激励一下我哦,笔芯 ❤️
参考资料
- Tree-Shaking 性能优化实际 – 原理篇
- Tree-Shaking 性能优化实际 – 实际篇
- 三十分钟把握 Webpack 性能优化
- 玩转 webpack,使你的打包速度晋升 90%
- 带你深度解锁 Webpack 系列(优化篇)
- Webpack 5 中的新个性
- 辛辛苦苦学会的 webpack dll 配置,可能曾经过期了
欢送关注凹凸实验室博客:aotu.io
或者关注凹凸实验室公众号(AOTULabs),不定时推送文章。