我经常觉得 javascript 代码总体运行速度比它应有的速度慢得多,这仅仅是因为它没有得到适当的优化。下面是我发现有用的常见优化技术的总结。请注意,性能的权衡往往是可读性,因此何时追求性能还是可读性的问题留给读者去思考。我还要指出,谈论优化必然需要谈论基准测试。如果函数只占实际整体运行时间的一小部分,那么花几个小时对函数进行微优化以使其运行速度提高 100 倍是毫无意义的。如果要进行优化,第一步也是最重要的一步就是基准测试。我将在后面的要点中介绍这个主题。还要注意,微基准测试往往存在缺陷,可能包括这里介绍的那些。我已尽力避免这些陷阱,但不要盲目地应用这里介绍的任何要点而不进行基准测试。

我已经为所有可能的情况提供了可运行的示例。它们默认显示我在我的机器上得到的结果(archlinux 上的 brave 122),但你可以自己运行它们。尽管我不愿意这么说,但 Firefox 在优化游戏中已经落后了一点,只占流量的很小一部分 目前,所以我不建议使用您在 Firefox 上获得的结果作为有用的指标。

0.逃避工作

这听起来可能很明显,但它必须在这里,因为优化不能再有另一个第一步:如果你想要优化,你应该首先考虑避免工作。这包括记忆化、惰性和增量计算等概念。这将根据上下文以不同的方式应用。例如,在 React 中,这意味着应用 memo()useMemo() 以及其他适用的原语。

1. 避免字符串比较

JavaScript 可以轻松隐藏字符串比较的实际成本。如果您需要在 C 中比较字符串,则可以使用 strcmp(a, b) 函数。JavaScript 使用 === 相反,这样你就看不到 strcmp但它就在那里,并且字符串比较通常(但并非总是)需要将字符串中的每个字符与另一个字符串中的字符进行比较;字符串比较是 O(n)。应避免的一种常见 JavaScript 模式是将字符串用作枚举。但随着 TypeScript 的出现,这种情况应该很容易避免,因为枚举默认为整数。

// No
enum Position {
  TOP    = 'TOP',
  BOTTOM = 'BOTTOM',
}
// Yeppers
enum Position {
  TOP,    // = 0
  BOTTOM, // = 1
}

以下是成本的比较:

// 1. string compare
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. int compare
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
}

正如你所见,差异可能很大。差异不一定是由于 strcmp 成本较高,因为引擎有时会使用字符串池并通过引用进行比较,但这也是因为整数在 JS 引擎中通常按值传递,而字符串始终作为指针传递,并且内存访问成本较高(请参阅第 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 种以上形状)。假设你 真的 想要保持单态,因为速度下降非常厉害:

// setup
let _ = 0
// 1. monomorphic
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: _ } // all shapes are equal
// 2. polymorphic
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: _ } // this shape is different
// 3. megamorphic
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 } // all shapes are different
// test case
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 组件 props 可能会触发此问题

例如,以下是 简单案例 我在 React 的代码库中找到了,但他们已经有一个 影响更大的案件 几年前也遇到过同样的问题,因为他们用整数初始化对象,然后存储浮点数。是的,改变类型也会改变形状。是的,背后隐藏着整数和浮点类型 number.处理它。

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 loop 允许循环一次。
// setup:
const numbers = Array.from({ length: 10_000 }).map(() => Math.random())
// 1. functional
const result =
  numbers
    .map(n => Math.round(n * 10))
    .filter(n => n % 2 === 0)
    .reduce((a, n) => a + n, 0)
// 2. imperative
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.
// Proxy objects are harder to optimize because their get/set function might
// be running custom logic, so engines can't make their usual assumptions.
const proxy = new Proxy(point, { get: (t, k) => { return t[k] } })
// Some engines can make proxy costs disappear, but those optimizations are
// expensive to make and can break easily.
const x = proxy.x
 
// 2.
// Usually ignored, but accessing an object via `.` or `[]` is also an
// indirection. In easy cases, the engine may very well be able to optimize the
// cost away:
const x = point.x
// But each additional access multiplies the cost, and makes it harder for the
// engine to make assumptions about the state of `point`:
const x = this.state.circle.center.point.x
 
