优化 JavaScript,为了乐趣和收益

我经常觉得 JavaScript 代码总体上运行速度比它本应更慢,简单因为它没有被正确优化。以下是我发现有用的常见优化技术总结。需要注意的是,性能的权衡通常是可读性,所以何时追求性能而不是可读性,这是读者自行决定的问题。我还要注意,谈论优化必然需要谈论基准测试。如果你花费数小时来微调一个函数,让它的运行速度提高 100 倍,但这个函数在实际运行时只占总运行时间的一小部分,那么这样的优化是毫无意义的。如果要优化,第一步也是最重要的一步是基准测试。我将在后面的点中涉及这个主题。同时也要注意,微基准测试通常存在缺陷,这可能包括这里提到的一些。我尽力避免了这些陷阱,但不要盲目地应用这里介绍的任何观点而不进行基准测试。

我已经为所有可能的情况包含了可运行的示例。它们默认显示我在我的机器上(Brave 122 on Arch Linux)得到的结果,但你也可以自己运行它们。尽管我不愿意这样说,但是 Firefox 在优化游戏中已经落后了一些,并且只占很小一部分流量暂时,所以我不建议使用在 Firefox 上得到的结果作为有用的指标。

0. 避免不必要的工作

这听起来可能很明显,但需要提到这一点,因为优化没有其他的第一步:如果你试图进行优化,你应该首先考虑避免不必要的工作。这包括诸如记忆化、惰性求值和增量计算等概念。这将根据上下文的不同而有所不同。例如,在 React 中,这意味着应用 memo()useMemo() 和其他适用的基元。

1. 避免字符串比较

JavaScript 使得隐藏字符串比较的真实成本变得很容易。如果你需要在 C 中比较字符串,你会使用 strcmp(a, b) 函数。JavaScript 使用 === 代替,所以你看不到 strcmp。但实际上是有的,字符串比较通常需要将字符串中的每个字符与另一个字符串中的字符进行比较;字符串比较的复杂度是 O(n)。一个常见的 JavaScript 模式是避免将字符串用作枚举。但是随着 TypeScript 的出现,这应该很容易避免,因为枚举默认是整数。

// 不好
enum Position {
  TOP    = 'TOP',
  BOTTOM = 'BOTTOM',
}
// 好的
enum Position {
  TOP,    // = 0
  BOTTOM, // = 1
}

下面是成本的比较:

// 1. 字符串比较
const Position = {
  TOP: 'TOP',
  BOTTOM: 'BOTTOM',
}

let _ = 0
for (let i = 0; i < 1000000; i++) {
  let current = i % 2 === 0 ?
    Position.TOP : Position.BOTTOM
  if (current === Position.TOP)
    _ += 1
}
// 2. 整数比较
const Position = {
  TOP: 0,
  BOTTOM: 1,
}

let _ = 0
for (let i = 0; i < 1000000; i++) {
  let current = i % 2 === 0 ?
    Position.TOP : Position.BOTTOM
  if (current === Position.TOP)
    _ += 1
}
关于基准测试

百分比结果表示在 1 秒内完成的操作数,除以得分最高的案例的操作数。分数越高越好。

正如你所见,差异可能很大。差异不仅仅是由于需要比较的数量不同,还因为整数在 JavaScript 引擎中通常是按值传递的,而字符串总是作为指针传递的,内存访问是昂贵的(参见第 5 节)。在字符串密集的代码中,这可能会产生巨大的影响。

举个实际例子,我能够使这个 JSON5 JavaScript 解析器运行速度提高 2 倍*,只需用数字替换字符串常量。
*不幸的是,它没有被合并,但这就是开源的方式。

2. 避免不同的形状

JavaScript 引擎尝试通过假设对象具有特定的形状,并且函数将接收到相同形状的对象来优化代码。这使它们可以一次为该形状的所有对象存储键,并在单独的扁平数组中存储值。在 JavaScript 中表示如下:

const objects = [
  {
    name: 'Anthony',
    age: 36,
  },
  {
    name: 'Eckhart',
    age: 42
  },
]
const shape = [
  { name: 'name', type: 'string' },
  { name: 'age',  type: 'integer' },
]

const objects = [
  ['Anthony', 36],
  ['Eckhart', 42],
]
术语说明

我为这个概念使用了“形状”一词,但请注意你也可能会发现使用 “隐藏类”“映射” 这些术语来描述它,这取决于引擎。

例如,如果以下函数在运行时接收到具有形状 { x: number, y: number } 的两个对象,引擎会推测未来的对象将具有相同的形状,并为该形状生成经过优化的机器代码。

function add(a, b) {
  return { x: a.x + b.x, y: a.y + b.y}
}

如果传递的对象不是形状为 { x, y } 的对象,而是形状为 { y, x } 的对象,引擎将需要撤销它的推测,函数会突然变得明显较慢。我在这里会限制解释,因为你应该阅读 mraleph 的优秀文章 如果你想要更多细节,但我要强调特别是 V8 有 3 种模式,用于访问:单态(1 个形状)、多态(2-4 个形状)和超多态(5+ 个形状)。让我们说你 真的 想保持单态,因为速度变慢是非常明显的:

// 设置
let _ = 0
// 1. 单态
const o1 = { a: 1, b: _, c: _, d: _, e: _ }
const o2 = { a: 1, b: _, c: _, d: _, e: _ }
const o3 = { a: 1, b: _, c: _, d: _, e: _ }
const o4 = { a: 1, b: _, c: _, d: _, e: _ }
const o5 = { a: 1, b: _, c: _, d: _, e: _ } // 所有形状相同
// 2. 多态
const o1 = { a: 1, b: _, c: _, d: _, e: _ }
const o2 = { a: 1, b: _, c: _, d: _, e: _ }
const o3 = { a: 1, b: _, c: _, d: _, e: _ }
const o4 = { a: 1, b: _, c: _, d: _, e: _ }
const o5 = { b: _, a: 1, c: _, d: _, e: _ } // 这个形状不同
// 3. 超多态
const o1 = { a: 1, b: _, c: _, d: _, e: _ }
const o2 = { b: _, a: 1, c: _, d: _, e: _ }
const o3 = { b: _, c: _, a: 1, d: _, e: _ }
const o4 = { b: _, c: _, d: _, a: 1, e: _ }
const o5 = { b: _, c: _, d: _, e: _, a: 1 } // 所有形状都不同
// 测试用例
function add(a1, b1) {
  return a1.a + a1.b + a1.c + a1.d + a1.e +
          b1.a + b1.b + b1.c + b1.d + b1.e }

let result = 0
for (let i = 0; i < 1000000; i++) {
  result += add(o1, o2)
  result += add(o3, o4)
  result += add(o4, o5)
}

说起来容易做起来难,但是:请使用完全相同的形状创建所有的对象。即使是像 以不同顺序编写你的 React 组件属性 这样的微不足道的事情也可能触发这种情况。

例如,这里是 React 代码库中我发现的 简单案例,但是几年前他们已经发生了 同样问题的更严重情况,因为他们将一个对象初始化为整数,然后稍后存储为浮点数。是的,更改类型也会更改形状。是的,number 背后隐藏着整数和双精度浮点数类型。

数字表示

引擎通常将整数编码为值。例如,V8 使用紧凑的 Smi(SMall Integer)值表示整数,JSC 使用 双重标记 通过值传递数字,就像 SpiderMonkey 一样。其余的作为指针传递给更复杂的表示。

3. 避免数组/对象方法

我和任何其他人一样热爱函数式编程,但除非你在 Haskell/OCaml/Rust 中工作,在这些语言中,函数式代码会被编译成高效的机器代码,否则函数式编程总是比命令式编程慢。

const result =
  [1.5, 3.5, 5.0]
    .map(n => Math.round(n))
    .filter(n => n % 2 === 0)
    .reduce((a, n) => a + n, 0)

这些方法的问题在于:

  1. 它们需要对数组进行完全复制,这些副本稍后需要由垃圾收集器释放。我们将在第 5 节详细探讨内存 I/O 的问题。
  2. 对于 N 个操作,它们需要循环 N 次,而 for 循环将允许一次循环。
// 设置:
const numbers = Array.from({ length: 10_000 }).map(() => Math.random())
// 1. 函数式
const result =
  numbers
    .map(n => Math.round(n * 10))
    .filter(n => n % 2 === 0)
    .reduce((a, n) => a + n, 0)
// 2. 命令式
let result = 0
for (let i = 0; i < numbers.length; i++) {
  let n = Math.round(numbers[i] * 10)
  if (n % 2 !== 0) continue
  result = result + n
}

