前言
10月10号,刚刚过完中秋国庆,webpack
官方就不懂声色的说webpack5
上线了...It's released
,本篇文章主要从webpack4
和webpack5
对比的角度入手,看一看webpack5
到底有哪些改变,让我们来动手实践一下吧。
主要功能更新General direction
This release focus on the following
:(此版本重点关注一下内容:)
Improve build performance with Persistent Caching
(通过持久缓存提高构建性能)Improve Long Term Caching with better algorithms and defaults
(使用更好的算法和默认值选项改善长期缓存)Improve bundle size with better Tree Shaking and Code Generation
(通过更好的Tree Shaking和代码生成来改善生成包大小)Improve compatibility with the web platform
(改善与Web平台的兼容性)Clean up internal structures that were left in a weird state while implementing features in v4 without introducing any breaking changes
( 在不引入任何重大更改的前提下,实现v4版本中的功能的同时清理处于异常状态的内部结构)Prepare for future features by introducing breaking changes now, allowing us to stay on v5 for as long as possible
(现在就引入重大更改,为将来的功能做准备,使我们能够尽可能长时间地使用v5)
上手开始
新建项目
新建两个项目 webpack4-demo
、 webpack5-demo
,我们将从两个版本的角度进行上手比较
可以看到webpack4
的最后一个正式版为4.44.2
,我们就以4.44.2
版本为例:
// webpack4-demo
npm init -y
npm install webpack@4.44.2 --save-dev
npm install webpack-cli --save-dev
// webpack5-demo
npm init -y
npm install webpack --save-dev // 默认安装最新5.0.0版本
npm install webpack-cli --save-dev
package.json
添加 "dev": "webpack --mode development"
,
"prod": "webpack --mode production"
如下图:
跑一下试试
新建一个src
文件夹,新建index.js
,写一句console.log("Fruit Bro")
在development
模式下
npm run dev
webpack5
运行时间132ms
,webpack4
只需要71ms
,好吧...
看一下打包结果
生成dist
文件大小的比较:
webpack5
webpack5
生成文件只有867字节
。webpack5
在只有一个文件的时候,直接用eval
包裹代码,没有了webpack4
的key value
的相关内容。简单粗暴。
webpack4
webpack4
生成文件4kb
。可以看到,webpack5
生成文件的大小只有webpack4
生成文件四分之一
左右。
在production
模式下:
npm run prod
webpack4
生成的文件如下:
大小为954字节
。
webpack5
生成的文件如下:
对你没有看错,没有任何多余的代码,只有一句console.log("Fruit Bro")
,大小也只有25字节
。
webpack5
通过代码生成改善了包的大小。
下面开始介绍它主要的几个更新。
1. 重大变更:长期缓存
确定的 Chunk、模块 ID 和导出名称
新增了长期缓存的算法。这些算法在生产模式下是默认启用的。chunkIds: "deterministic"
moduleIds: "deterministic"
mangleExports: "deterministic"
该算法以确定性的方式为模块和分块分配短的(3 或 5 位)数字 ID
, 这是包大小和长期缓存之间的一种权衡。
moduleIds/chunkIds/mangleExports: false
禁用默认行为,你可以通过插件提供一个自定义算法。请注意,在 webpack 4
中,moduleIds/chunkIds: false
如果没有自定义插件,则可以正常运行,而在 webpack 5
中,你必须提供一个自定义插件。
迁移:最好使用 chunkIds
、moduleIds
和 mangleExports
的默认值。你也可以选择使用旧的默认值chunkIds: "size",moduleIds: "size", mangleExports: "size"
,这将会生成更小的包,但为了缓存,会更频繁地将其失效。
注意:在 webpack 4
中,散列的模块 id
会导致gzip
性能降低。这与模块顺序的改变有关,已经被修正。
注意:在 webpack 5
中,deterministic Ids
在生产模式下是默认启用的。
弃用了 optimization.moduleIds: "hashed"
,添加了 optimization.moduleIds: "deterministic"
。
下面我们用代码验证一下:
// sync.js
const result = 'Fruit Bro'
export default result
// random.js
function random() {
console.log(Math.random())
}
export default random
// index.js 异步引入
import('./sync').then((_) => {
console.log(_)
})
import('./random').then((random) => {
console.log(random)
})
在webpack4
下,异步生成了 0.js、 1.js
和 main.js
从上图可以看出,0.js
为random.js
,1.js
为sync.js
,我们删除random.js
之后重新编译,发现0.js
变成了sync.js
...
在webpack4
的版本中sync.js、random.js
会被一次分配给一个chunkId
。然后生成的main.js
根据chunkId
加载对应的文件,但是悲剧的是如果此时我删掉 import("./sync.js").then((_) => {console.log(_)})
这一行的话会导致random.js
从原来的1
变成了0
,这对我们的浏览器缓存是非常不友好的。当然我们用Magic Comments
也可以解决这个问题,可是,webpack5
将不需要引入任何的外力,如上我们遇到prod
陌生的带数字的JS
,就是为了增强long-term caching
,增加了新的算法,并在生产模式下使用以下配置开启。这些算法以确定性的方式为模块和数据块分配非常短(3或4个字符)的数字 id
。
// webpack5生产环境的默认配置
module.exports = {
optimization:{
chunkIds: "deterministic”,
moduleIds: "deterministic"
}
}
// webpack4生产环境的默认配置
module.exports = {
optimization:{
chunkIds: "natural”,
moduleIds: "size"
}
}
webpack5生成文件如下,即使我们删除了其中的一个文件,对另一个文件的名字也是没有影响的。
2. 重大变更:性能优化
持久缓存(更快的编译速度)
现在有一个文件系统缓存。它是可选的,可以通过以下配置启用:
module.exports = {
cache: {
// 1. 将缓存类型设置为文件系统
type: 'filesystem',
buildDependencies: {
// 2. 将你的 config 添加为 buildDependency,以便在改变 config 时获得缓存无效
config: [__filename],
// 3. 如果你有其他的东西被构建依赖,你可以在这里添加它们
// 注意,webpack、加载器和所有从你的配置中引用的模块都会被自动添加
},
},
};
重要说明:
默认情况下,webpack
假定 webpack
所在的 node_modules
目录只被包管理器修改。对 node_modules
来说,哈希值和时间戳
会被跳过。 出于性能考虑,只使用包名和版本
。 只要不指定resolve.symlinks: false,Symlinks(即npm/yarn link)
就没有问题(无论如何都要避免)。 不要直接编辑 node_modules
中的文件,除非你用 snapshot.managedPaths: []
以剔除该优化。 当使用 Yarn PnP
时,webpack
假设 yarn
缓存是不可改变的(通常是这样)。 你可以使用 snapshot.immutablePaths: []
来退出这个优化。
缓存将默认存储在 node_modules/.cache/webpack
(当使用 node_modules
时)或 .yarn/.cache/webpack
(当使用 Yarn PnP
时)中。 当所有的插件都正确处理缓存时,你可能永远都不需要手动删除它。
许多内部插件也会使用持久性缓存。例如 SourceMapDevToolPlugin
(缓存 SourceMap
的生成)或 ProgressPlugin
(缓存模块数量)
持久性缓存将根据使用情况自动创建多个缓存文件,以优化对缓存的读写访问。
默认情况下,时间戳将用于开发模式的快照,而文件哈希将用于生产模式。 文件哈希也允许在CI
中使用持久性缓存。
目前项目编译的时间为136ms
。
// webpack.config.js
const path = require('path');
module.exports = {
cache: {
// 1. 将缓存类型设置为文件系统
type: 'filesystem',
// 2. 将缓存文件夹命名为 .temp_cache
cacheDirectory: path.resolve(__dirname, '.temp_cache')
}
}
配置之后的编译时间为125ms
,如下图,可能是因为我们的项目文件太小导致的编译效果不明显,只能在后续的项目中进行实践了。
运行之后会生成下面的目录:
cache
在webpack4
版本中就有,不过需要进行配置, webpack5
不再需要cache-loader
。 对于babel cacheDirectory
等也是如此。
3. 重大变更: 功能清除
不再为 Node.js
模块自动引用 Polyfills
在早期,webpack
的目的是为了让大多数的Node.js
模块运行在浏览器中,但如今模块的格局已经发生了变化,现在许多模块主要是为前端而编写。webpack <= 4
的版本中提供了许多 Node.js
核心模块的 polyfills
,一旦某个模块引用了任何一个核心模块(如 cypto
模块),webpack
就会自动引用这些 polyfills
。
尽管这会使得使用为 Node.js
编写模块变得容易,但它在构建时给 bundle
附加了庞大的 polyfills
。在大部分情况下,这些 polyfills
并非必须。
从 webpack 5
开始不再自动填充这些 polyfills
,而会专注于前端模块兼容。我们的目标是提高 web
平台的兼容性。
迁移:
- 尽量使用前端兼容的模块
- 可以手动为
Node.js
核心模块添加polyfill
。错误提示会告诉你如何实现。 Package
作者:在package.json
中添加browser
字段,使package
与前端兼容。为浏览器提供其他的实现/dependencies
。
下面我们动手实现一下:
- 分别在
webpack4-demo
和webpack5-demo
中的index.js
写如下代码:
// index.js
const crypto = require('crypto')
const hash = crypto.createHash('md5')
hash.update('Hello, Fruit Bro!')
console.log(hash.digest('hex'))
- 运行
npm run dev
我们发现在webpack4
中,打包之后的文件达到了惊人的1.6MB
,包含了大量的cypto
的polyfill
在webpack5
下运行则会提示如下相应的错误
告诉我们如果我们想要使用这个polyfill
,需要install
对应的包
- 在
webpack5-demo
中根据错误提示安装如下npm
包
npm install crypto-browserify --save
npm install buffer --save
npm install stream-browserify --save
- 在
webpack5-demo
中根据错误配置webpack.config.js
// webpack.config.js
module.exports = {
resolve: {
fallback: {
"crypto": require.resolve("crypto-browserify"),
"buffer": require.resolve("buffer/"),
"stream": require.resolve("stream-browserify")
}
}
}
在安装了上面的三个npm
包后,配置好webpack.config.js
,运行npm run dev
,终于不再报错。而打包出的文件也达到了惊人的1.5MB
。
正如上面所述,我们尽量使用前端兼容的模块。如果要使用Node.js
的模块,可以手动进行添加。从而减小打包后的JS
体积。
4. 重大变更: 构建优化
tree-shaking
在webpack4
中要深度tree-shaking
需要使用插件 webpack-deep-scope-plugin
。
webpack5
能够跟踪对导出的嵌套属性的访问。这可以改善重新导出命名空间对象时的Tree Shaking
(清除未使用的导出和混淆导出)。
// inner.js
export const a = 'a'
export const b = 'b'
// module.js
import * as inner from './inner'
export { inner }
// index.js
import * as module from './module'
console.log(module.inner.a)
打包出来的文件如下:
可以看到,main.js
中只有一个闭包,包含一句console.log('a')
,连变量名都没有了,b
也没有被打包进来,说明webpack5确实进行了深度的tree-shaking
注: 只能在production
模式下进行tree-shaking
,在development
模式是不支持的。
代码块拆分与模块大小 minSize、maxSize
现在模块的尺寸比单一的数字更好的表达方式。现在有不同类型的大小。
SplitChunksPlugin
现在知道如何处理这些不同的大小,并将它们用于 minSize
和 maxSize
。 默认情况下,只有 javascript
大小被处理,但你现在可以传递多个值来管理它们:
在webpack4中默认只能处理js的大小
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
commons: {
chunks: "all",
name: "commons",
minChunks: 1,
minSize: "数值",
maxSize: "数值"
}
}
}
}
}
但在webpack5
中,可以把js
和css
都进行处理
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
commons: {
chunks: "all",
name: "commons",
}
},
//最小的文件大小 超过之后将不予打包
minSize: {
javascript: 0,
style: 0,
},
//最大的文件 超过之后继续拆分
maxSize: {
javascript: 1, //故意写小的效果更明显
style: 3000,
}
}
}
}
你仍然可以使用一个数字来表示大小。在这种情况下,webpack
会自动使用默认的大小类型。
mini-css-extract-plugin
使用 css/mini-extra
作为大小类型,并将此大小类型自动添加到默认类型中。
总结
趁着周末上手体验了一下webpack5
,还有很多细节没有提到,很多地方写的不到位,在后续也会持续更新,建议大家看着官方文档一起学习,写的不到位的地方也希望大家能多多指正~
参考文档: