js-performance-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

JavaScript Performance Patterns

JavaScript运行时性能优化模式

Runtime performance micro-patterns for JavaScript hot paths. These patterns matter most in tight loops, frequent callbacks (scroll, resize, animation frames), and data-heavy operations. They apply to any JavaScript environment — React, Vue, vanilla, Node.js.
针对JavaScript热路径的运行时性能微优化模式。这些模式在密集循环、频繁回调(如scroll、resize、动画帧)和数据密集型操作中效果最为显著。它们适用于所有JavaScript环境——React、Vue、原生JavaScript、Node.js。

When to Use

适用场景

Reference these patterns when:
  • Profiling reveals a hot function or tight loop
  • Processing large datasets (1,000+ items)
  • Handling high-frequency events (scroll, mousemove, resize)
  • Optimizing build-time or server-side scripts
  • Reviewing code for performance in critical paths
在以下场景中参考这些模式:
  • 性能分析显示存在热点函数或密集循环
  • 处理大型数据集(1000条以上数据)
  • 处理高频事件(scroll、mousemove、resize)
  • 优化构建时或服务端脚本
  • 审查关键路径中的代码性能

Instructions

使用说明

  • Apply these patterns only in measured hot paths — code that runs frequently or processes large datasets. Don't apply them to cold code paths where readability is more important than nanosecond gains.
  • 仅在经性能分析确认的热路径中应用这些模式——即频繁运行或处理大型数据集的代码。对于冷代码路径,可读性比纳秒级的性能提升更重要,请勿应用这些模式。

Details

详细内容

Overview

概述

Micro-optimizations are not a substitute for algorithmic improvements. Always address the algorithm first (O(n^2) to O(n), removing waterfalls, reducing DOM mutations). Once the algorithm is right, these patterns squeeze additional performance from hot paths.

微优化不能替代算法层面的改进。请始终优先优化算法(例如将O(n²)复杂度优化为O(n)、消除请求瀑布、减少DOM修改)。在算法优化完成后,这些模式可以进一步挖掘热路径的性能潜力。

1. Use
Set
and
Map
for Lookups

1. 使用
Set
Map
实现快速查找

Impact: HIGH for large collections — O(1) vs O(n) per lookup.
Array methods like
.includes()
,
.find()
, and
.indexOf()
scan linearly. For repeated lookups against the same collection, convert to
Set
or
Map
first.
Avoid — O(n) per check:
typescript
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)
Prefer — O(1) per check:
typescript
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)
For key-value lookups, use
Map
instead of scanning an array of objects:
typescript
// 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)

影响:大型集合下效果显著——每次查找的时间复杂度为O(1),远优于数组的O(n)。
数组的
.includes()
.find()
.indexOf()
方法都是线性扫描。如果需要对同一集合进行多次查找,建议先将其转换为
Set
Map
不推荐——每次检查复杂度为O(n):
typescript
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):
typescript
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
替代数组对象扫描:
typescript
// 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. Batch DOM Reads and Writes

2. 批量处理DOM读写

Impact: HIGH — Prevents layout thrashing.
Interleaving DOM reads (e.g.,
offsetHeight
,
getBoundingClientRect
) with DOM writes (e.g.,
style.height = ...
) forces the browser to recalculate layout multiple times. Batch all reads first, then all writes.
Avoid — layout thrashing (read/write/read/write):
typescript
elements.forEach(el => {
  const height = el.offsetHeight    // read → forces layout
  el.style.height = `${height * 2}px` // write
})
// Each iteration forces a layout recalculation
Prefer — batched reads then writes:
typescript
// Read phase
const heights = elements.map(el => el.offsetHeight)

// Write phase
elements.forEach((el, i) => {
  el.style.height = `${heights[i] * 2}px`
})
For complex cases, use
requestAnimationFrame
to defer writes to the next frame, or use a library like fastdom.
CSS class approach — single reflow:
typescript
// 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;'

影响:效果显著——避免布局抖动。
交替执行DOM读取(如
offsetHeight
getBoundingClientRect
)和DOM写入(如
style.height = ...
)会迫使浏览器多次重新计算布局。建议先批量完成所有读取操作,再执行所有写入操作。
不推荐——布局抖动(读/写/读/写交替):
typescript
elements.forEach(el => {
  const height = el.offsetHeight    // read → forces layout
  el.style.height = `${height * 2}px` // write
})
// Each iteration forces a layout recalculation
推荐——先批量读取再批量写入:
typescript
// 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类方案——仅一次重排:
typescript
// 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. Cache Property Access in Tight Loops

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

