JS 代码技巧 vol.6 — 20 个性能增强野路子,从渲染到网络全栈提速 JS 代码技巧 vol.6 — 20 个性能优化野路子,从渲染到网络全栈提速 小不的代码技巧系列第 6 期 主题: 性能优化野路子 这期篇幅比前几期长——性能优化这事,"知道"和"用上"之间隔着十万八千里 😎 一次盘 20 个,从渲染、JS 执行、网络、加载、内存五个维度下手。 哈喽哇!我是 小不 , 不简说 的不~ 作为一个在代码界"翻车"无数次的选手,我算是看明白了: 坑这东西吧,要么不踩,踩就踩大的 😂 今天这期,主打一个 性能优化野路子 ——不整虚的,全是实战里哭出来的教训。看不看随你~反正翻车实录又不收钱 Ps:文末有惊喜(不是广告!) 一、渲染层(4 个) 1. 减少重排重绘:读写分离 DOM 读和写交替会反复触发 layout。 // ❌ 反例:读 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 条资料直接渲染?浏览器当场给你白屏。 // 思路:只渲染可视区域 + 上下 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 // ❌ 反例: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。 // 主线程 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. 避免内存泄漏:定时器/闭包/全局 经典三件套: // 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. 事件委托:减少监听器数量 // ❌ 反例: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 更优 // 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 协商缓存 // 强缓存:浏览器不请求服务器,直接读本地 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 关键资源 < link rel = "preload" href = "/main.css" as = "style" /> < link rel = "prefetch" href = "/next-page.js" /> < 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% < 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() // 路由懒加载(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 必备 // 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 内联 + 骨架屏 < head > < style > /* 关键 CSS */ style > head > 骨架屏:内容加载前显示占位 UI, 让用户感觉"快了" (感知性能 vs 实际性能)。 坑在哪 :内联 CSS 不能超过 14KB(TCP 慢启动阈值),多了反而拖累。 16. DNS 预解析 + 预连接 < 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 // ❌ 反例:JSON.parse(JSON.stringify(obj)) // 问题:丢失函数、Date、undefined、循环引用会爆栈 // ✅ 正解:structuredClone(Node 17+ / 现代浏览器) const copy = structuredClone (obj); 坑在哪 : structuredClone 不能克隆函数、Symbol、原型链。Vue 的 reactive 对象要小心(克隆后丢响应性)。 18. WeakMap / WeakSet:自动 GC // 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 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 // ❌ 反例:连续 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:三连随缘,催更的会被打 😂
JS 代码技巧 vol.6 — 20 个性能增强野路子,从渲染到网络全栈提速
JS 代码技巧 vol.6 — 20 个性能增强野路子,从渲染到网络全栈提速 JS 代码技巧 vol.6 — 20 个性能优化野路子,从渲染到网络全栈提速 小不的代码技巧系列第 6 期 主题: 性能优化野路子 这期篇幅比前几期长——性能优化这事,"知道"和"用上"之间隔着十万八千里 😎 一次盘 20 个,从渲染、JS 执行、网络、加载、内存五个维度下手。 哈喽哇!我是 小不 , 不简说 的不~ 作...