// 3.
// And finally, function calls can also have a cost. Engine are generally good
// at inlining these:
function getX(p) { return p.x }
const x = getX(p)
// But it's not guaranteed that they can. In particular if the function call
// isn't from a static function but comes from e.g. an argument:
function Component({ point, getX }) {
  return getX(point)
}

目前,V8 上的代理基准测试尤其残酷。上次我检查时,代理对象总是从 JIT 回退到解释器,从这些结果来看,情况可能仍然如此。

// 1. proxy access
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. direct access
const point = { x: 10, y: 20 }
const x = point.x
 
for (let _ = 0, i = 0; i < 100_000; i++) { _ += x }

我还想展示访问深层嵌套对象与直接访问的区别,但引擎非常擅长 通过逃逸分析优化对象访问 当存在热循环和常量对象时。我插入了一些间接方法来防止这种情况。

// 1. nested access
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. direct access
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) }

5. 避免缓存未命中

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

5.1 预取

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

// setup:
const K = 1024
const length = 1 * K * K
 
// Theses points are created one after the other, so they are allocated
// sequentially in memory.
const points = new Array(length)
for (let i = 0; i < points.length; i++) {
  points[i] = { x: 42, y: 0 }
}
 
// This array contains the *same data* as above, but shuffled randomly.
const shuffledPoints = shuffle(points.slice())
// 1. sequential
let _ = 0
for (let i = 0; i < points.length; i++) { _ += points[i].x }
// 2. random
let _ = 0
for (let i = 0; i < shuffledPoints.length; i++) { _ += shuffledPoints[i].x }

我到底该怎么做呢?

这方面可能是最难实践的,因为 javascript 没有办法将对象放入内存中,但你可以利用这些知识,如上例所示,例如在重新排序或排序数据之前对其进行操作。你不能假设按顺序创建的对象会在一段时间后停留在同一位置,因为垃圾收集器可能会移动它们。有一个例外,那就是数字数组,最好 TypedArray 实例:

// from this
const points = [{ x: 0, y: 5 }, { x: 0, y: 10 }]
 
// to this
const points = new Int64Array([0, 5, 0, 10])

更详细的例子如下: 参见此链接*。
*请注意,它包含一些现在已经过时的优化,但总体而言仍然是准确的。

5.2 L1/2/3 中的缓存

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

// setup:
const KB = 1024
const MB = 1024 * KB
 
// These are approximate sizes to fit in those caches. If you don't get the
// same results on your machine, it might be because your sizes differ.
const L1  = 256 * KB
const L2  =   5 * MB
const L3  =  18 * MB
const RAM =  32 * MB
 
// We'll be accessing the same buffer for all test cases, but we'll
// only be accessing the first 0 to `L1` entries in the first case,
// 0 to `L2` in the second, etc.
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)] }

我到底该怎么做呢?

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

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

6. 避免使用大型物体

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

// setup:
const USERS_LENGTH = 1_000
// setup:
const byId = {}
Array.from({ length: USERS_LENGTH }).forEach((_, id) => {
  byId[id] = { id, name: 'John'}
})
let _ = 0
// 1. [] access
Object.keys(byId).forEach(id => { _ += byId[id].id })
// 2. direct access
Object.values(byId).forEach(user => { _ += user.id })

我们还可以观察到,随着物体尺寸的增加,性能如何不断下降:

// setup:
const USERS_LENGTH = 100_000

我到底该怎么做呢?

如上所示,避免频繁索引大型对象。最好事先将对象转换为数组。组织数据以在模型上使用 ID 会有所帮助,因为您可以使用 Object.values() 而不必参考键图来获取 ID。

7. 使用 eval

有些 JavaScript 模式很难针对引擎进行优化,因此使用 eval() 或其衍生物,你可以让这些图案消失。在这个例子中,我们可以观察如何使用 eval() 避免使用动态对象键创建对象的成本:

// setup:
const key = 'requestId'
const values = Array.from({ length: 100_000 }).fill(42)
// 1. without 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. with 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() 应用:不信任用户输入,净化传入的所有内容 eval()代码,并且不会产生任何 XSS 可能性。另请注意,某些环境不允许访问 eval(),例如带有 CSP 的浏览器页面。

