# webpack 高级
优化 webpack:
- 提升开发体验
- 提升打包构建速度
- 减少代码体积
- 优化代码运行性能
# 1. 提升开发体验
介绍:
- 浏览器运行的是 webpack 编译后的代码,很难看懂
- 一旦出错,报错位置 是 编译后的文件的出错代码位置,定位到源码位置比较困难
- SourceMap 就是编译后的代码与源代码的映射,运行的代码一旦出错,就可以定位到源代码错误位置的行和列
配置:
module.exports = {
mode: 'development',
/*
优点:打包编译速度快,只包含行映射
缺点:没有列映射
*/
devtool: 'cheap-module-source-map',
// ...
};
module.exports = {
mode: 'production',
/*
优点:包含行/列映射
缺点:打包编译速度更慢
*/
devtool: 'source-map',
// ...
};
# 2. 提升打包构建速度
# 2.1. HMR
说明:
- HotModuleReplacement (HMR/热模块替换)
- 在程序运行中,替换、添加或删除模块,而无需重新加载整个页面。
CSS 热替换:
配置
devServer = { // ... hot: true, // 默认已开启 }
style-loader 已经实现热替换接口,一旦样式发生变化,就用新的
<style>
替换掉旧的。
JS 热替换:
默认情况下,一旦更改 JS 代码,就会刷新页面
手动将某个模块添加到热替换
if (module.hot) { // 一旦 sum.js 发生变化,就会将新模块重新执行一次 // 如果此模块有副作用,就会出问题 module.hot.accept('./js/sum'); }
# 2.2. oneOf
说明:
- 打包时每个文件都要把所有 rule.test 过一遍,看是否匹配,匹配则执行相应 loader
- oneOf 用于,一旦匹配第一个就停止,不再继续匹配下去
配置:
module.rules = [
// 把所有规则包在一个 rule 里,用 oneOf
{
oneOf: [
{ test: /\.css$/, /* ... */ },
{ test: /\.less$/, /* ... */ },
{ test: /\.(png|jpe?g|gif|webp)$/, /* ... */ },
{ test: /\.(ttf|woff2?)$/, /* ... */ },
// ...
]
}
]
备注:
- 针对文件特别多的项目,打包速度是有提升的
参考:
# 2.3. include / exclude
npm 安装的第三方库都在 node_modules 里,这些文件通常都是编译好的,不需要我们额外编译。
src 目录下的才是真正需要处理的。
以下两个配置,只能选择一个:
- include: 包含
- exclude: 排除
include/exclude 值:
- string: To match the input must start with the provided string. I. e. an absolute directory path, or absolute path to the file.
- RegExp: It's tested with the input.
- function: It's called with the input and must return a truthy value to match.
- array: At least one of the Conditions must match.
- object: All properties must match. Each property has a defined behavior.
{ and: [Condition] }
: All Conditions must match.{ or: [Condition] }
: Any Condition must match.{ not: [Condition] }
: All Conditions must NOT match.
示例:
module.rules = [
{ include: path.resolve(ROOT_PATH, './src'), },
{ exclude: /node_modules/, },
{ exclude: (...args) => { /* ... */ return true; } },
{
include: [
path.resolve(ROOT_PATH, './src'),
/node_modules[/\]xyz/,
],
},
{
include: {
and: [/* ... */],
or: [/* ... */],
not: [/* ... */],
}
}
]
参考:
# 2.4. cache
说明:
- 缓存 eslint 检查结果、babel 的编译结果,再次打包速度会更快
配置:
module.rules = [
// ...
{
test: /\.js$/,
loader: 'babel-loader',
options: {
/*
the default cache directory in node_modules/.cache/babel-loader
Default false
*/
cacheDirectory: true,
/*
When set, each Babel transform output will be compressed with Gzip
Default true
*/
cacheCompression: false,
},
},
]
plugins = [
new ESLintWebpackPlugin({
/*
The cache is enabled by default to decrease execution time.
*/
cache: true,
/*
Specify the path to the cache location. Can be a file or a directory.
Default: node_modules/.cache/eslint-webpack-plugin/.eslintcache
*/
cacheLocation: path.resolve(ROOT_PATH, './node_modules/.cache/.eslintcache'),
}),
]
参考:
# 2.5. thead
说明:
- 提升打包速度,就是提升处理 JS 的速度
- JS 的处理,主要涉及的工具 —— eslint、babel、Terser(JS 压缩,已内置,生产默认开启)
- 可以开启多线程同时使用这些工具处理 JS
注意:
每个进程启动就有大约要 600ms,在特别耗时的操作中使用比较好
可以启动的进程数,就是 CPU 的核数
const os = require('os'); const threads = os.cpus().length; console.log('CPU 核数:', threads); // 4
安装:
npm i -D thread-loader
## "thread-loader": "^4.0.1",
配置:
const os = require('os');
const TerserPlugin = require('terser-webpack-plugin');
const THREADS = os.cpus().length - 1;
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.js$/,
use: [
{
/*
Runs the following loaders in a worker pool.
*/
loader: 'thread-loader',
options: {
/*
the number of spawned workers,
defaults to (number of cpus - 1)
or fallback to 1 when require('os').cpus() is undefined
*/
workers: THREADS,
},
},
{ loader: 'babel-loader' },
],
},
]
},
plugins: [
new ESLintWebpackPlugin({
/*
Will run lint tasks across a thread pool.
The pool size is automatic unless you specify a number.
Default: false
*/
threads: THREADS, // boolean | number
}),
],
optimization: {
// 压缩相关的放这里
minimizer: [
/*
生产模式下,已开启此插件,额外配置的话就需要重新写
*/
new TerserPlugin({
/*
Use multi-process parallel running to improve the build speed.
Default: true.
Default number of concurrent runs: os.cpus().length - 1
*/
parallel: THREADS, // boolean | number
}),
],
},
}
参考:
- thread-loader (opens new window)
- terser-webpack-plugin (opens new window)
- eslint-webpack-plugin/#threads (opens new window)
# 3. 减少代码体积
# 3.1. Tree Shaking
说明:
- 摇树,移除未使用的 JS 代码
- 特别是第三方 工具函数库、组件库,我们只使用了极少部分,却引入了全部
- webpack 默认已开启此功能
- 针对 ES Module
参考:
# 3.2. Babel
说明:
- 默认情况下,babel 会给所有编辑的文件添加相同的辅助代码(运行时代码),如
_extend
- 可以使用
@babel/plugin-transform-runtime
插件,将运行时代码作为一个单独的模块引入,避免为每个编译的文件都引入
安装:
npm i -D @babel/plugin-transform-runtime
## "@babel/plugin-transform-runtime": "^7.21.4",
配置:(babel.config.js
)
module.exports = {
presets: [ '@babel/preset-env' ],
plugins: [
/*
A plugin that enables the re-use of Babel's injected helper code to save on codesize.
*/
'@babel/plugin-transform-runtime'
]
};
参考:
# 3.3. Image Minimizer
说明:
- 压缩图片
安装:
# image-minimizer-webpack-plugin 使用 imagemin 工具
npm i -D image-minimizer-webpack-plugin imagemin
## "image-minimizer-webpack-plugin": "^3.8.2",
## "imagemin": "^8.0.1",
# imagemin 使用以下插件处理图片
# gif svg
npm i -D imagemin-gifsicle imagemin-svgo
## "imagemin-gifsicle": "^7.0.0",
## "imagemin-svgo": "^10.0.1",
# 无损压缩 (jpg png)
npm i -D imagemin-jpegtran imagemin-optipng
## "imagemin-jpegtran": "^7.0.0",
## "imagemin-optipng": "^8.0.0",
# 有损压缩 (jpg png)
npm i -D imagemin-mozjpeg imagemin-pngquant
配置:
const ImageMinimizerPlugin = require('image-minimizer-webpack-plugin');
optimization.minimizer = [
// ...
new ImageMinimizerPlugin({
minimizer: {
implementation: ImageMinimizerPlugin.imageminGenerate,
options: {
plugins: [
['gifsicle', { interlaced: true }],
['jpegtran', { progressive: true }],
['optipng', { optimizationLevel: 5 }],
[
'svgo',
{
plugins: [
'preset-default',
'prefixIds',
{
name: 'sortAttrs',
params: {
xmlnsOrder: 'alphabetical',
},
},
],
},
],
],
},
},
}),
]
备注:
- 安装包时,如果出现
jpegtran.exe
、optipng.exe
找不到,可以到官网下载,然后放到指定目录 - 如果出现 gifsicle 安装失败,请参考:
getaddrinfo ENOENT raw.githubusercontent.com
, 参考 “解决访问github慢的问题” 这篇文章- 参考 解决npm install各种报错的6种方案 Error: Command failed: cmd.exe autoreconf -ivf以及gifsicle pre-build test fail (opens new window)
参考:
- image-minimizer-webpack-plugin (opens new window)
- jpegtran 官网地址 (opens new window)
- OptiPNG 官网地址 (opens new window)
# 4. 优化代码运行性能
# 4.1. Code Split
# 4.1.1. 说明
- 将所有 JS 文件打包到一个文件中,体积太大了,使加载时间变长了
- code split
- 分割文件:将打包的文件分成多个
- 按需加载:需要哪个就加载哪个
# 4.1.2. module、chunk、bundle
- module: ESM、CommonJS
- chunk: 1. 入口点; 2. 异步载入(
import()
); 3.splitChunks.cacheGroups.{cacheGroup}
- bundle: 最终生成的文件
参考:Webpack 理解 Chunk (opens new window)
# 4.1.3. 动态导入(按需加载)
import('../common/math')
.then(({ add, default: math }) => {
console.log(add(1, 2))
})
# 4.1.4. ESLint 配置(import() 语法
)
module.exports = {
// 如果不支持 `import()` 语法,则配置以下插件
plugins: [ 'import' ]
}
# 4.1.5. 异步 chunk 命名
webpack.config.js:
参考:output/#outputchunkfilename (opens new window)
module.exports = { output: { // This option determines the name of each output bundle. filename: 'static/js/main.js', // This option determines the name of non-initial chunk files. // 就算设置了 `[name]`,webpack 还是以 `[id]` 命令,这时就需要设置 Magic Comments chunkFilename: 'static/js/[name].js' } }
Magic Comments:
参考:magic-comments (opens new window)
import( /* webpackChunkName: "division" */ './js/division' ).then(({default: divide}) => { console.log(divide(4, 2)); })
# 4.1.6. splitChunks
默认配置:
module.exports = {
//...
optimization: {
// This configuration object represents the default behavior of the SplitChunksPlugin
splitChunks: {
/*
This indicates which chunks will be selected for optimization
'initial': 初始 chunk, 入口点 chunk
'async': 异步 chunk
'all': 'initial' + 'async'
*/
chunks: 'async',
/*
Minimum size, in bytes, for a chunk to be generated.
*/
minSize: 20000,
/*
拆分后剩余的块的最小体积,开发模式默认为 0,生产模式默认为 minSize
*/
minRemainingSize: 0,
/*
The minimum times must a module be shared among chunks before splitting.
*/
minChunks: 1,
/*
Maximum number of parallel requests when on-demand loading. (按需加载时的最大并行请求数。)
*/
maxAsyncRequests: 30,
/*
Maximum number of parallel requests at an entry point.
*/
maxInitialRequests: 30,
/*
强制拆的阈值,默认大于 50KB 就强行拆
忽略 minRemainingSize, maxAsyncRequests, maxInitialRequests
*/
enforceSizeThreshold: 50000,
cacheGroups: {
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
reuseExistingChunk: true,
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
},
};
配置:
module.exports = {
// ...
optimization: {
// ...
splitChunks: {
chunks: 'all',
}
},
}
参考:
# 4.1.7. 统一命名
config | value | desc |
---|---|---|
output.filename | 'static/js/[name].js' | entry (non-initial) |
output.chunkFilename | 'static/js/[name].chunk.js' | non-initial chunk files |
output.assetModuleFilename | 'static/media/[name].[hash:8][ext][query]' | image,audio,video files |
MiniCssExtractPlugin.filename | 'static/css/[name].css' | css file |
MiniCssExtractPlugin.chunkFilename | 'static/css/[name].chunk.css' | non-initial chunk css file |
备注:
- Asset Modules: rule.type 的值为
asset
,asset/**
- Asset Modules 可以使用 output.assetModuleFilename 统一配置名称规则
- 使用
[name]
是避免多入口时名称冲突
参考:
- output.assetModuleFilename (opens new window)
- Asset Modules (opens new window)
- template-strings (opens new window)
# 4.2. Preload / Prefetch
说明:
- 在浏览器空闲时,加载后续需要使用的资源
- 加载资源,缓存,不执行
区别:
- | preload | prefetch |
---|---|---|
加载时机 | 立即加载资源 | 空闲时加载资源 |
优先级 | 高 | 低 |
资源 | 只能加载当前页面后续需要使用的资源 | 可以加载当前页面后续需要使用的资源,也可以加载下一个页面需要使用的资源 |
使用场景 | 当前页面优先级高的资源 | 下一个页面需要使用的资源 |
兼容性 | 95.81%, 详情 (opens new window) | 78.76%, IE11+, 详情 (opens new window) |
安装:
npm i -D @vue/preload-webpack-plugin
## "@vue/preload-webpack-plugin": "^2.0.0",
配置:
const PreloadWebpackPlugin = require('@vue/preload-webpack-plugin');
module.exports = {
// ...
plugins: [
// ...
new PreloadWebpackPlugin({
/*
预加载的类型: 'preload' (默认), 'prefetch'
*/
rel: 'prefetch',
/*
设置 as 的值,默认依赖文件的后缀名来设置,也可以显式制定
*/
// as: 'script',
/*
哪些类别的 chunk 需要预加载: asyncChunks (默认值), all, initial
*/
// include: 'asyncChunks',
/*
排除不需要预加载的 chunk,如 sourcemaps ,默认值: [/\.map/]
*/
// fileBlacklist: [/\.map/]
}),
]
};
参考:
# 4.3. Network Cache
解决的问题:
- 当缓存的资源发生变化时,能加载新的资源
- 当一个文件发生变化时,不会影响到使用该文件的其他文件
# 4.3.1. hash
说明:
- 静态资源默认走浏览器强缓存,如果资源的 URL 不变化,不会再去服务器请求该资源
- 为了使强缓存失效,必须改变 URL,要么改 query string 要么改文件名
- webpack 给文件名添加 hash 值,一旦文件内容变化 hash 值就改变
hash:
fullhash
(webpack4 是hash
)- 修改任意一个文件,所有文件的 hash 都会改变
chunkhash
- 根据 chunk 生成 hash,chunk 内的 JS、CSS 文件共享这个 hash
contenthash
- 根据文件内容生成 hash,只有文件内容变化了 hash 才会改变
- 所有文件的 hash 都是独享的
配置:
config | value | desc |
---|---|---|
output.filename | 'static/js/[name].[contenthash:8].js' | entry (non-initial) |
output.chunkFilename | 'static/js/[name].[contenthash:8].chunk.js' | non-initial chunk files |
output.assetModuleFilename | 'static/media/[name].[hash][ext][query]' | image,audio,video files |
MiniCssExtractPlugin.filename | 'static/css/[name].[contenthash:8].css' | css file |
MiniCssExtractPlugin.chunkFilename | 'static/css/[name].[contenthash:8].chunk.css' | non-initial chunk css file |
参考:
# 4.3.2. runtimeChunk
问题:
源文件:
src/ common.js main.js main.js: import { add } from './common' console.log(add(1, 2))
编译后:
dist/ common.111.js main.222.js main.222.js: import { add } from './common.111' console.log(add(1, 2))
当
common.js
发生变化后,重新编译:dist/ common.333.js main.444.js main.444.js: import { add } from './common.333' console.log(add(1, 2))
为了避免修改一个文件,导致其它文件跟着变化,我们需要把 文件和 hash 的对应关系存起来,
这样,一旦文件变化,只会重新请求改变的文件和关系文件。
配置:
module.exports = {
optimization: {
/*
true or 'multiple'
adds an additional chunk containing only the runtime to each entrypoint.
This setting is an alias for:
{
name: (entrypoint) => `runtime~${entrypoint.name}`,
}
'single'
creates a runtime file to be shared for all generated chunks.
This setting is an alias for:
{
name: 'runtime',
}
*/
runtimeChunk: {
name: (entrypoint) => `runtime~${entrypoint.name}`,
},
}
}
参考:
# 4.4. core-js
说明:
@babel/preset-env
只能处理 ES6 语法,对 ES6 新增的 API 不做处理core-js
就是专门处理 ES6 API 兼容性的 polyfill (垫片/补丁)
注意:
如果使用 Promise 后,ESLint 报错
const promise = Promise.resolve(); promise.then(() => { console.log("hello promise"); });
请安装 @babel/eslint-parser
npm i -D @babel/eslint-parser
并配置 ESLint
module.exports = {
// 支持最新的最终 ECMAScript 标准
parser: '@babel/eslint-parser',
};
安装:
npm i core-js
## "core-js": "^3.30.1"
手动全部引入 polyfill:
import 'core-js';
手动按需引入 polyfill:
import 'core-js/es/promise';
自动按需引入,配置 babel.config.js:
module.exports = {
presets: [
['@babel/preset-env', {
/*
Adds specific imports for polyfills when they are used in each file.
*/
useBuiltIns: 'usage',
/*
string or { version: string, proposals: boolean }, defaults to "2.0".
The version string can be any supported core-js versions. For example, "3.8" or "2.0".
*/
corejs: {
/*
It is recommended to specify the minor version otherwise "3" will be interpreted as "3.0"
*/
version: '3.30',
/*
This will enable polyfilling of every proposal supported by core-js@3.30
*/
proposals: true
}
}]
],
};
备注:
- babel 的编译,依赖 browserslist 的设置
参考:
# 4.5. PWA
说明:
- progressive web application, 渐进式网络应用程序
- 离线后,刷新网页也可以继续访问
- 静态资源都被缓存了,启动 ServiceWorkers 提供服务
安装:
npm i -D workbox-webpack-plugin
## "workbox-webpack-plugin": "^6.5.4"
配置:
const WorkboxPlugin = require('workbox-webpack-plugin');
module.exports = {
// ...
plugins: [
new WorkboxPlugin.GenerateSW({
/*
这些选项帮助快速启用 ServiceWorkers,不允许遗留任何“旧的” ServiceWorkers
*/
clientsClaim: true,
skipWaiting: true,
}),
]
};
使用:(在 main.js 中注册)
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('./service-worker.js')
.then((registration) => {
console.log('SW registered: ', registration);
})
.catch((registrationError) => {
console.log('SW registration failed: ', registrationError);
});
});
}
备注:
可以在 Cache Storage 中查看缓存的内容,具体位置如下:
Chrome -> Developer tool -> Application 选项卡 Storage/ Cache Storage/ workbox-precache-v2-http://127.0.0.1:8080
参考:
- progressive-web-application (opens new window)
- https://caniuse.com/?search=serviceWorker (opens new window)
# 5. 总结
从四个角度对 webpack 和代码进行了优化:
提升开发体验
- 使用
Source Map
让开发或上线时代码报错能有更加准确的错误提示。
- 使用
提升 webpack 提升打包构建速度
- 使用
HotModuleReplacement
让开发时只重新编译打包更新变化了的代码,不变的代码使用缓存,从而使更新速度更快。 - 使用
OneOf
让资源文件一旦被某个 loader 处理了,就不会继续遍历了,打包速度更快。 - 使用
Include/Exclude
排除或只检测某些文件,处理的文件更少,速度更快。 - 使用
Cache
对 eslint 和 babel 处理的结果进行缓存,让第二次打包速度更快。 - 使用
Thead
多进程处理 eslint 和 babel 任务,速度更快。(需要注意的是,进程启动通信都有开销的,要在比较多代码处理时使用才有效果)
- 使用
减少代码体积
- 使用
Tree Shaking
剔除了没有使用的多余代码,让代码体积更小。 - 使用
@babel/plugin-transform-runtime
插件对 babel 进行处理,让辅助代码从中引入,而不是每个文件都生成辅助代码,从而体积更小。 - 使用
Image Minimizer
对项目中图片进行压缩,体积更小,请求速度更快。(需要注意的是,如果项目中图片都是在线链接,那么就不需要了。本地项目静态图片才需要进行压缩。)
- 使用
优化代码运行性能
- 使用
Code Split
对代码进行分割成多个 js 文件,从而使单个文件体积更小,并行加载 js 速度更快。并通过 import 动态导入语法进行按需加载,从而达到需要使用时才加载该资源,不用时不加载资源。 - 使用
Preload / Prefetch
对代码进行提前加载,等未来需要使用时就能直接使用,从而用户体验更好。 - 使用
Network Cache
能对输出资源文件进行更好的命名,将来好做缓存,从而用户体验更好。 - 使用
Core-js
对 js 进行兼容性处理,让我们代码能运行在低版本浏览器。 - 使用
PWA
能让代码离线也能访问,从而提升用户体验。
- 使用
上一篇: 下一篇: