关于 js-performance-patterns
面向 JavaScript 开发者的性能模式参考,聚焦经测量确认的热点路径,涵盖循环、查找和 DOM 操作等。强调先分析性能,再针对关键代码优化,避免影响非热点代码的可读性。
主要功能
- 01使用 Set 和 Map 查找
- 02批量执行 DOM 读写
- 03缓存紧凑循环中的值
- 04记忆化重复计算
- 05合并数据遍历
- 06用 requestAnimationFrame 调度视觉更新
适用场景
- 01优化大型集合中的重复查找
- 02减少 DOM 更新引发的布局重算
- 03优化高频事件处理程序
- 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 性能指南。