8. 谨慎使用字符串

我们已经在上面看到字符串比它们看起来的要昂贵得多。好吧,我这里有一个好消息/坏消息的情况,我将按照唯一合乎逻辑的顺序宣布(坏消息在前,好消息在后):字符串比它们看起来的要复杂,但使用得当也可以非常高效。

由于其上下文,字符串操作是 JavaScript 的核心部分。为了优化字符串密集型代码,引擎必须发挥创造力。我的意思是,它们必须表示 String 根据用例,C++ 中具有多个字符串表示的对象。您应该担心两种一般情况,因为它们适用于 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'])
 
// See ma, no array copies!
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)
}
 
// This represents "He", but it still contains no array copies.
// It's a SlicedString to a ConcatenatedString to two BytesString
const firstTwoLetters = substring(message, 0, 2)

但问题是:一旦你需要开始改变这些字节,你就开始支付复制成本。假设我们回到我们的 String 类并尝试添加一个 .trimEnd 方法:

class String {
  abstract value(): char[] {}
 
  trimEnd() {
    // `.value()` here might be calling
    // our Sliced->Concatenated->2*Bytes string!
    const bytes = this.value()
 
    const result = bytes.slice()
    while (result[result.length - 1] === ' ')
      result.pop()
    return new BytesString(result)
  }
}

因此,让我们跳到一个例子,比较使用变异的操作与仅使用连接的操作:

// setup:
const classNames = ['primary', 'selected', 'active', 'medium']
// 1. mutation
const result =
  classNames
    .map(c => `button--${c}`)
    .join(' ')
// 2. concatenation
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)
//    ^ will keep `large` alive

这里的解决办法是利用变异方法。如果我们在 small,它将强制复制,并将旧指针指向 large 会迷路:

// replace a token that doesn't exist
const small = small.replace('#'.repeat(small.length + 1), '')

有关详细信息,请参阅 V8 上的 string.h 或者 JavaScriptCore 上的 JSString.h

9. 使用专业化

性能优化中的一个重要概念是 专业化:调整逻辑以适应特定用例的限制。这通常意味着弄清楚哪些条件 可能 适合您的情况,并针对这些情况进行编码。

假设我们是一个商家,有时需要向其产品列表添加标签。根据经验,我们知道我们的标签通常是空的。了解了这些信息后,我们可以针对这种情况专门设计我们的函数:

// setup:
const descriptions = ['apples', 'oranges', 'bananas', 'seven']
const someTags = {
  apples: '::promotion::',
}
const noTags = {}
 
// Turn the products into a string, with their tags if applicable
function productsToString(description, tags) {
  let result = ''
  description.forEach(product => {
    result += product
    if (tags[product]) result += tags[product]
    result += ', '
  })
  return result
}
 
// Specialize it now
function productsToStringSpecialized(description, tags) {
  // We know that `tags` is likely to be empty, so we check
  // once ahead of time, and then we can remove the `if` check
  // from the inner loop
  if (isEmpty(tags)) {
    let result = ''
    description.forEach(product => {
      result += product + ', '
    })
    return result
  } else {
    let result = ''
    description.forEach(product => {
      result += product
      if (tags[product]) result += tags[product]
      result += ', '
    })
    return result
  }
}
function isEmpty(o) { for (let _ in o) { return false } return true }
 
// 1. not specialized
for (let i = 0; i < 100; i++) {
  productsToString(descriptions, someTags)
  productsToString(descriptions, noTags)
  productsToString(descriptions, noTags)
  productsToString(descriptions, noTags)
  productsToString(descriptions, noTags)
}
// 2. specialized
for (let i = 0; i < 100; i++) {
  productsToStringSpecialized(descriptions, someTags)
  productsToStringSpecialized(descriptions, noTags)
  productsToStringSpecialized(descriptions, noTags)
  productsToStringSpecialized(descriptions, noTags)
  productsToStringSpecialized(descriptions, noTags)
}

这种优化可以给你带来适度的改进,但这些改进会累积起来。它们是对更关键的优化(如形状和内存 I/O)的一个很好的补充。但请注意,如果你的条件发生变化,专业化可能会对你不利,所以在应用这一优化时要小心。

