一、先别急着优化:用工具找瓶颈
不要靠感觉优化,先测,再改。
1.1 使用浏览器性能工具
以 Chrome 为例:
-
F12打开 DevTools -
Performance 面板 → 点击录制 → 操作页面 → 停止录制
你可以看到:-
哪些函数耗时最长
-
哪些时间在布局 / 重绘(Layout / Paint)
-
哪些是长任务(Long Task > 50ms)
-
1.2 常用几个指标
-
First Contentful Paint (FCP):首屏内容出现时间
-
Largest Contentful Paint (LCP):最大内容块出现时间
-
Total Blocking Time (TBT):主线程被 JS 阻塞的时间
-
Time To Interactive (TTI):页面可交互时间
优化时可以围绕它们做文章:
减少 JS 体积、打碎长任务、延迟不急用的逻辑。
二、减少 JavaScript 体积和执行时间
2.1 按需加载(Code Splitting)
不要一上来就加载全站 JS,可以按页面或功能拆包:
// 只有点击按钮时才加载某模块
button.addEventListener('click', async () => {
const module = await import('./big-module.js')
module.run()
})
好处:
-
首屏 JS 体积减少
-
加载和解析时间更短
配合 Webpack / Vite / Rollup 等打包工具,可以做到自动按路由拆包。
2.2 减少不必要的 polyfill 和第三方库
-
只支持现代浏览器时,没必要加载大量 ES5 polyfill;
-
有些场景完全可以用原生 API 替代:
// ❌ 引入 lodash 只为了一个 debounce import debounce from 'lodash/debounce'
// ✅ 自己实现一个简单的即可 function debounce(fn, delay) { let timer = null return function (...args) { clearTimeout(timer) timer = setTimeout(() => fn.apply(this, args), delay) } }
经验法则:能用原生就用原生,实在麻烦再上库,尽量避免为一个小功能引入几十 KB 的依赖。
2.3 减少重复计算和频繁创建对象
-
如果某个值可以提前算好,就别在循环里一遍遍算:
// ❌ n 每次循环都从数组长度读取 for (let i = 0; i < arr.length; i++) { // ... }
// ✅ 缓存长度 for (let i = 0, len = arr.length; i < len; i++) { // ... }
-
频繁创建临时对象、闭包也会导致 GC 压力升高。
尽量复用结构或缓冲区,不要在热路径上疯狂 new。
三、优化 DOM 操作:减少重排和重绘
JavaScript 本身很快,真正在拖后腿的往往是 DOM 操作。
3.1 批量修改 DOM,而不是一条一条改
// ❌ 每次循环都插入 DOM,触发多次重排
const ul = document.querySelector('ul')
items.forEach(text => {
const li = document.createElement('li')
li.textContent = text
ul.appendChild(li)
})
// ✅ 使用 DocumentFragment 一次性插入
const ul = document.querySelector('ul')
const frag = document.createDocumentFragment()
items.forEach(text => {
const li = document.createElement('li')
li.textContent = text
frag.appendChild(li)
})
ul.appendChild(frag)
3.2 避免读写 DOM 交替
// ❌ 读写交替,会导致浏览器频繁计算布局
div.style.width = (div.offsetWidth + 10) + 'px'
div.style.height = (div.offsetHeight + 10) + 'px'
// ✅ 先读再写
const w = div.offsetWidth
const h = div.offsetHeight
div.style.width = (w + 10) + 'px'
div.style.height = (h + 10) + 'px'
3.3 使用 class 切换代替逐条改样式
// ❌ 多次写 style
el.style.color = 'red'
el.style.fontSize = '14px'
el.style.fontWeight = 'bold'
// ✅ 一次改 class,让 CSS 做剩下的事情
el.classList.add('error')
四、事件与滚动相关优化:防抖与节流
4.1 防抖(debounce)
适合「只在停下来之后触发一次」的场景,比如搜索输入框联想。
function debounce(fn, delay) {
let timer = null
return function (...args) {
clearTimeout(timer)
timer = setTimeout(() => fn.apply(this, args), delay)
}
}
window.addEventListener('resize', debounce(() => {
console.log('窗口改变后 300ms 再执行')
}, 300))
4.2 节流(throttle)
适合持续触发但要限制频率的场景,比如滚动监听:
function throttle(fn, delay) {
let last = 0
return function (...args) {
const now = Date.now()
if (now - last >= delay) {
last = now
fn.apply(this, args)
}
}
}
window.addEventListener('scroll', throttle(() => {
console.log('每 100ms 最多执行一次')
}, 100))
五、避免长任务:让页面“不卡顿”
浏览器主线程如果被一个 JS 任务占用时间太久(> 50ms),就会出现:
-
点击没反应
-
滚动卡住
-
输入延迟
5.1 拆分大循环
// ❌ 一次处理 10 万条数据,卡死 UI
bigArray.forEach(item => heavyCompute(item))
// ✅ 分片处理
function chunkProcess(list, handler, chunkSize = 1000) {
let index = 0
function next() {
const end = Math.min(index + chunkSize, list.length)
for (; index < end; index++) {
handler(list[index])
}
if (index < list.length) {
requestIdleCallback(next) // 或 setTimeout(next, 0)
}
}
next()
}
chunkProcess(bigArray, heavyCompute)
5.2 利用 requestAnimationFrame 做动画
以前我们习惯用 setInterval / setTimeout 做动画,但这会和浏览器的刷新节奏不匹配。
function animate() {
// 更新位置
box.style.transform = `translateX(${x}px)`
x += 2
if (x < 300) {
requestAnimationFrame(animate)
}
}
requestAnimationFrame(animate)
requestAnimationFrame 会在浏览器下一帧绘制前调用,动画更流畅也更省电。
5.3 使用 Web Worker 处理计算密集任务
如果你有复杂的计算(如大数据排序、压缩、加密),可以扔到 Worker:
// main.js
const worker = new Worker('worker.js')
worker.postMessage(bigData)
worker.onmessage = e => {
console.log('计算结果:', e.data)
}
// worker.js
onmessage = e => {
const result = heavyCompute(e.data)
postMessage(result)
}
这样主线程仍然可以保持流畅的交互。
六、内存优化和避免泄漏
内存泄漏会慢慢拖垮页面,严重时浏览器直接崩溃。
6.1 常见泄漏来源
-
全局变量 / 挂在
window上的引用 -
定时器
setInterval没有清理 -
事件监听未移除
-
闭包中意外持有大对象引用
6.2 解决方案示例
// ❌ setInterval 不清理
setInterval(() => {
console.log('do something')
}, 1000)
// ✅ 组件销毁或页面离开时清理
const timer = setInterval(...)
window.addEventListener('beforeunload', () => {
clearInterval(timer)
})
// ❌ 事件监听一直存在
button.addEventListener('click', handleClick)
// ✅ 不需要时移除
button.removeEventListener('click', handleClick)
Chrome DevTools 的 Memory 面板可以帮助你查看堆快照,查找无法被 GC 回收的对象。
七、网络与资源相关优化(与 JS 强相关)
即使 JS 写得很好,如果资源加载慢,首屏一样会很慢。
7.1 按需加载脚本
-
非首屏必须的脚本使用
defer或async: -
defer: 等 HTML 解析完按顺序执行,适合业务 JS -
async: 下载完就执行,顺序不保证,适合统计、广告脚本
7.2 懒加载组件 / 路由
前端框架中(React/Vue),对大组件路由做懒加载:
// Vue Router
const PageA = () => import('./pages/PageA.vue')
const routes = [
{ path: '/a', component: PageA }
]
八、数据结构与算法层面的优化
有时候瓶颈不是浏览器,而是你写的算法。
8.1 合理选择数据结构
-
需要频繁查找:用
Map或对象字典,而不是Array.find; -
需要频繁去重:用
Set; -
动态增删:避免在数组头部插入删除(
unshift/shift会移动大量元素)。// 查找是否存在 // ❌ O(n) const exists = arr.includes(id)
// ✅ O(1) —— 先构造 Set const set = new Set(arr) const existsFast = set.has(id)
8.2 减少深拷贝
过度使用 JSON.parse(JSON.stringify(obj)) 或各种 deepClone 实现,会很伤性能。
尽量设计数据结构时避免层级过深,必要时只做「局部拷贝」。
九、简单的性能优化 checklist
开发完一个功能,可以按照下面的清单检查一下:
-
首屏是否只加载了必需的 JS?
-
大模块是否按需加载 / 拆包了?
-
高频事件是否加了防抖 / 节流?
-
DOM 操作是否做了批量处理?是否避免了读写交替?
-
是否存在长任务(>50ms)?能否拆分或丢给 Worker?
-
定时器、事件监听是否会在不需要时清理?
-
是否存在明显的重复计算或深拷贝?
-
是否在 DevTools 里看过性能录制和内存快照?
十、结语
JavaScript 性能优化并不是某个“绝招”,而是一套思路:
测瓶颈 → 找最慢的点 → 针对性优化 → 再测