Appearance
性能优化
分析依赖包大小
在开始进行打包优化前,需要先分析当前依赖包的情况,一般来说,我们需要搞清楚以下几点:
问题 | 解决方案 |
---|---|
打包产物中,某些js文件是否过大? | 合理拆包 |
第三方依赖是否过多导致打包变慢? | 依赖外置 |
Vite实现方案
安装 rollup-plugin-visualizer
插件:
ts
import visualizer from 'rollup-plugin-visualizer'
export default defineConig({
plugins: [
visualizer({
sourcemap: true,
}),
]
})
TIP
如果在分析文件中看不到具体的文件内容,需要将 build.sourcemap
设置为 true
。
插件设置完成后,再次打包,在项目根目录将生成 stats.html
文件,在浏览器中打开该文件:
在我的这个项目里,第三方依赖被打成了两个 js
文件,每个文件都超过了 1M
。
很显然,打包产物过大,需要进行拆包处理。
并且,由于项目依赖的第三方库比较多,我们可以通过将依赖外置的方式来减少对第三方库的打包,从而加快构建速度。
拆包
我们先来拆包,拆包的原则是:
- 保证每个
js
产物不要过大; - 如果是在HTTP1.1协议下,还要保证每个
js
产物不要过小,否则会打出太多的包,运行时将会阻塞导致应用启动过慢; js
产物的命名清晰。
Vite实现方案
Vite的打包是基于rollup的(至少在Vite5及以前版本是这样的),因此,Vite没有提供拆包的配置,而是推荐用户直接覆盖rollup配置。
实现思路是修改 build.rollupOptions.output.manualChunks
配置,文档参考rollup的官方文档:https://rollupjs.org/configuration-options/#output-manualchunks 。
这里给出一份我的实践思路,首先,提供一个配置文件 config.ts
(位于项目目录 build/config.ts
中),用户可以配置:
ts
export type Chunks = Record<string, string[]>
export interface Config {
/** 打包分包 */
chunks: Chunks
}
export default {
chunks: {
'app': ['vue', 'vue-router', 'pinia'],
'ui': ['element-plus'],
'chart': ['echarts'],
'utility': ['lodash', 'dayjs'],
}
} as Config
TIP
上述设置中,vue
、 vue-router
、 pinia
三个依赖包将被打入最终的 app.js
中。
然后,在 vite.config.ts
中配置:
ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks(id) {
for (const [name, packages] of Object.entries(config.chunks)) {
const pass = packages.some(packageName => id.includes(`node_modules/${packageName}\/`))
if (pass) return name
}
if (id.includes('node_modules')) return 'vendor'
},
},
},
},
})
TIP
注意看 manualChunks
回调的最后一行,如果没有命中 chunks
中设置的规则,将会将 node_modules
中的依赖全部打进 vendor.js
中。
接下来,执行打包命令验证一下拆包的结果:
依赖外置 + CDN
我们在写下这行代码时:
ts
import { merge } from 'lodash'
merge(a, b)
执行打包时,打包器会将 lodash
的代码打包到最终的产物中。
我们假设这个库的大小为10M,那打包时,光花在这一个包的处理时间都会特别长。
如果我们能让最终的代码变成这样:
ts
const { merge } = window._
然后在入口html中加入这么一段 <script>
脚本:
html
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
那打包时,我们就可以跳过 lodash
这个库的处理,从而节省一个库的处理时间。如果我们将所有耗时的依赖包全部外置,那理论上,打包就不需要时间。
TIP
无论使用什么打包器(webpack、vite、rollup等),究其本质,其原理都是一样的。
Vite实现方案
在Vite中,我们可以使用 vite-plugin-cdn-import
插件快速实现这件事:
ts
import cdn from 'vite-plugin-cdn-import'
export default defineConig({
plugins: [
cdn({
name: 'lodash',
var: '_',
path: 'https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js',
})
]
})
这个插件大致做了这些事情:
- 在rollup的
external
配置中,新增一条lodash
,保证打包时,跳过它 - 在rollup的
output.globals
配置中,新增一条lodash: '_'
,保证打包产物中,引用模块时,通过全局方式进行引用 - 在生成最终的入口html文件时,加入
<script>
脚本,保证最终运行时,在全局可以读取到引用
合理设置浏览器兼容策略
请看这段代码:
ts
const hello = () => {
return new Promise(() => {
console.log('hello')
})
}
hello()
当你把这段代码运行在Chrome 44版本下时,将会发生报错,这是因为,箭头函数最低在Chrome 45版本才被支持;而 Promise
特性在Chrome33版本得到支持。(具体参见 caniuse)
为了让代码可以兼容更低版本的浏览器,一般情况下,我们需要做两件事:
- 将代码转译为旧的语法,如将箭头函数转译为普通函数,这个过程称为
transform
; - 提供环境支持,例如上述代码中使用到了
Promise
,则需要在全局注入该特性,这个过程称为polyfill
。
Vite实现方案
在Vite中,想要实现低版本兼容,还是非常简单,只需要使用官方的 @vitejs/plugin-legacy
插件即可。
ts
export default defineConfig({
legacy({
// 根据情况设置自己的浏览器版本,或在根目录下新建.browserslistrc文件设置
// 为了让其他插件共享配置,更推荐配置文件的方式
// targets: ['chrome>=96'],
}),
})
bash
# browserslist配置主要有以下两个作用
# 1. vite legacy插件为js提供polyfill
# 2. postcss autoprefixer插件为css提供prefix
[production]
last 2 versions and not dead, > 0.3%, Firefox ESR
WARNING
注意注释的内容,尽管在legacy插件中,可以非常方便地设备目标浏览器的版本,但我们仍然建议使用 .browserslistrc
文件配置。
原因是,除了legacy插件需要做js相关的兼容外,css也需要,例如postcss的 autoprefixer
插件,如果我们在legacy插件中设置一次兼容浏览器版本,再为 autoprefixer
设置一次兼容浏览器版本,那就可能出现不一致的情况,为了保持一致,我们让所有工具都从源头的 .browserslistrc
文件中读取浏览器特性列表,从而实现兼容。
那么,浏览器兼容策略为什么跟性能优化有关呢?原因是:
不合适的(过低的)浏览器兼容版本将会导致打包速度明显下降,因为要转译的语法和补充的polyfill更多,同时,也会导致最终生成的打包产物过大。
因此,设置兼容的浏览器版本范围时,我们需要 切合实际去考量,到底需要最低兼容到什么版本,适当地放弃部分版本太低的用户来换取开发、构建与运行的体验和性能,这是值得的。
TIP
如果你的应用针对现代浏览器用户开发,那就保持默认配置即可,less is more。
关于浏览器兼容设置的所有细节,请参见 浏览器兼容。
资源压缩(gzip)
注意
此项优化只能提升运行速度,不能提高打包速度。
打包时,除了原有文件外,为每份静态文件(一般是html/js/css)生成一份压缩版本,这其实会降低打包的速度。
然后,让浏览器读取更小体积的压缩文件,这样加载的速度会变快,从而让应用启动更快,白屏时间更短。
Vite实现方案
在Vite中,通过 vite-plugin-compression
插件快速实现这件事:
ts
export default defineConfig({
plugins: [
compression({
algorithm: 'gzip',
}),
]
})
打包时效果如下:
去掉预处理器
毫无疑问,预处理器的设计是优秀的。看看这段代码:
css
table {
height: 300px;
}
table tr {
background-color: lightblue;
}
table tr td {
font-size: 14px;
}
table tr td div {
font-weight: bold;
}
CSS的语法限制,往往让我们不得不写出极度冗余的代码,预处理器的出现,解决了这些问题:
scss
table {
height: 300px;
tr {
background-color: lightblue;
td {
font-size: 14px;
div {
font-weight: bold;
}
}
}
}
这种嵌套语法,延续了HTML元素嵌套的传统。
简单来讲,预处理器的作用是:通过提前使用最新的CSS语法或使用预处理器自创的语法,更快地书写样式代码,提升研发效率,降低维护成本。
不仅仅是嵌套的语法,预处理器还提供了这些优秀的特性:
特性 | 解决的问题 | scss样例代码 |
---|---|---|
变量 | 更好地复用,减少常量的重复编写,降低维护难度 | $color: red; |
导入 | 拆分模块,更清晰的结构与更低的维护成本 | @import('base.scss') |
mixin | 复用,相当于拆分代码片段 | @mixin base{ color: red; } |
逻辑 | 支持分支、循环等逻辑 | @if $color {} |
函数 | 进一步复用 | @function base() { @return 1 } |
其他 | 颜色、数学运算等高级特性,快速编写代码 | lighten(#e1d7d2, 30%) |
好了,让我们回归到问题中来。预处理器确实有诸多优秀的特性,但是:
我们的项目中真的需要这么多的特性吗?我们使用预处理器的程度如何?
事实是,我们高估了自己使用预处理器的程度,以为会用到很多特性,但其实没有。对于绝大多数上层应用而言,使用预处理器的程度仅限于 变量、嵌套、导入 三个重要特性。
而这些特性,都在后处理器PostCSS中得到了很好的支持,让我们来看看怎么使用它们:
特性 | 替代方法 |
---|---|
变量 | 直接使用CSS变量,如 --color: red ,如果确实需要兼容低版本浏览器,使用 postcss-custom-properties 插件 |
嵌套 | 使用 postcss-nesting 插件 |
导入 | 使用 postcss-import 插件,一般上层构建工具(如Vite)默认就提供了支持 |
也就是说,如果使用Vite,只需要新增一个
nesting
插件即可。
为什么要去除预处理器?
去掉预处理器的原因有二:
- less is more ,概念简单化,减轻开发者的心智负担;
- 性能提升 ,预处理与后处理都是要花时间的,同时使用预处理器与后处理器,意味着每次打包至少要启动两个处理CSS的引擎,这会让打包速度下降明显,而如果所有问题都回归到后处理器中,即便处理的流程变长变复杂了,但是速度仍然要快于同时使用两个处理工具。
如何去除?
找到项目中所有使用了预处理器的地方,改掉它们即可。
最理想的情况是,你只需要将
.scss
文件 改为.css
文件即可。但如果项目使用了预处理器的高级语法,你就得花点心思琢磨如何在PostCSS中重构它们了。
图片压缩
注意
此项优化只能提升运行速度,不能提高打包速度。
图片过大,加载时间慢,会引起 window.onload
回调过晚,从而导致网站的性能指标不佳。
Vite实现方案
首先,安装 vite-plugin-imagemin
插件,然后配置:
ts
export default defineConfig({
plugins: [
imagemin({
mozjpeg: {
quality: 10,
},
pngquant: {
quality: [0.5, 0.5],
},
})
]
})
需要注意,该插件在中国安装时会报错,请按照插件文档的解决方案进行解决。
其他小技巧
关闭日志
ts
export default defineConfig({
logLevel: 'silent'
})
TIP
也可以设置为 error
仅捕捉一些错误日志。
关闭压缩大小报告
ts
export default defineConfig({
build: {
reportCompressedSize: false,
}
})
安装时忽略钩子脚本的执行
当 package.json
中设置了:
json
{
"scripts": {
"prepare": "husky install"
}
}
钩子脚本在安装前后会自动执行,此时评估如果钩子函数不影响安装与打包的过程,那就可以通过以下命令,在安装时忽略这些脚本的执行,提高安装速度,从而提升整个打包环节的速度。
bash
# pnpm
pnpm i --ignore-scripts
# npm
npm i --ignore-scripts
# yarn
yarn install --ignore-scripts