诸如 Object.values()Object.keys()Object.entries() 之类的对象方法也存在类似的问题,因为它们还会分配更多数据,而内存访问是所有性能问题的根源。真的,我发誓,我将在第 5 节中向你展示。

4. 避免间接引用

另一个寻找优化收益的地方是任何间接引用的源头,我可以看到主要有 3 个源头:

const point = { x: 10, y: 20 }

// 1.
// 代理对象更难优化,因为它们的 get/set 函数可能会运行自定义逻辑,因此引擎无法做出通常的假设。
const proxy = new Proxy(point, { get: (t, k) => { return t[k] } })
// 一些引擎可以使代理成本消失,但这些优化很昂贵且容易破坏。
const x = proxy.x

// 2.
// 通常被忽略,但通过 `.` 或 `[]` 访问对象也是一种间接引用。在简单情况下,引擎可能很好地优化掉这种成本:
const x = point.x
// 但是每次额外访问都会增加成本,并使引擎更难以对 `point` 的状态做出假设:
const x = this.state.circle.center.point.x

// 3.
// 最后,函数调用也可能有成本。引擎通常擅长将这些内联:
function getX(p) { return p.x }
const x = getX(p)
// 但不能保证它们可以。特别是如果函数调用不是来自静态函数,而是来自例如参数:
function Component({ point, getX }) {
  return getX(point)
}

目前,对于 V8 引擎来说,代理基准测试尤其残酷。上次我查看时,代理对象总是从 JIT 回退到解释器,从这些结果来看,现在可能仍然是这样。

// 1. 代理访问
const point = new Proxy({ x: 10, y: 20 }, { get: (t, k) => t[k] })

for (let _ = 0, i = 0; i < 100_000; i++) { _ += point.x }
// 2. 直接访问
const point = { x: 10, y: 20 }
const x = point.x

for (let _ = 0, i = 0; i < 100_000; i++) { _ += x }

我还想展示访问深度嵌套对象与直接访问的效果,但是当存在热循环和常量对象时,引擎非常擅长通过逃逸分析 优化掉对象访问。我插入了一些间接性以防止这种情况发生。

// 1. 嵌套访问
const a = { state: { center: { point: { x: 10, y: 20 } } } }
const b = { state: { center: { point: { x: 10, y: 20 } } } }
const get = (i) => i % 2 ? a : b

let result = 0
for (let i = 0; i < 100_000; i++) {
  result = result + get(i).state.center.point.x }
// 2. 直接访问
const a = { x: 10, y: 20 }.x
const b = { x: 10, y: 20 }.x
const get = (i) => i % 2 ? a : b

let result = 0
for (let i = 0; i < 100_000; i++) {
  result = result + get(i) }

这一点需要一些低级知识,但即使在 JavaScript 中也有影响,所以我会解释。从 CPU 的角度来看,从 RAM 中检索内存很慢。为了加快速度,它主要使用两种优化。

5.1 预取

第一种是预取:它提前获取更多内存,希望这是你感兴趣的内存。它总是猜测,如果你请求一个内存地址,你将对紧随其后的内存区域感兴趣。因此,顺序访问数据是关键。在下面的示例中,我们可以观察以随机顺序访问内存的影响。

// 设置:
function shuffle(array) {
  let currentIndex = array.length,  randomIndex;

  while (currentIndex > 0) {
    randomIndex = Math.floor(Math.random() * currentIndex);
    currentIndex--;
    [array[currentIndex], array[randomIndex]] = [
      array[randomIndex], array[currentIndex]];
  }

  return array;
}
// 设置:
const K = 1024
const length = 1 * K * K

// 这些点是一个接一个地创建的,因此它们在内存中是顺序分配的。
const points = new Array(length)
for (let i = 0; i < points.length; i++) {
  points[i] = { x: 42, y: 0 }
}

// 此数组包含与上述相同的数据,但是随机打乱了顺序。
const shuffledPoints = shuffle(points.slice())
// 1. 顺序
let _ = 0
for (let i = 0; i < points.length; i++) { _ += points[i].x }
// 2. 随机
let _ = 0
for (let i = 0; i < shuffledPoints.length; i++) { _ += shuffledPoints[i].x }

这一方面可能是最难以实践的,因为 JavaScript 没有一种方法可以将对象放入内存中。你不能假设顺序创建的对象会保持在相同的位置,因为垃圾收集器可能会将它们移动。有一个例外,那就是数字数组,最好是 TypedArray 实例:

// 从这里
const points = [{ x: 0, y: 5 }, { x: 0, y: 10 }]

