写这篇文章的起因很简单:前阵子项目上线后,老板说页面打开慢、卡,搞得我天天加班排查性能问题。后来一通优化下来,页面确实快了不少。现在趁有空,把这段经历总结下来,也算给自己留个笔记,给各位踩坑的兄弟们一点参考。
先别谈优化技巧,咱得搞清楚一个事儿:你到底遇到的是什么样的性能问题?
通常来说,前端的性能瓶颈有这么几类:
页面加载慢:比如打开首页要等好几秒。
交互卡顿:点击按钮没反应、拖动不流畅。
内存泄漏:用久了页面越来越卡,甚至崩溃。
资源太大了:JS 文件一个几 MB,图片太高清。
如果你连这些问题都不明确,就瞎优化,那不是在救火,是在添乱。
很多项目都是一个 bundle.js
,动不动几 MB。你不分包,浏览器就要一次性下载全部代码,慢得很自然。
怎么解决?
如果你用的是 Webpack,配合动态引入(import()
)就能实现懒加载:
// 原来的写法
import BigComponent from './BigComponent';
// 改成动态引入
const BigComponent = () => import('./BigComponent');
配合路由切分、组件懒加载,首屏文件能瘦不少。
用户没滚到底下,图片没必要全加载吧?可以用 loading="lazy"
:
或者结合 IntersectionObserver
自己写懒加载逻辑,兼容性更强。
静态资源可以加上强缓存(Cache-Control、ETag),避免每次都重新加载。Webpack 打包时可以加上 [contenthash]
来做文件指纹:
output: {
filename: '[name].[contenthash].js'
}
这样一改动才更新,其他文件都走缓存,省流量也快。
直接操作 DOM 比较重,尤其是循环创建大量元素,建议用批处理或虚拟 DOM。
比如原来是:
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div');
div.innerText = i;
document.body.appendChild(div); // 每次都插入 DOM
}
改成这样会好点:
const frag = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div');
div.innerText = i;
frag.appendChild(div);
}
document.body.appendChild(frag); // 一次插入
样式变化会触发浏览器的重排(Reflow)和重绘(Repaint),能合并就合并。
比如:
// 差的做法
el.style.width = '100px';
el.style.height = '100px';
el.style.color = 'red';
可以合并写成:
el.style.cssText = 'width:100px;height:100px;color:red';
或者直接加一个类名:
el.classList.add('active-style');
有些操作(比如 resize
、scroll
、input
)触发频率太高,CPU 直接拉满。用**节流(throttle)和防抖(debounce)**处理一下。
// 简单防抖
function debounce(fn, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// 简单节流
function throttle(fn, delay) {
let last = 0;
return function (...args) {
const now = Date.now();
if (now - last > delay) {
last = now;
fn.apply(this, args);
}
};
}
如果 JS 主线程跑了个死循环,页面会直接卡死。比如这样写法就很危险:
for (let i = 0; i < 1e9; i++) {
doSomething(i);
}
可以拆成异步块处理:
let i = 0;
function run() {
const chunk = 10000;
const now = performance.now();
while (i < 1e9 && performance.now() - now < 16) {
doSomething(i);
i++;
}
if (i < 1e9) {
requestAnimationFrame(run);
}
}
run();
这样主线程还能喘口气。
如果你真的要处理计算密集型任务(比如图片处理、加密解密),可以用 Web Worker
把任务丢到子线程:
const worker = new Worker('worker.js');
worker.postMessage({ type: 'calc', data: bigData });
worker.onmessage = (e) => {
console.log('计算结果:', e.data);
};
比如定时器、事件监听器,如果页面销毁了但还引用着,就内存泄漏了。
// BAD
window.addEventListener('scroll', handler);
// 页面销毁时忘了清除
改成:
window.addEventListener('scroll', handler);
// 销毁时清除
window.removeEventListener('scroll', handler);
当你需要缓存一些 DOM 或对象,但又不想干扰垃圾回收,可以用 WeakMap
:
const cache = new WeakMap();
function cacheData(el, data) {
cache.set(el, data);
}
// 不手动清理,el 被销毁后会自动清掉
Chrome DevTools:老朋友了,Performance 面板看时间轴,Memory 看堆快照,Network 看资源加载。
Lighthouse:可以做个页面体检,看看哪些点性能差。
Web Vitals:Google 推的几个核心指标(LCP、FID、CLS),和真实用户体验挂钩。
优化 JS 性能,说难也不难,说简单也不简单。核心是:知道用户真正卡在哪、慢在哪,然后按需下手,不要一通胡乱优化。