运行时优化
1. 防抖与节流
处理高频触发事件,避免函数被无意义地重复调用。
防抖(Debounce)— 最后一次触发后才执行
连续触发:|--触发--触发--触发--触发--| 等待N毫秒 |-- 执行一次 --|
function debounce(fn, delay) {
let timer = null
return function (...args) {
clearTimeout(timer)
timer = setTimeout(() => fn.apply(this, args), delay)
}
}
// 搜索框输入:停止输入 500ms 后才发请求
input.addEventListener('input', debounce((e) => search(e.target.value), 500))
适用:搜索框输入、窗口 resize 结束后重新计算
节流(Throttle)— 固定间隔内只执行一次
连续触发:|--触发触发触发触发触发--|
执行: |-- 执行 --|-- 执行 --|-- 执行 --|(每N毫秒最多一次)
function throttle(fn, interval) {
let lastTime = 0
return function (...args) {
const now = Date.now()
if (now - lastTime >= interval) {
lastTime = now
fn.apply(this, args)
}
}
}
// 滚动事件:每 200ms 最多执行一次
window.addEventListener('scroll', throttle(updateScrollPosition, 200))
适用:滚动监听、鼠标移动、按钮防连点
2. 事件委托
将子元素的事件监听器挂在父元素上,利用事件冒泡统一处理。
// 差:为每个 li 绑定事件(1000 个 li = 1000 个监听器)
document.querySelectorAll('li').forEach(li => {
li.addEventListener('click', handleClick)
})
// 好:只绑定一个监听器,通过 target 判断
document.querySelector('ul').addEventListener('click', (e) => {
const li = e.target.closest('li')
if (li) handleClick(li)
})
优点:减少内存占用,动态新增的子元素也能响应事件,无需重新绑定。
3. 分割大任务
JS 长时间占用主线程会阻塞渲染(超过 50ms 就能感知到卡顿)。 将大任务拆分为小块,分散到多帧中执行。
requestAnimationFrame — 分批渲染 DOM
// 一次性插入 10000 个节点 → 卡死
// 改为每帧插入 20 个,分 500 帧完成
function insertInBatches(total, batchSize = 20) {
function loop(remaining) {
if (remaining <= 0) return
requestAnimationFrame(() => {
const fragment = document.createDocumentFragment()
const count = Math.min(remaining, batchSize)
for (let i = 0; i < count; i++) {
const div = document.createElement('div')
div.textContent = total - remaining + i
fragment.appendChild(div)
}
container.appendChild(fragment)
loop(remaining - count)
})
}
loop(total)
}
requestIdleCallback — 空闲时执行非关键任务
// 数据上报、日志收集等非紧急任务,等浏览器空闲再做
function processNonCriticalTasks(tasks) {
requestIdleCallback((deadline) => {
// deadline.timeRemaining() 返回当前帧剩余的空闲时间
while (deadline.timeRemaining() > 0 && tasks.length > 0) {
doTask(tasks.shift())
}
// 没处理完,等下次空闲继续
if (tasks.length > 0) processNonCriticalTasks(tasks)
})
}
| API | 时机 | 适用场景 |
|---|---|---|
requestAnimationFrame | 每帧渲染前 | 分批 DOM 操作、动画 |
requestIdleCallback | 浏览器空闲时 | 数据上报、预加载、非关键计算 |
4. Web Worker — 并行计算
JS 是单线程的,耗时的纯计算任务(加密、大数据处理)会阻塞主线程。 Web Worker 在独立线程中运行,不阻塞 UI。
// worker.js
self.addEventListener('message', (e) => {
const result = heavyComputation(e.data)
self.postMessage(result)
})
// 主线程
const worker = new Worker('./worker.js')
worker.postMessage(largeData)
worker.addEventListener('message', (e) => {
console.log('计算结果:', e.data)
})
限制:Worker 无法访问 DOM,只能处理纯数据计算。
5. 减少回流与重绘
避免强制同步布局(Layout Thrashing)
读取布局属性(offsetWidth、scrollTop 等)会强制浏览器先刷新样式计算。 在循环中交替读写会触发大量回流:
// 差:读写交替,每次循环都触发一次回流
elements.forEach(el => {
el.style.width = el.offsetWidth + 10 + 'px' // 读 → 强制回流 → 写
})
// 好:先批量读,再批量写
const widths = elements.map(el => el.offsetWidth) // 批量读(一次回流)
elements.forEach((el, i) => {
el.style.width = widths[i] + 10 + 'px' // 批量写
})
合成层(Composite Layer)
将频繁动画的元素提升到独立图层,由 GPU 合成,跳过 Layout 和 Paint:
/* 提示浏览器提前准备合成层 */
.animate-element {
will-change: transform, opacity;
transform: translateZ(0); /* 触发硬件加速 */
}
/* 动画优先用 transform/opacity,不触发回流 */
.slide-in {
transition: transform .3s ease, opacity .3s ease;
}
触发合成层的条件:
transform/opacity/filter不为默认值will-change指定了任意属性video、canvas、iframe元素
⚠️ 合成层并非越多越好——每个合成层都消耗 GPU 显存,过多反而导致"层爆炸"。 只对真正需要动画的元素使用
will-change。
批量 DOM 操作
// 差:多次操作触发多次回流
el.style.width = '100px'
el.style.height = '200px'
el.style.margin = '10px'
// 好1:一次性修改 class
el.classList.add('expanded')
// 好2:离线操作 DocumentFragment
const fragment = document.createDocumentFragment()
for (let i = 0; i < 100; i++) {
fragment.appendChild(createItem(i))
}
container.appendChild(fragment) // 只触发一次回流
优先级总结
高频触发事件 → 防抖 / 节流
大量子元素事件 → 事件委托
长时间 JS 任务 → requestAnimationFrame 分批 / Web Worker 并行
非关键任务 → requestIdleCallback 空闲执行
频繁动画 → transform/opacity + will-change 合成层
批量 DOM 操作 → DocumentFragment / classList 批量修改