Impact: MEDIUM — Reduces repeated property resolution.
Accessing deeply nested properties or array
.length
in every iteration adds overhead in tight loops.
Avoid:
typescript
for (let i = 0; i < data.items.length; i++) {
  process(data.items[i].value.nested.prop)
}
Prefer:
typescript
const { items } = data
for (let i = 0, len = items.length; i < len; i++) {
  const val = items[i].value.nested.prop
  process(val)
}
This matters for arrays with 10,000+ items or when called at 60fps. For small arrays or infrequent calls, the readable version is fine.

影响:中等——减少重复的属性解析开销。
在每次循环迭代中访问深层嵌套属性或数组
.length
会增加额外开销。
不推荐:
typescript
for (let i = 0; i < data.items.length; i++) {
  process(data.items[i].value.nested.prop)
}
推荐:
typescript
const { items } = data
for (let i = 0, len = items.length; i < len; i++) {
  const val = items[i].value.nested.prop
  process(val)
}
这在处理10000条以上数据的数组或60fps高频调用场景中尤为重要。对于小型数组或低频调用,保持可读性的版本即可。

4. Memoize Expensive Function Results

4. 缓存昂贵函数的计算结果

Impact: MEDIUM-HIGH — Avoids recomputing the same result.
When a pure function is called repeatedly with the same arguments, cache the result.
Simple single-value cache:
typescript
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)
})
Multi-key cache with Map:
typescript
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
}
For caches that can grow unbounded, use an LRU strategy or
WeakMap
for object keys.

影响:中高——避免重复计算相同结果。
当纯函数被反复调用且参数相同时,缓存其计算结果。
简单单值缓存:
typescript
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的多键缓存:
typescript
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. Combine Iterations Over the Same Data

5. 合并对同一数据的多次迭代

Impact: MEDIUM — Single pass instead of multiple.
Chaining
.filter().map().reduce()
creates intermediate arrays and iterates the data multiple times. For large arrays in hot paths, combine into a single loop.
Avoid — 3 iterations, 2 intermediate arrays:
typescript
const result = users
  .filter(u => u.active)
  .map(u => u.name)
  .reduce((acc, name) => acc + name + ', ', '')
Prefer — single pass:
typescript
let result = ''
for (const u of users) {
  if (u.active) {
    result += u.name + ', '
  }
}
For small arrays (< 100 items), the chained version is fine and more readable. Optimize only when profiling shows it matters.

影响:中等——单次遍历替代多次遍历。
链式调用
.filter().map().reduce()
会创建中间数组并多次遍历数据。对于热路径中的大型数组,建议合并为单次循环。
不推荐——3次遍历,2个中间数组:
typescript
const result = users
  .filter(u => u.active)
  .map(u => u.name)
  .reduce((acc, name) => acc + name + ', ', '')
推荐——单次遍历:
typescript
let result = ''
for (const u of users) {
  if (u.active) {
    result += u.name + ', '
  }
}
对于小型数组(少于100条),链式版本更具可读性且完全可行。仅当性能分析显示存在问题时再进行优化。

6. Short-Circuit with Length Checks First

6. 优先检查长度以短路执行

Impact: LOW-MEDIUM — Avoids expensive operations on empty inputs.
Before running expensive comparisons or transformations, check if the input is empty.
typescript
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)
  )
}

影响:低到中等——避免对空输入执行昂贵操作。
在执行昂贵的比较或转换之前,先检查输入是否为空。
typescript
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. Return Early to Skip Unnecessary Work

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

Impact: LOW-MEDIUM — Reduces average-case execution.
Structure functions to exit as soon as possible for common non-matching cases.
Avoid — always does full work:
typescript
function processEvent(event: AppEvent) {
  let result = null
  if (event.type === 'click') {
    if (event.target && event.target.matches('.actionable')) {
      result = handleAction(event)
    }
  }
  return result
}
Prefer — exits early:
typescript
function processEvent(event: AppEvent) {
  if (event.type !== 'click') return null
  if (!event.target?.matches('.actionable')) return null
  return handleAction(event)
}

影响:低到中等——减少平均执行时间。
设计函数结构,使其在遇到常见不匹配情况时尽早退出。
不推荐——始终执行完整逻辑:
typescript
function processEvent(event: AppEvent) {
  let result = null
  if (event.type === 'click') {
    if (event.target && event.target.matches('.actionable')) {
      result = handleAction(event)
    }
  }
  return result
}
推荐——提前退出:
typescript
function processEvent(event: AppEvent) {
  if (event.type !== 'click') return null
  if (!event.target?.matches('.actionable')) return null
  return handleAction(event)
}

8. Hoist RegExp and Constant Creation Outside Loops

