前言
Vite 4.3 正式发布!
彼时彼刻,Vite 以其极致的项目启动速度和 HMR 速度,在前端工具链领域一骑绝尘无人能敌,后来,挑战者出现了,它就是由 webpack 创始人联合开发的 Turbopack,基于 Rust,号称比 Vite 快 10X!
此时此刻,Vite 要拿回属于自己的荣耀,用手中的 JavaScript 和架构上的设计优化,对抗气势汹汹的 Rust!
Vite 架构设计方面的优化这里不做介绍,本文只会从 JavaScript 语言层面,介绍 Vite 4.3 利用语言特性做了哪些优化,帮助大家更好的掌握 JavaScript 💪
介绍每个优化点时,会简单介绍如何优化的、相关的 commit、优化前和优化后的(伪)代码、测试代码并给出结果。
并行 await
相关 commit:
- perf: parallelize await exportsData from depsInfo
- perf: parallelize imports processing in import analysis plugin
- perf(moduleGraph): resolve dep urls in parallel
主要变动:
// 🙅 before
for (let promise of promiseList) {
await promise
}
// 😍 after
await Promise.all(promiseList)
测试代码:
const COUNT = 1_000_000
function makePromiseList() {
const promiseList: Promise<any>[] = []
for (let i = 0; i < COUNT; i++) {
const promise = new Promise((resolve) => {
setTimeout(() => resolve(0), 200)
})
promiseList.push(promise)
}
return promiseList
}
const promiseList = makePromiseList()
async function before() {
console.time('before')
for (let promise of promiseList) {
await promise
}
console.timeEnd('before')
}
async function after() {
console.time('after')
await Promise.all(promiseList)
console.timeEnd('after')
}
setTimeout(() => {
before()
after()
}, 1000)
测试结果:
before: 185ms
after: 131ms
避免使用 new URL()
相关 commit:
主要变动:
// 🙅 before
new URL(url)
// 😍 after
(通过操作字符串获得新的 url)
测试代码:
const COUNT = 1_000_000
function before() {
console.time('before')
for (let i = 0; i < COUNT; i++) {
const url = new URL('http://lccl.cc')
url.protocol = 'https://'
const newUrl = url.origin
}
console.timeEnd('before')
}
function after() {
console.time('after')
for (let i = 0; i < COUNT; i++) {
const newUrl = 'http://lccl.cc'.replace('http://', 'https://')
}
console.timeEnd('after')
}
before()
after()
测试结果:
before: 1033ms
after: 68ms
提取正则
相关 commit:
- perf: extract regex and use Map in data-uri plugin
- perf: more regex improvements
- perf: reuse regex in plugins
主要变动:
// 🙅 before
/\d/i.test('')
// 😍 after
const reg = /\d/i
reg.test('')
测试代码:
const COUNT = 10_000_000
function before() {
console.time('before')
for (let i = 0; i < COUNT; i++) {
/base64/i.test('')
}
console.timeEnd('before')
}
function after() {
console.time('after')
const reg = /base64/i
for (let i = 0; i < COUNT; i++) {
reg.test('')
}
console.timeEnd('after')
}
before()
after()
测试结果:
before: 111ms
after: 95ms
使用 startsWith/slice 代替正则替换
相关 commit:
主要变动:
// 🙅 before
str.replace(/^node:/, '')
// 😍 after
const prefix = 'node:'
str.startsWith(prefix) ? str.slice(prefix.length) : str
测试代码:
const COUNT = 10_000_000
const module = 'node:http'
function before() {
console.time('before')
const reg = /^node:/
for (let i = 0; i < COUNT; i++) {
module.replace(reg, '')
}
console.timeEnd('before')
}
function after() {
console.time('after')
const prefix = 'node:'
for (let i = 0; i < COUNT; i++) {
module.startsWith(prefix) ? module.slice(prefix.length) : module
}
console.timeEnd('after')
}
before()
after()
输出:
before: 298ms
after: 112ms
使用 includes 代替正则匹配
相关 commit:
主要变动:
// 🙅 before
/生命/.test(str)
// 😍 after
str.includes('生命')
测试代码:
const COUNT = 10_000_000
const str = '于 你的生命之中'
function before() {
console.time('before')
for (let i = 0; i < COUNT; i++) {
/生命/.test(str)
}
console.timeEnd('before')
}
function after() {
console.time('after')
for (let i = 0; i < COUNT; i++) {
str.includes('生命')
}
console.timeEnd('after')
}
before()
after()
输出:
before: 173ms
after: 141ms
这个示例中, before()
效率低的原因有两个:
- 构建正则表达式比构建字符串更耗时
RegExp.prototype.test()
比String.prototype.includes()
更耗时
使用 ===
代替 endsWith
相关 commit:
主要变动:
// 🙅 before
str.endsWith('/')
// 😍 after
str[str.length - 1] === '/'
测试代码:
const COUNT = 10_000_000
const str = '你陪我步入蝉夏,越过城市喧嚣'
const tail = str[str.length - 1]
function before() {
console.time('before')
for (let i = 0; i < COUNT; i++) {
str.endsWith(tail)
}
console.timeEnd('before')
}
function after() {
console.time('after')
for (let i = 0; i < COUNT; i++) {
str[str.length - 1] === tail
}
console.timeEnd('after')
}
before()
after()
输出:
before: 85ms
after: 20ms
String.prototype.startsWith()
也是同样的道理。
其他
还有一个 commit,使用 Map<string, string>
代替了 { [key: string]: string }
,也就是使用 map
存储键值对都是字符串的数据结构,理论上来说效率会比 object
高,但是实际测试发现并没有,有知道为什么的小伙伴欢迎留言 👏
🆕 4-24 更新: 感谢评论区 @markthree 提供的文章资料,Map
为删除键值对做了特别的性能优化,但是如果只涉及添加、获取键值对的操作,Map
和 Object
相比性能是不占优势的。
测试代码:
const COUNT = 10_000_000
function before() {
console.time('before')
const map: Record<number, number> = {}
for (let i = 0; i < COUNT; i++) {
map[i] = i
map[i]
}
console.timeEnd('before')
}
function after() {
console.time('after')
const map: Map<number, number> = new Map()
for (let i = 0; i < COUNT; i++) {
map.set(i, i)
map.get(i)
}
console.timeEnd('after')
}
before()
after()
测试结果:
before: 184ms
after: 1627ms
接下来我们把读取键值对的操作改为删除键值对。
测试代码:
function before() {
...
for () {
map[i] = i
delete map[i]
}
}
function after() {
...
for () {
map.set(i, i)
map.delete(i)
}
}
测试结果:
before: 1321ms
after: 410ms
总结
- 对于
Promise
实例列表,尽可能的使用Promise.all()
并发执行 new URL()
是很耗时的,如果可以,请通过操作字符串得到新的 url- 如果一个正则会被多次使用,最好提取出来成为一个常量,因为这样只会构建一次
- 正则表达式这把瑞士军刀,很强大、很方便,但大部分情况下,性能不如
String.prototype
上的 API 性能好 - 如果涉及到大量的删除键值对的操作,
Map
对象的性能更优一些,如果只是添加、查找键值对,Object
对象性能更优
最后,从测试代码中可以看到, COUNT
设为上百万、上千万的时候,最终执行的结果才会有几十毫秒、几百毫秒的差距。在日常开发中,除非数据量巨大、对性能有要求的场景(如虚拟列表、基础库)可以考虑这种极致的性能压榨写法,否则,建议还是从可读性、可维护性、易用性方面去写代码。