// 到这里
const points = new Int64Array([0, 5, 0, 10])

有关更详细的示例,请参阅此链接*。
*请注意,它包含一些现在已过时的优化,但整体准确性仍然存在。

5.2 在 L1/2/3 中缓存

CPU 使用的第二种优化是 L1/L2/L3 缓存:这些就像更快的 RAM,但它们也更昂贵,所以它们要小得多。它们包含 RAM 数据,但充当 LRU 缓存。当数据处于“热”状态(正在处理)时,数据会进来,当新的工作数据需要空间时,它会被写回主 RAM。因此,关键在于尽可能少地使用数据,以保持您的工作数据集在快速缓存中。在下面的示例中,我们可以观察到破坏每个连续缓存的效果。

// 设置:
const KB = 1024
const MB = 1024 * KB

// 这些是大约的大小,以适应这些缓存。如果您在您的机器上没有得到相同的结果,可能是因为您的大小不同。
const L1  = 256 * KB
const L2  =   5 * MB
const L3  =  18 * MB
const RAM =  32 * MB

// 我们将为所有测试用例访问相同的缓冲区,但在第一种情况下,我们只会访问第 0 到 `L1` 条目,在第二种情况下,我们只会访问第 0 到 `L2` 条目,依此类推。
const buffer = new Int8Array(RAM)
buffer.fill(42)

const random = (max) => Math.floor(Math.random() * max)
// 1. L1
let r = 0; for (let i = 0; i < 100000; i++) { r += buffer[random(L1)] }
// 2. L2
let r = 0; for (let i = 0; i < 100000; i++) { r += buffer[random(L2)] }
// 3. L3
let r = 0; for (let i = 0; i < 100000; i++) { r += buffer[random(L3)] }
// 4. RAM
let r = 0; for (let i = 0; i < 100000; i++) { r += buffer[random(RAM)] }

无情地消除每一个可以消除的数据或内存分配。你的数据集越小,程序运行得越快。对于 95% 的程序来说,内存 I/O 是瓶颈。另一个很好的策略是将你的工作分成块,并确保你一次处理一小块数据集。

有关 CPU 和内存的更多详细信息,请参见此链接

关于不可变数据结构

不可变性对于清晰性和正确性来说非常好,但在性能方面,更新不可变数据结构意味着要复制容器,而这会增加更多的内存 I/O,使您的缓存失效。在可能的情况下,应避免不可变的数据结构。

关于 { …展开 } 运算符

它非常方便,但每次使用它时,都会在内存中创建一个新对象。更多的内存 I/O,更慢的缓存!

6. 避免大对象

如第2节所述,引擎使用形状来优化对象。然而,当形状变得太大时,引擎别无选择,只能使用常规哈希映射(类似于 Map 对象)。正如我们在第5节中看到的,缓存未命中会显著降低性能。哈希映射容易出现这种情况,因为它们的数据通常是随机和均匀分布在它们所占据的内存区域上的。让我们看看这个按 ID 索引的一些用户的地图的行为。

// 设置:
const USERS_LENGTH = 1_000
// 设置:
const byId = {}
Array.from({ length: USERS_LENGTH }).forEach((_, id) => {
  byId[id] = { id, name: 'John'}
})
let _ = 0
// 1. [] 访问
Object.keys(byId).forEach(id => { _ += byId[id].id })
// 2. 直接访问
Object.values(byId).forEach(user => { _ += user.id })

我们还可以观察到随着对象大小的增长,性能如何持续下降:

// 设置:
const USERS_LENGTH = 100_000
// 设置:
const byId = {}
Array.from({ length: USERS_LENGTH }).forEach((_, id) => {
  byId[id] = { id, name: 'John'}
})
let _ = 0
// 1. [] 访问
Object.keys(byId).forEach(id => { _ += byId[id].id })
// 2. 直接访问
Object.values(byId).forEach(user => { _ += user.id })

如上所示,避免频繁地索引大对象。最好事先将对象转换为数组。组织您的数据以在模型上具有 ID 可以有所帮助,因为您可以使用 Object.values() 而无需引用键映射来获取 ID。

7. 使用 eval

有些 JavaScript 模式很难为引擎优化,通过使用 eval() 或其衍生物,您可以使这些模式消失。在此示例中,我们可以观察使用 eval() 如何避免创建具有动态对象键的对象的成本:

