mcpField
J
js-performance-patterns

js-performance-patterns

github.com/patternsdev开发257691 次安装28 周前更新
代码仓库

关于 js-performance-patterns

面向 JavaScript 开发者的性能模式参考,聚焦经测量确认的热点路径,涵盖循环、查找和 DOM 操作等。强调先分析性能,再针对关键代码优化,避免影响非热点代码的可读性。

主要功能

  1. 01使用 Set 和 Map 查找
  2. 02批量执行 DOM 读写
  3. 03缓存紧凑循环中的值
  4. 04记忆化重复计算
  5. 05合并数据遍历
  6. 06用 requestAnimationFrame 调度视觉更新

适用场景

  1. 01优化大型集合中的重复查找
  2. 02减少 DOM 更新引发的布局重算
  3. 03优化高频事件处理程序
  4. 04审查已分析的 JavaScript 热点路径

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/patternsdev/skills --skill js-performance-patterns

安装后在 Claude Code 中输入/js-performance-patterns确认已加载。

详细介绍

JavaScript 性能模式

目录

适用于 JavaScript 热路径的运行时性能微模式。这些模式在紧密循环、高频回调(滚动、调整大小、动画帧)和数据密集型操作中尤为重要。它们适用于任何 JavaScript 环境——React、Vue、原生 JavaScript、Node.js。

适用场景

在以下情况参考这些模式:

  • 性能分析发现了热点函数或紧密循环
  • 处理大型数据集(1,000 项以上)
  • 处理高频事件(滚动、鼠标移动、调整大小)
  • 优化构建时或服务端脚本
  • 审查关键路径中的代码性能

说明

  • 仅在经过测量的热路径中应用这些模式——也就是频繁运行或处理大型数据集的代码。不要将它们用于可读性比纳秒级收益更重要的冷路径代码。

详情

概述

微优化不能替代算法改进。首先解决算法问题(将 O(n^2) 改为 O(n)、消除瀑布式调用、减少 DOM 修改)。算法正确后,再使用这些模式进一步提升热路径的性能。


1. 使用 Set 和 Map 进行查找

影响:大型集合中为高——每次查找为 O(1),而非 O(n)。

.includes()、.find() 和 .indexOf() 等数组方法会线性扫描。若要针对同一个集合反复查找,先将其转换为 Set 或 Map。

避免——每次检查为 O(n):

const allowedIds = ['a', 'b', 'c', /* ...hundreds more */]

function isAllowed(id: string) {
  return allowedIds.includes(id) // scans entire array
}

items.filter(item => allowedIds.includes(item.id)) // O(n * m)

推荐——每次检查为 O(1):

const allowedIds = new Set(['a', 'b', 'c', /* ...hundreds more */])

function isAllowed(id: string) {
  return allowedIds.has(id)
}

items.filter(item => allowedIds.has(item.id)) // O(n)

对于键值查找,使用 Map,而不是扫描对象数组:

// Avoid
const users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]
const user = users.find(u => u.id === targetId) // O(n)

// Prefer
const userMap = new Map(users.map(u => [u.id, u]))
const user = userMap.get(targetId) // O(1)

2. 批量读取和写入 DOM

影响:高——避免布局抖动。

交替读取 DOM(例如 offsetHeight、getBoundingClientRect)和写入 DOM(例如 style.height = ...)会迫使浏览器多次重新计算布局。先批量完成所有读取,再批量完成所有写入。

避免——布局抖动(读/写/读/写):

elements.forEach(el => {
  const height = el.offsetHeight    // read → forces layout
  el.style.height = `${height * 2}px` // write
})
// Each iteration forces a layout recalculation

推荐——先批量读取,再批量写入:

// Read phase
const heights = elements.map(el => el.offsetHeight)

// Write phase
elements.forEach((el, i) => {
  el.style.height = `${heights[i] * 2}px`
})

对于复杂情况,使用 requestAnimationFrame 将写入延迟到下一帧,或使用 fastdom 之类的库。

CSS 类方案——只触发一次重排:

// Avoid multiple style mutations
el.style.width = '100px'
el.style.height = '200px'
el.style.margin = '10px'

// Prefer — one reflow
el.classList.add('expanded')
// or
el.style.cssText = 'width:100px;height:200px;margin:10px;'

3. 在紧密循环中缓存属性访问

影响:中——减少重复的属性解析。

在每次迭代中访问深层嵌套属性或数组的 .length,会在紧密循环中增加开销。

避免:

for (let i = 0; i < data.items.length; i++) {
  process(data.items[i].value.nested.prop)
}

推荐:

const { items } = data
for (let i = 0, len = items.length; i < len; i++) {
  const val = items[i].value.nested.prop
  process(val)
}

对于包含 10,000 项以上的数组,或以 60fps 调用时,这一点很重要。对于小型数组或低频调用,可读性更好的版本即可。


4. 记忆化开销较大的函数结果

影响:中到高——避免重复计算相同结果。

当纯函数反复使用相同参数调用时,缓存其结果。

简单的单值缓存:

function memoize<T extends (...args: any[]) => any>(fn: T): T {
  let lastArgs: any[] | undefined
  let lastResult: any

  return ((...args: any[]) => {
    if (lastArgs && args.every((arg, i) => Object.is(arg, lastArgs![i]))) {
      return lastResult
    }
    lastArgs = args
    lastResult = fn(...args)
    return lastResult
  }) as T
}

const expensiveCalc = memoize((data: number[]) => {
  return data.reduce((sum, n) => sum + heavyTransform(n), 0)
})