8. 将正则表达式和常量创建提升到循环外部

Impact: LOW-MEDIUM — Avoids repeated compilation.
Creating RegExp objects or constant values inside loops or frequently-called functions wastes CPU.
Avoid — compiles regex 10,000 times:
typescript
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)
  })
}
Prefer — compile once:
typescript
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))
}

影响:低到中等——避免重复编译。
在循环或频繁调用的函数内部创建正则表达式对象或常量会浪费CPU资源。
不推荐——编译正则表达式10000次:
typescript
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)
  })
}
推荐——仅编译一次:
typescript
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))
}

9. Use
toSorted()
,
toReversed()
,
toSpliced()
for Immutability

9. 使用
toSorted()
toReversed()
toSpliced()
实现不可变性

Impact: LOW — Correct immutability without manual copying.
The new non-mutating array methods avoid the
[...arr].sort()
pattern and communicate intent more clearly.
Avoid — manual copy then mutate:
typescript
const sorted = [...items].sort((a, b) => a.price - b.price)
const reversed = [...items].reverse()
const without = [...items]; without.splice(index, 1)
Prefer — non-mutating methods:
typescript
const sorted = items.toSorted((a, b) => a.price - b.price)
const reversed = items.toReversed()
const without = items.toSpliced(index, 1)
These are available in all modern browsers and Node.js 20+.

影响:低——无需手动复制即可实现正确的不可变性。
新的非变异数组方法避免了
[...arr].sort()
这类手动复制模式,且意图表达更清晰。
不推荐——手动复制后变异:
typescript
const sorted = [...items].sort((a, b) => a.price - b.price)
const reversed = [...items].reverse()
const without = [...items]; without.splice(index, 1)
推荐——非变异方法:
typescript
const sorted = items.toSorted((a, b) => a.price - b.price)
const reversed = items.toReversed()
const without = items.toSpliced(index, 1)
这些方法在所有现代浏览器和Node.js 20+版本中可用。

10. Use
requestAnimationFrame
for Visual Updates

10. 使用
requestAnimationFrame
处理视觉更新

Impact: MEDIUM — Syncs with the browser's render cycle.
DOM updates triggered outside the rendering cycle (from timers, event handlers, etc.) can cause jank. Batch visual updates inside
requestAnimationFrame
.
Avoid — updates outside render cycle:
typescript
window.addEventListener('scroll', () => {
  progressBar.style.width = `${getScrollPercent()}%`
  counter.textContent = `${getScrollPercent()}%`
}, { passive: true })
Prefer — synced to render:
typescript
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 })

影响:中等——与浏览器渲染周期同步。
在渲染周期外触发的DOM更新(如定时器、事件处理器等)可能导致卡顿。将视觉更新批量放入
requestAnimationFrame
中执行。
不推荐——渲染周期外更新:
typescript
window.addEventListener('scroll', () => {
  progressBar.style.width = `${getScrollPercent()}%`
  counter.textContent = `${getScrollPercent()}%`
}, { passive: true })
推荐——与渲染周期同步:
typescript
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. Use
structuredClone
for Deep Copies

11. 使用
structuredClone
实现深拷贝

Impact: LOW — Correct deep cloning without libraries.
structuredClone()
handles circular references, typed arrays, Dates, RegExps, Maps, and Sets — unlike
JSON.parse(JSON.stringify())
.
typescript
// Avoid — loses Dates, Maps, Sets, undefined values
const copy = JSON.parse(JSON.stringify(original))

// Prefer — handles all standard types
const copy = structuredClone(original)
Note:
structuredClone
cannot clone functions or DOM nodes. For those cases, implement a custom clone.

影响:低——无需库即可实现正确的深拷贝。
structuredClone()
可以处理循环引用、类型化数组、Date、RegExp、Map和Set——这是
JSON.parse(JSON.stringify())
无法做到的。
typescript
// 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. Prefer
Map
Over Plain Objects for Dynamic Keys

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

Impact: LOW-MEDIUM — Better performance for frequent additions/deletions.
V8 optimizes plain objects for static shapes. When keys are added and removed dynamically (caches, counters, registries),
Map
provides consistently better performance.
typescript
// 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)
})

影响:低到中等——频繁增删操作时性能更优。
V8引擎针对静态结构的普通对象进行了优化。当需要动态添加和删除键(如缓存、计数器、注册表)时,
Map
的性能表现始终更稳定。
typescript
// 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)
})

Source

来源

Patterns from patterns.dev — JavaScript performance guidance for the broader web engineering community.
这些模式来自patterns.dev——面向广大Web工程师的JavaScript性能指导资源。