你有没有遇到过这种场景:
- 首屏 3 秒起步,用户一滑就掉帧
- 列表一长就发烫,点个按钮都要“思考人生”
- QA 一句“你这页面怎么这么卡”,直接把你送进性能优化地狱
我后来发现一个很反直觉的真相:性能优化不是靠“少写点代码”,而是靠“把重活交给浏览器做”。
现代浏览器给了我们一堆“外挂级 API”,用对地方,提升肉眼可见。
下面这 9 个 API,就是我最常用的一套“性能急救包”。每个都给你通俗解释 + 直接可用的例子。内容参考了原文的核心结构与示例。
01|IntersectionObserver:懒加载别再 scroll 监听了(真的会卡)
人话解释:
以前你监听 scroll,每滚一下就算一堆位置,主线程很容易被打爆。
IntersectionObserver 直接让浏览器告诉你: “这个元素进视口了没?” —— 省心还省性能。
最常用:图片懒加载
// 观察器:当元素进入视口就触发
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src; // 真正开始加载
observer.unobserve(img); // 用完就取消观察,避免重复触发
}
});
});
// 让所有 img[data-src] 都被观察
document.querySelectorAll("img[data-src]").forEach((img) => {
observer.observe(img);
});
小技巧:
- 别忘了
unobserve,不然一直观察也会有开销。
02|requestIdleCallback:把“非关键任务”放到浏览器空闲时做
人话解释:
埋点上报、预加载、缓存清理这些事很重要,但不急。
requestIdleCallback 的意思就是: “你先让用户操作顺滑,空了我再做这些杂活。”
例子:空闲时上报 + 预加载
requestIdleCallback(() => {
sendAnalytics(); // 埋点上报
preloadNextPage(); // 预加载下一页资源/数据
});
注意:
- 它是“尽量空闲就做”,不是“保证立刻执行”,关键逻辑别放这里。
03|requestAnimationFrame:动画就该跟着屏幕刷新走
人话解释:
setTimeout 做动画很容易掉帧、抖动。
requestAnimationFrame(简称 rAF)会在浏览器下一帧渲染前回调,天然顺滑。页面切后台还会自动降速/暂停。
例子:从 0 移动到 200
let x = 0;
function animate() {
x += 2;
element.style.transform = `translateX(${x}px)`;
if (x < 200) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
04|ResizeObserver:监听“某个元素”尺寸变化(不是只能监听窗口)
人话解释:
你想监听一个容器变宽变窄(比如图表容器、拖拽面板),用 window.resize 不靠谱。
ResizeObserver 可以监听任意元素的尺寸变化。
例子:容器变化就重绘图表
const ro = new ResizeObserver((entries) => {
entries.forEach((entry) => {
const { width, height } = entry.contentRect;
console.log("新尺寸:", width, height);
// chart.resize(width, height); // 你的图表重绘逻辑
});
});
ro.observe(document.getElementById("chart-container"));
05|performance.now():别用 Date.now 测性能了(不够准)
人话解释:
性能优化最怕“我感觉快了”。
performance.now() 是高精度计时(更准),适合测函数耗时。
例子:测一段重逻辑执行时间
const start = performance.now();
heavyCalculation(); // 你的耗时逻辑
const end = performance.now();
console.log(`耗时:${end - start}ms`);
06|preload & prefetch:资源加载顺序,决定首屏生死
人话解释:
很多页面慢不是因为 JS 慢,而是关键资源没优先加载。
preload:这个资源很关键,现在就优先拉prefetch:这个资源以后可能用,有空再偷偷下
例子:preload 首屏关键 CSS / 字体
<link rel="preload" href="/styles/critical.css" as="style">
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
例子:prefetch 下一页可能用到的 JS
<link rel="prefetch" href="/chunks/profile-page.js">
07|Cache API + Service Worker:二次访问快到离谱,甚至离线可用
人话解释:
第一次访问走网络,第二次直接走缓存,体验会“突然变快”。
Service Worker 可以拦截请求,Cache API 负责存/取缓存。
例子:优先缓存命中,否则走网络(最常见)
// service-worker.js
self.addEventListener("fetch", (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
return cached || fetch(event.request);
})
);
});
补一句重点:
- 真上线要做版本管理,否则会出现“怎么还在用旧资源”。
08|Web Worker:主线程别硬扛大计算(卡死就靠它救)
人话解释:
页面卡死,最常见原因就是:主线程在算大数据/做重逻辑。
Worker 可以把计算放到后台线程,主线程继续流畅响应点击/滚动。
例子:主线程发数据 → Worker 处理 → 回传结果
main.js(主线程)
const worker = new Worker("worker.js");
worker.postMessage(hugeData);
worker.onmessage = (e) => {
console.log("处理完成:", e.data);
};
worker.js(后台线程)
self.onmessage = (e) => {
const result = heavyProcess(e.data); // 大计算
self.postMessage(result);
};
09|document.visibilityState:页面切后台,就别浪费资源了
人话解释:
用户切到别的标签页,你的页面还在轮询接口、跑动画、刷新数据——这就是“暗地里烧钱”。
visibilityState 能告诉你页面是否在前台:后台就停,回来再恢复。
例子:后台暂停轮询/视频,前台恢复
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
stopPolling(); // 停止轮询
stopVideo(); // 暂停视频/动画
} else {
resumePolling(); // 恢复轮询
resumeVideo(); // 恢复视频/动画
}
});
最后:照这个顺序上手,最快见效
如果你想“今天改一点,今天就变快”,推荐顺序:
- IntersectionObserver(懒加载先做对)
- preload / prefetch(首屏关键资源优先)
- visibilityState(后台节流,省电省钱)
- requestAnimationFrame(动画丝滑)
- Web Worker(大计算别卡主线程)
- Service Worker + Cache(二次访问起飞)