JS 代码技巧 vol.6 — 20 个性能优化野路子,从渲染到网络全栈提速

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:三连随缘,催更的会被打 😂

相关推荐
小岛前端18 小时前
展示!我 Vibe 了一个 Codex HUD
前端·ai编程
我是大卫18 小时前
【图】React源码解析-从源码数据结构、执行机制对比、闭包陷阱与最佳实践四个维度,深挖useMemo和useCallback的底层原理
前端·react.js·源码
不一样的少年_18 小时前
不用 LangChain,手搓 AI Agent:给大模型装上“手”,让它自己读项目文件
前端·人工智能·agent
索西引擎18 小时前
【React】useState 函数式更新机制:闭包陷阱规避与状态一致性保障分析
前端·react.js·前端框架
SimonKing18 小时前
阿里要求全员卸载 Claude Code:事件始末与深层逻辑
java·后端·程序员
anyup19 小时前
从文本到具身:我给 AI Agent 搭了套实时 3D 交互身体
前端·人工智能·aigc
lichenyang45319 小时前
给弱模型一本说明书:我把文档规范做成单文件 Skill,也终于分清了 Skill 和 MCP
前端
用户2986985301419 小时前
前端实战:在 React 中使用 JavaScript 一键导出 Excel 图表与形状为图片
javascript·react.js·excel
咖啡无伴侣19 小时前
Vue3 DOM 异步更新与性能优化全套学习笔记
前端·vue.js