// 设置:
const key = 'requestId'
const values = Array.from({ length: 100_000 }).fill(42)
// 1. 没有 eval
function createMessages(key, values) {
  const messages = []
  for (let i = 0; i < values.length; i++) {
    messages.push({ [key]: values[i] })
  }
  return messages
}

createMessages(key, values)
// 2. 使用 eval
function createMessages(key, values) {
  const messages = []
  const createMessage = new Function('value',
    `return { ${JSON.stringify(key)}: value }`
  )
  for (let i = 0; i < values.length; i++) {
    messages.push(createMessage(values[i]))
  }
  return messages
}

createMessages(key, values)

显然,有关 eval() 的通常警告也适用:不要相信用户输入,对传递到 eval() 的代码进行清理,并且不要创建任何 XSS 可能性。此外,请注意,某些环境不允许访问 eval(),例如具有 CSP 的浏览器页面。

8. 谨慎使用字符串

如上所述,字符串比它们看起来的要昂贵。好吧,我这里有一些好消息/坏消息的情况,我会按照唯一合乎逻辑的顺序(先坏后好)宣布:字符串比它们看起来的要复杂,但是如果使用得当,它们也可以非常高效。

字符串操作是 JavaScript 的核心部分,因为它的上下文。为了优化字符串密集的代码,引擎必须有创造性。我的意思是,它们必须使用多个字符串表示在 C++ 中表示 String 对象,具体取决于用例。有两种一般情况你应该担心,因为它们对于 V8(到目前为止,迄今为止最常见的引擎)是成立的,通常在其他引擎中也是如此。

首先,使用 + 连接的字符串不会创建两个输入字符串的副本。该操作创建对每个子字符串的指针。如果这是 TypeScript,它会像这样:

class String {
  abstract value(): char[] {}
}

class BytesString {
  constructor(bytes: char[]) {
    this.bytes = bytes
  }
  value() {
    return this.bytes
  }
}

class ConcatenatedString {
  constructor(left: String, right: String) {
    this.left = left
    this.right = right
  }
  value() {
    return [...this.left.value(), ...this.right.value()]
  }
}

function concat(left, right) {
  return new ConcatenatedString(left, right)
}

const first = new BytesString(['H', 'e', 'l', 'l', 'o', ' '])
const second = new BytesString(['w', 'o', 'r', 'l', 'd'])

// 瞧,没有数组复制!
const message = concat(first, second)

其次,字符串切片也不需要创建副本:它们可以简单地指向另一个字符串中的范围。继续以上面的例子:

class SlicedString {
  constructor(source: String, start: number, end: number) {
    this.source = source
    this.start = start
    this.end = end
  }
  value() {
    return this.source.value().slice(this.start, this.end)
  }
}

function substring(source, start, end) {
  return new SlicedString(source, start, end)
}

// 这表示 "He",但它仍然不包含数组副本。它是一个 `SlicedString`,指向一个 `ConcatenatedString`,然后是两个 `BytesString`。

但是这里有个问题:一旦您需要开始对这些字节进行突变,这就是您开始支付副本成本的时刻。假设我们回到我们的 String 类,并尝试添加一个 .trimEnd 方法:

class String {
  abstract value(): char[] {}

  trimEnd() {
    // `.value()` 可能在此处调用
    // 我们的 Sliced->Concatenated->2*Bytes 字符串!
    const bytes = this.value()

    const result = bytes.slice()
    while (result[result.length - 1] === ' ')
      result.pop()
    return new BytesString(result)
  }
}

所以让我们跳到一个例子,比较使用使用突变的操作和仅使用连接:

// 设置:
const classNames = ['primary', 'selected', 'active', 'medium']
// 1. 突变
const result =
  classNames
    .map(c => `button--${c}`)
    .join(' ')
// 2. 连接
const result =
  classNames
    .map(c => 'button--' + c)
    .reduce((acc, c) => acc + ' ' + c, '')

通常情况下,尽量避免突变。这包括诸如 .trim().replace() 等方法。考虑一下如何避免这些方法。在某些引擎中,字符串模板也可能比 + 更慢。目前在 V8 中,情况是如此。现在是这样,但未来可能不是,因此,像往常一样,请进行基准测试。

关于上述的 SlicedString,您应该注意,如果在内存中保留了大字符串的小子字符串,它可能会阻止垃圾回收器收集大字符串!如果您正在处理大文本并从中提取小字符串,那么您可能会泄漏大量内存。

