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