使用 Map 的多键缓存:

const cache = new Map<string, Result>()

function getResult(key: string): Result {
  if (cache.has(key)) return cache.get(key)!
  const result = computeExpensiveResult(key)
  cache.set(key, result)
  return result
}

对于可能无限增长的缓存,使用 LRU 策略,或对对象键使用 WeakMap。


5. 合并对同一数据的遍历

影响:中——单次遍历取代多次遍历。

链式调用 .filter().map().reduce() 会创建中间数组,并多次遍历数据。对于热路径中的大型数组,将操作合并到单个循环中。

避免——3 次遍历,2 个中间数组:

const result = users
  .filter(u => u.active)
  .map(u => u.name)
  .reduce((acc, name) => acc + name + ', ', '')

推荐——单次遍历:

let result = ''
for (const u of users) {
  if (u.active) {
    result += u.name + ', '
  }
}

对于小型数组(少于 100 项),链式写法即可,且可读性更好。只有在性能分析显示确有影响时才进行优化。


6. 优先检查长度,提前短路

影响:低到中——避免对空输入执行开销较大的操作。

执行开销较大的比较或转换之前,先检查输入是否为空。

function findMatchingItems(items: Item[], query: string): Item[] {
  if (items.length === 0 || query.length === 0) return []

  const normalized = query.toLowerCase()
  return items.filter(item =>
    item.name.toLowerCase().includes(normalized)
  )
}

7. 提前返回,跳过不必要的工作

影响:低至中 — 提升平均情况下的执行效率。

合理组织函数,在常见的不匹配情况下尽早退出。

避免 — 总是执行完整操作:

function processEvent(event: AppEvent) {
  let result = null
  if (event.type === 'click') {
    if (event.target && event.target.matches('.actionable')) {
      result = handleAction(event)
    }
  }
  return result
}

推荐 — 提前退出:

function processEvent(event: AppEvent) {
  if (event.type !== 'click') return null
  if (!event.target?.matches('.actionable')) return null
  return handleAction(event)
}

8. 将 RegExp 和常量的创建移到循环外

影响:低至中 — 避免重复编译。

在循环内或频繁调用的函数中创建 RegExp 对象或常量值会浪费 CPU。

避免 — 编译正则表达式 10,000 次:

function validate(items: string[]) {
  return items.filter(item => {
    const pattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
    return pattern.test(item)
  })
}

推荐 — 只编译一次:

const EMAIL_PATTERN = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/

function validate(items: string[]) {
  return items.filter(item => EMAIL_PATTERN.test(item))
}

所有现代浏览器和 Node.js 20+ 均支持这些方法。


9. 使用 toSorted()、toReversed()、toSpliced() 实现不可变操作

影响:低 — 无需手动复制即可正确实现不可变操作。

新的非变更数组方法无需使用 [...arr].sort() 模式,也能更清晰地表达意图。

避免 — 手动复制后再修改:

const sorted = [...items].sort((a, b) => a.price - b.price)
const reversed = [...items].reverse()
const without = [...items]; without.splice(index, 1)

推荐 — 使用非变更方法:

const sorted = items.toSorted((a, b) => a.price - b.price)
const reversed = items.toReversed()
const without = items.toSpliced(index, 1)

所有现代浏览器和 Node.js 20+ 均支持这些方法。


10. 使用 requestAnimationFrame 更新视觉内容

影响:中 — 与浏览器的渲染周期同步。

在渲染周期之外触发的 DOM 更新(例如由计时器、事件处理程序等触发)可能导致卡顿。将视觉更新批量放入 requestAnimationFrame 中。

避免 — 在渲染周期之外更新:

window.addEventListener('scroll', () => {
  progressBar.style.width = `${getScrollPercent()}%`
  counter.textContent = `${getScrollPercent()}%`
}, { passive: true })

推荐 — 与渲染同步:

let ticking = false

window.addEventListener('scroll', () => {
  if (!ticking) {
    requestAnimationFrame(() => {
      const pct = getScrollPercent()
      progressBar.style.width = `${pct}%`
      counter.textContent = `${pct}%`
      ticking = false
    })
    ticking = true
  }
}, { passive: true })

11. 使用 structuredClone 进行深拷贝

影响:低 — 无需库即可正确进行深拷贝。

与 JSON.parse(JSON.stringify()) 不同,structuredClone() 能处理循环引用、类型化数组、Dates、RegExps、Maps 和 Sets。

// Avoid — loses Dates, Maps, Sets, undefined values
const copy = JSON.parse(JSON.stringify(original))

// Prefer — handles all standard types
const copy = structuredClone(original)

注意:structuredClone 无法克隆函数或 DOM 节点。对于这些情况,请实现自定义克隆。


12. 动态键优先使用 Map,而不是普通对象

影响:低至中 — 频繁添加或删除时性能更好。

V8 针对静态形状优化普通对象。当键动态添加和删除时(例如缓存、计数器、注册表),Map 能提供更稳定的性能。

// Avoid for dynamic keys
const counts: Record<string, number> = {}
items.forEach(item => {
  counts[item.category] = (counts[item.category] || 0) + 1
})

// Prefer for dynamic keys
const counts = new Map<string, number>()
items.forEach(item => {
  counts.set(item.category, (counts.get(item.category) ?? 0) + 1)
})

来源

这些模式来自 patterns.dev — 面向更广泛 Web 工程社区的 JavaScript 性能指南。

评论

支持 Markdown