JS 代码技巧 vol.6 --- 20 个性能优化野路子,从渲染到网络全栈提速
小不的代码技巧系列第 6 期
主题:性能优化野路子
这期篇幅比前几期长------性能优化这事,"知道"和"用上"之间隔着十万八千里 😎
一次盘 20 个,从渲染、JS 执行、网络、加载、内存五个维度下手。
哈喽哇!我是小不 ,不简说的不~
作为一个在代码界"翻车"无数次的选手,我算是看明白了:坑这东西吧,要么不踩,踩就踩大的😂
今天这期,主打一个 性能优化野路子------不整虚的,全是实战里哭出来的教训。看不看随你~反正翻车实录又不收钱
Ps:文末有惊喜(不是广告!)
一、渲染层(4 个)
1. 减少重排重绘:读写分离
DOM 读和写交替会反复触发 layout。
js
// ❌ 反例:读 1 次、写 1 次、再读 1 次、再写 1 次
// 浏览器被迫 layout 两次
divs.forEach((div) => {
const h = div.offsetHeight; // 读
div.style.height = h + 10 + "px"; // 写 → 触发 layout
});
// ✅ 正解:先全部读完,再全部写
const heights = divs.map((div) => div.offsetHeight);
divs.forEach((div, i) => {
div.style.height = heights[i] + 10 + "px";
});
坑在哪 :DOM 读操作会刷新 layout 缓存(getBoundingClientRect、offsetTop、offsetHeight 等都是)。写操作会异步触发 layout。混着来就来回 layout。
2. transform / opacity 走 composite 层
CSS 属性分三类性能等级:
| 等级 | 触发 | 属性 |
|---|---|---|
| 高 | 只 composite | transform / opacity |
| 中 | paint | color / background |
| 低 | layout | width / height / top / left |
动画用 transform: translateX() 而不是 left: 100px,FPS 直接拉满。
坑在哪 :will-change: transform 可以提示浏览器提前创建合成层,但别滥用------每个合成层都吃 GPU 内存。
3. 虚拟列表:长列表性能救星
10000 条数据直接渲染?浏览器当场给你白屏。
js
// 思路:只渲染可视区域 + 上下 buffer
// 库:vue-virtual-scroller / react-virtual / @tanstack/virtual
// 简化版:只渲染视口内的 row
const start = Math.floor(scrollTop / itemHeight);
const end = start + Math.ceil(viewportHeight / itemHeight);
const visible = list.slice(start, end);
坑在哪:表格、固定列、动态行高会复杂化虚拟列表的实现。能用库就别手写。
4. 图片懒加载:IntersectionObserver
js
// ❌ 反例:scroll 监听 + getBoundingClientRect
window.addEventListener("scroll", () => {
imgs.forEach((img) => {
const rect = img.getBoundingClientRect();
if (rect.top < window.innerHeight) img.src = img.dataset.src;
});
});
// ✅ 正解:IntersectionObserver 一行
const io = new IntersectionObserver((entries) => {
entries.forEach((e) => {
if (e.isIntersecting) {
e.target.src = e.target.dataset.src;
io.unobserve(e.target);
}
});
});
imgs.forEach((img) => io.observe(img));
坑在哪:IntersectionObserver 在 iframe 里有时不同步;首屏图片别懒加载,会拖慢 LCP。
二、JS 执行层(4 个)
5. Web Worker 跑重计算
主线程跑 100ms 的计算 = 页面卡 100ms。
js
// 主线程
const worker = new Worker("./heavy.js");
worker.postMessage(data);
worker.onmessage = (e) => render(e.data);
// heavy.js
self.onmessage = (e) => {
const result = heavyCompute(e.data); // 复杂计算
self.postMessage(result);
};
坑在哪 :Worker 里没有 DOM、没有 window、没有 document 。没有 DOM、没有 window、没有 document! ⚠️ 共享数据用 postMessage(结构化克隆)或 SharedArrayBuffer。
6. 避免内存泄漏:定时器/闭包/全局
经典三件套:
js
// 1. 定时器忘 clear
const timer = setInterval(() => doSomething(), 1000);
// 组件销毁时忘了 clearInterval(timer)
// 2. 闭包挂大对象
function bindClick() {
const bigData = new Array(1000000);
document.getElementById("btn").onclick = () => console.log(bigData.length);
// btn 元素被移除前,bigData 永远不释放
}
// 3. 全局变量
window.cache = fetchHugeData();
// 页面关闭前永不释放
坑在哪 :Chrome DevTools 的 Memory 标签可以录 heap snapshot,生产前必做一次泄漏检测。
7. 事件委托:减少监听器数量
js
// ❌ 反例:1000 个 li 绑 1000 个监听器
document.querySelectorAll("li").forEach((li) => {
li.addEventListener("click", handleClick);
});
// ✅ 正解:委托到父元素,一个监听器搞定
ul.addEventListener("click", (e) => {
if (e.target.matches("li")) handleClick(e);
});
坑在哪 :委托的事件要冒泡到父元素,stopPropagation() 之后的子元素事件就丢了。
8. Map vs Object:大数据量下 Map 更优
js
// 100 万条数据
const map = new Map();
const obj = {};
// Map: 插入 100ms, 查找 50ms
// Object: 插入 150ms, 查找 80ms, 还可能被原型链污染
map.has(key);
map.get(key);
map.set(key, val);
坑在哪 :Object 在 V8 里有"hidden class"优化,少量 key(< 30)时 Object 反而更快。大量动态 key 选 Map。
三、网络层(4 个)
9. 强缓存 vs 协商缓存
js
// 强缓存:浏览器不请求服务器,直接读本地
Cache-Control: max-age=31536000 // 一年不请求
// 协商缓存:浏览器问服务器"我有这个,过没过期?"
ETag: "abc123"
If-None-Match: "abc123" // 服务器返回 304 表示还能用
坑在哪:HTML 千万别用强缓存(改了用户看不到);静态资源(JS/CSS 带 hash)放心用强缓存。
10. HTTP/2 vs HTTP/1.1
HTTP/1.1 一个域名同时只能 6 个请求,多了排队。
HTTP/2 多路复用:所有请求在一个连接上并行,没有队头阻塞。
坑在哪:HTTP/2 不解决所有问题------首字节延迟靠 preload / 103 Early Hints,连接迁移靠 HTTP/3 (QUIC)。
11. Preload / Prefetch 关键资源
html
<!-- preload:当前页关键资源,高优先级 -->
<link rel="preload" href="/main.css" as="style" />
<!-- prefetch:下一页可能用到的资源,低优先级 -->
<link rel="prefetch" href="/next-page.js" />
<!-- dns-prefetch:提前解析域名 -->
<link rel="dns-prefetch" href="//cdn.example.com" />
坑在哪 :preload 用多了反而抢首屏带宽,只 preload 首屏真正阻塞渲染的资源。
12. WebP / AVIF 替代 JPEG/PNG
| 格式 | 体积(vs JPEG) | 兼容性(2026) |
|---|---|---|
| JPEG | 100% | 100% |
| WebP | -30% | 98% |
| AVIF | -50% | 95% |
html
<picture>
<source srcset="img.avif" type="image/avif" />
<source srcset="img.webp" type="image/webp" />
<img src="img.jpg" alt="" />
</picture>
坑在哪 :动画 GIF 改用 WebP/AVIF 动画能省 80%+,老 IE 只能 JPEG。对老用户做兼容,不是对技术妥协。
四、加载层(4 个)
13. 代码分割:动态 import()
js
// 路由懒加载(webpack/vite 默认支持)
const Home = () => import("./Home.vue");
const About = () => import("./About.vue");
// 组件懒加载:弹窗、图表等"按需打开"的
const ChartModal = () => import("./ChartModal.vue");
// 点击才加载
button.onclick = async () => {
const { showModal } = await import("./modal.js");
showModal();
};
坑在哪 :拆分太细会增加 HTTP 请求数,反而拖慢首屏。按路由/按业务模块拆,别按函数拆。
14. 路由懒加载:SPA 必备
js
// Vue Router
const routes = [
{ path: "/", component: () => import("./views/Home.vue") },
{ path: "/about", component: () => import("./views/About.vue") },
];
// React Router 6
const Home = lazy(() => import("./pages/Home"));
坑在哪 :用户进入路由时才加载该 chunk,有几百 ms 等待------加骨架屏或 loading 提升感知性能。
15. 首屏 CSS 内联 + 骨架屏
html
<!-- 关键 CSS 直接内联,省一次请求 -->
<head>
<style>
/* 关键 CSS */
</style>
</head>
骨架屏:内容加载前显示占位 UI,让用户感觉"快了"(感知性能 vs 实际性能)。
坑在哪:内联 CSS 不能超过 14KB(TCP 慢启动阈值),多了反而拖累。
16. DNS 预解析 + 预连接
html
<link rel="dns-prefetch" href="//cdn.example.com" />
<link rel="preconnect" href="//cdn.example.com" crossorigin />
dns-prefetch:只解析域名(省 DNS 时间)preconnect:建 TCP + TLS(省握手时间)
坑在哪:preconnect 数量有上限(6 个),用多了反而拖累。
五、内存层(4 个)
17. 大对象拷贝:structuredClone 替代 JSON
js
// ❌ 反例:JSON.parse(JSON.stringify(obj))
// 问题:丢失函数、Date、undefined、循环引用会爆栈
// ✅ 正解:structuredClone(Node 17+ / 现代浏览器)
const copy = structuredClone(obj);
坑在哪 :structuredClone 不能克隆函数、Symbol、原型链。Vue 的 reactive 对象要小心(克隆后丢响应性)。
18. WeakMap / WeakSet:自动 GC
js
// Map: 强引用,key 没了 value 还在内存
const map = new Map();
let obj = { id: 1 };
map.set(obj, "data");
obj = null; // map 里 obj 还在 → 内存泄漏
// WeakMap: 弱引用,key 没了 value 自动释放
const wm = new WeakMap();
let obj2 = { id: 1 };
wm.set(obj2, "data");
obj2 = null; // wm 里的 data 自动释放 🎉
坑在哪 :WeakMap 不能遍历(没有 size/keys/values),只能 get/set/has/delete。需要遍历的场景老老实实用 Map。
19. 对象池:复用对象减少 GC
js
class ObjectPool {
constructor(create, reset) {
this.create = create;
this.reset = reset;
this.pool = [];
}
acquire() {
return this.pool.pop() || this.create();
}
release(obj) {
this.reset(obj);
this.pool.push(obj);
}
}
// 粒子系统场景
const particlePool = new ObjectPool(
() => ({ x: 0, y: 0, vx: 0, vy: 0 }),
(p) => {
p.x = p.y = p.vx = p.vy = 0;
},
);
坑在哪:只对"高频创建销毁"的小对象有效。普通业务对象用对象池是过度设计。
20. React batching:合并 setState
js
// ❌ 反例:连续 setState 触发多次 render
function handle() {
setCount((c) => c + 1);
setName("小不");
setFlag(true);
// React 18 之前会 render 3 次
}
// ✅ React 18+ 自动 batching:上面 1 次 render
升级 React 18+,事件回调、Promise、setTimeout 里的 setState 都会自动合并。
坑在哪 :用 ref 强制更新(flushSync)会破坏 batching,别没事 flush。
📦 收个尾
这 20 个技巧浓缩一下:
- 最高频踩坑:preload 滥用、滚动监听忘加 passive、Web Worker 误用 DOM
- 最值得收藏 :transform 走 composite 层、
structuredClone替代 JSON、虚拟列表 - 核心思路:性能优化是 trade-off,先测后优
学到了就是赚到了,犹豫徘徊等于白来~
写到最后
想要啥技巧?评论区甩个题目过来~
- 你刚踩的坑
- 项目里反复写的代码
- 想搞清楚但一直懒得查的 API
小不看到...不一定回 😂 毕竟代码里翻车太多,腾不出手~
Ps:三连随缘,催更的会被打 😂