Vite 4.3 正式发布,极致的性能优化!

21,342 阅读3分钟

前言

Vite 4.3 正式发布!

彼时彼刻,Vite 以其极致的项目启动速度和 HMR 速度,在前端工具链领域一骑绝尘无人能敌,后来,挑战者出现了,它就是由 webpack 创始人联合开发的 Turbopack,基于 Rust,号称比 Vite 快 10X!

此时此刻,Vite 要拿回属于自己的荣耀,用手中的 JavaScript 和架构上的设计优化,对抗气势汹汹的 Rust!

和其他工具的对比(项目启动): image.png

和其他工具的对比(HMR): image.png

和之前版本的对比(项目启动): image.png

和之前版本的对比(HMR): image.png

Vite 架构设计方面的优化这里不做介绍,本文只会从 JavaScript 语言层面,介绍 Vite 4.3 利用语言特性做了哪些优化,帮助大家更好的掌握 JavaScript 💪

介绍每个优化点时,会简单介绍如何优化的、相关的 commit、优化前和优化后的(伪)代码、测试代码并给出结果。

并行 await

相关 commit:

主要变动:

// 🙅 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:

主要变动:

// 🙅 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() 效率低的原因有两个:

  1. 构建正则表达式比构建字符串更耗时
  2. 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 为删除键值对做了特别的性能优化,但是如果只涉及添加、获取键值对的操作,MapObject 相比性能是不占优势的。

测试代码:

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 设为上百万、上千万的时候,最终执行的结果才会有几十毫秒、几百毫秒的差距。在日常开发中,除非数据量巨大、对性能有要求的场景(如虚拟列表、基础库)可以考虑这种极致的性能压榨写法,否则,建议还是从可读性、可维护性、易用性方面去写代码。