10.数据结构

我不会详细介绍数据结构,因为它们需要单独的帖子。但请注意,使用不正确的数据结构可能会产生 比上述任何优化都更有影响力。我建议你熟悉一下本地的 MapSet,并了解链表、优先级队列、树(RB 和 B+)和字典树。

举个简单的例子,让我们比较一下 Array.includes 反对 Set.has 附一个小清单:

// setup:
const userIds = Array.from({ length: 1_000 }).map((_, i) => i)
const adminIdsArray = userIds.slice(0, 10)
const adminIdsSet = new Set(adminIdsArray)
// 1. Array
let _ = 0
for (let i = 0; i < userIds.length; i++) {
  if (adminIdsArray.includes(userIds[i])) { _ += 1 }
}
// 2. Set
let _ = 0
for (let i = 0; i < userIds.length; i++) {
  if (adminIdsSet.has(userIds[i])) { _ += 1 }
}

正如您所见,数据结构的选择会产生很大的影响。

举一个现实世界的例子,我有一个案例,我们能够 将函数的运行时间从 5 秒缩短至 22 毫秒 通过用链接列表替换数组。

11. 基准测试

我把这一部分留到最后的原因只有一个:我需要通过上面有趣的部分来建立可信度。现在我(希望)已经明白了,让我告诉你,基准测试是优化中最重要的部分。它不仅是最重要的,而且也是 难的。即使有 20 年的经验,我仍然有时会创建有缺陷的基准,或者错误地使用分析工具。所以无论你做什么,请 尽最大努力进行正确的基准测试

11.0 从顶部开始

您的首要任务始终是优化构成运行时最大部分的函数/代码段。如果您花时间优化除顶部之外的任何内容,那么您就是在浪费时间。

11.1 避免微基准测试

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

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 }。但这是否适用于您的实际用例? ab 总是这种形状,还是会收到任何形状?如果在生产中收到多种形状,则此功能的行为会有所不同。

11.2 怀疑你的结果

如果您刚刚优化了某个函数,现在它的运行速度提高了 100 倍,请怀疑它。尝试反驳您的结果,在生产模式下尝试,向其投入一些东西。同样,也要怀疑您的工具。仅仅使用 devtools 观察基准测试就可以改变其行为。

11.3 选择目标

不同的引擎对某些模式的优化程度会比其他引擎更好或更差。您应该针对与您相关的引擎进行基准测试,并确定哪个引擎更重要。 这是一个真实的例子 在 Babel 中,改进 V8 意味着降低 JSC 的性能。

关于分析和开发工具的各种评论。

12.1 浏览器陷阱

如果您在浏览器中进行性能分析,请确保使用干净且空白的浏览器配置文件。我甚至为此使用了单独的浏览器。如果您在进行性能分析并且启用了浏览器扩展,它们可能会弄乱测量结果。React devtools 尤其会对结果产生重大影响,渲染代码可能看起来比实际更慢 镜子里 给您的用户。

12.2 样本与结构分析

浏览器分析工具是基于样本的分析器,会定期对您的堆栈进行采样。这有一个很大的缺点:在这些样本之间可能会调用非常小但非常频繁的函数,并且可能会在您获得的堆栈图中被严重低估。使用具有自定义采样间隔的 Firefox devtools 或具有 CPU 限制的 Chrome devtools 来缓解此问题。

12.3 行业工具

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

  • Chrome devtools 有相当多的实验性标记,可以帮助您找出运行缓慢的原因。当您需要调试浏览器中的样式/布局重新计算时,样式失效跟踪器非常有用。

  • deoptexplorer-vscode 扩展允许您加载 V8/chromium 日志文件,以了解您的代码何时触发去优化,例如当您将不同的形状传递给函数时。您不需要扩展来读​​取日志文件,但它会让体验更加愉快。

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

最后说明

希望您学到了一些有用的技巧。如果您有任何评论、更正或问题,请在页脚中发送电子邮件。我很乐意收到读者的反馈或问题。

如果您已经走到这一步,我邀请您来参观城堡。

Leave a Reply

Your email address will not be published. Required fields are marked *