const large = Array.from({ length: 10_000 }).map(() => 'string').join('')
const small = large.slice(0, 50)
//    ^ 将使 `large` 保持存活

这里的解决方案是利用突变方法。如果我们在 small 上使用其中一个方法,它将强制进行一次复制,旧的指向 large 的指针将丢失:

// 替换不存在的标记
const small = small.replace('#'.repeat(small.length + 1), '')

有关更多详细信息,请参见V8 中的 string.hJavaScriptCore 中的 JSString.h

关于字符串复杂性

我对事情的描述过于简略,但有很多实现细节会增加字符串的复杂性。通常有每个字符串表示的最小长度。例如,一个连接的字符串可能不适用于非常小的字符串。或者有时会有限制,例如避免指向子字符串的子字符串。阅读上面链接的 C++ 文件可以很好地了解实现细节,即使只是阅读注释。

9. 基准测试

在进行基准测试时,应考虑以下常见事项:

9.0 基准测试-优化循环

您应始终 1. 进行基准测试,然后 2. 优化列表中尚未完全优化的最高条目,然后 3. 转到步骤 1。

9.1 避免微基准测试

在生产模式下运行您的代码,并根据这些观察结果进行优化。JavaScript 引擎非常复杂,通常在微基准测试中的行为与实际场景中的行为不同。例如,考虑以下微基准测试:

const a = { type: 'div', count: 5, }
const b = { type: 'span', count: 10 }

function typeEquals(a, b) {
  return a.type === b.type
}

for (let i = 0; i < 100_000; i++) {
  typeEquals(a, b)
}

如果您稍早留意到了,您会意识到引擎将会为 { type: string, count: number } 的形状专门化该函数。但是在您的实际用例中是否成立?props 是否始终具有该形状,还是您还需要支持任何类型的形状?如果在生产环境中接收到许多形状,则此函数将表现出不同的行为。

9.2 怀疑您的结果

如果您刚刚优化了一个函数,使其运行速度提高了 100 倍,请对此表示怀疑。尝试证明您的结果是错误的,在生产模式下尝试它们,进行各种测试。

9.3 选择目标

不同的引擎将对某些模式进行更好或更差的优化。您应该为您相关的引擎进行基准测试,并优先考虑哪个引擎更重要。这里有一个实际的例子,在 Babel 中改进 V8 引擎意味着降低 JSC 的性能。

10. 分析 & 工具

有关分析和开发工具的各种注意事项。

10.1 浏览器陷阱

如果您在浏览器中进行分析,请确保使用干净和空白的浏览器配置文件。我甚至会为此使用单独的浏览器。如果您在分析过程中启用了浏览器扩展程序,它们可能会干扰测量结果。特别是 React 开发工具会显着影响结果,渲染代码可能比您的用户看到的效果慢。

10.2 抽样与结构化分析

浏览器分析工具是基于抽样的分析器,它在固定时间间隔内对您的堆栈进行采样。这有一个很大的缺点:非常小但非常频繁的函数可能会在这些样本之间被调用,并且在您收到的堆栈图表中可能被大大低估。使用 Firefox 开发工具自定义样本间隔或使用 CPU 限制功能的 Chrome 开发工具以避免此问题。

10.3 交易工具

除了常规的浏览器开发工具之外,了解以下选项可能会有所帮助:

  • Chrome 开发工具有许多实验性标志,可以帮助您弄清楚为什么事情会慢。当您需要在浏览器中调试样式/布局重新计算时,样式失效跟踪器是非常宝贵的。
    https://github.com/iamakulov/devtools-perf-features

  • deoptexplorer-vscode 扩展允许您加载 V8/chromium 日志文件,以了解何时触发了您的代码的非优化情况,例如当您将不同形状传递给函数时。您不需要扩展程序来读取日志文件,但它会使体验更加愉快。
    https://github.com/microsoft/deoptexplorer-vscode

  • 您始终可以为每个 JS 引擎编译调试 shell,以更详细地了解其工作原理。这使您可以运行 perf 和其他低级工具,并检查每个引擎生成的字节码和机器码。
    V8 的示例 | JSC 的示例 | SpiderMonkey 的示例 (缺失)

最后的说明

希望您学到了一些有用的技巧,如果您有任何评论或更正,请查看页脚中的电子邮件链接。

2024-03-22

访问量 200

扫码关注公众号“前端微志”

第一时间获取新周刊

预览图片