两个月前,我在排查一个慢到让人想砸键盘的 JavaScript 功能:用户仪表盘动不动就卡死、冻结、掉帧。
我一开始以为是复杂算法或架构问题:
- 复杂算法写炸了?
- API 慢得离谱?
- 15 层嵌套循环地狱?
- React 渲染雪崩?
结果都不是。
真正的元凶,是一行普通到你会下意识跳过的 JavaScript 代码。当我发现它在每次 render 上都稳定制造 600ms 延迟 时,那种感觉很复杂:一半羞耻,一半顿悟。更让我震惊的是,有多少开发者每天都在无意识地犯同一个错。
这篇不是讲玄学,我直接给你看:真实代码、真实修复、以及你忽略这些“微小性能坑”会付出的真实代价。
罪魁祸首:这一行让一切变慢
就是它:
JSON.parse(JSON.stringify(data))
我知道,我知道——“快速深拷贝”的祖传秘方,很多人早期都学过。
它确实:
但它也同样:
- 在稍微复杂一点的数据上慢得要命
- 在关键路径里会变成性能炸弹
这行一旦出现在 render、循环、事件回调里,本质就是在逼 JS:每次都把整个对象序列化成字符串,再把字符串反序列化回对象。 听起来很“干净”,实际上是在用主线程当燃料。
为什么这招“看着方便”,却是速度杀手
很多教程轻描淡写,但真相是:
1)stringify 很贵
JSON.stringify 需要遍历整个对象,把每个值转换成 JSON 能表达的形态。
2)parse 也很贵
JSON.parse 再把整套流程反过来做一遍。
3)深拷贝意味着内存翻倍
对象越大,复制出来的内存越多,卡顿(jank)就越容易出现。
4)它会挡住主线程
这意味着:UI 可能冻结、滚动会抖、交互会迟钝。
然后把它乘一下:
- React 组件反复 re-render
- Node.js 某个高频请求处理
- 一个循环
- 实时监听器
- 高频触发事件
你所谓的“简单深拷贝”,就会稳定燃烧掉几十毫秒、上百毫秒——直到用户开始怀疑产品的性能。
让我震惊的修复:更现代的深拷贝
别再用 JSON.parse(JSON.stringify()) 了。换成现代、原生的深拷贝 API:
structuredClone(data)
它为什么更香?
- 大对象通常更快
- 支持更多类型
- 对主线程压力更小(至少不像 JSON 那样粗暴)
- 边界情况更少
- 浏览器 + Node.js 原生支持
我甚至是改了半天性能日志,才意识到差距有多离谱。
真实基准测试(你自己也能跑)
console.time(“json”);
JSON.parse(JSON.stringify(bigObject));
console.timeEnd(“json”);
console.time(“structured”);
structuredClone(bigObject);
console.timeEnd(“structured”);
用一个“中等偏大”的对象(大约 6 万个属性)跑出来的平均值:
- JSON 克隆:约 55ms
structuredClone:约 8ms
这不是“抠细节”。 这是“性能瓶颈”和“顺滑体验”的分水岭。更尴尬的是,很多教程仍在推荐 JSON 那套,只因为“好理解”。但性能差距是真实存在的,而且随着应用复杂度增加,只会更明显。
什么时候必须避开 structuredClone(是的,它也有坑)
为了不把你忽悠瘸,我把 trade-off 摆出来。
structuredClone 不支持 或不适合的情况(常见坑位):
- 函数(Functions)
- DOM 节点
- 某些类实例(Class instances)(会丢掉原型/方法语义,结果不一定是你想要的)
- 某些环境下的递归对象处理差异(不同运行时细节可能不一致)
反而在一些场景里,JSON 方式可能“有意为之”:
- 你就是想把方法/原型全剥掉,做纯数据清洗
- 你需要数据被“标准化”为 JSON 形态
- 你在处理用户提交对象,需要过滤掉不可序列化的东西
但大多数时候,structuredClone 才是更面向未来的默认选项。而 JSON 深拷贝这招,更像一门应该从现代 JS 代码库里逐步退休的“老把式”。
顺手再爆几个“看起来无害、实际很伤”的一行代码
既然来了,就把常见雷区一起点名。
1)用 map() 做副作用
items.map(doSomething)
要副作用,请用:
items.forEach(doSomething)
map 的语义是“映射成新数组”,你不用返回值却硬用 map,不仅误导阅读,还可能错过一些优化机会。
2)在循环里 await
for (const i of arr) {
await doSomething(i);
}
这会强制串行执行,能慢到你怀疑人生。能并行就并行:
await Promise.all(arr.map(doSomething));
(当然,前提是你的任务允许并发,而且不会把后端打爆。)
3)在 render 里反复算“贵东西”
这在 React 里尤其常见,尤其致命。任何“每次 render 都重新算”的重计算,都值得被 memo、缓存、或移出 render。
4)日志打太多
一些浏览器环境下,console.log 真的会拖主线程。尤其在高频渲染/滚动/动画里,日志就是隐藏刹车。
一个迷你实用指南:如何快速定位“慢的那一行”
1)用 performance.now() 把可疑代码包起来,测毫秒级差异。
2)用 Chrome DevTools 的 Performance 重点看长的紫色块(scripting)。
3)React 的话开 React DevTools Profiler 直接找到哪个组件在“无意义重渲染”。
4)不要在 render 里做深拷贝。这是我做性能审计时最常见的“罪案现场”。
5)优先用原生 structuredClone,只有遇到不支持类型,再考虑降级方案。
这些动作做完,很多项目的 JS 卡顿能直接少一大截(不是玄学,是路径正确)。
最不舒服的真相
很多 JS 应用变慢,不是因为出现了什么惊天技术事故,而是因为开发者习惯了“随手捷径”。现代 JavaScript 到处都是“一行搞定”的诱惑,而其中一些“一行”,比你想象的代价高得多。
我把 JSON.parse(JSON.stringify()) 换成 structuredClone 的那一刻,困扰用户很久的 UI 冻结,几乎是瞬间消失。所以,是的——一行代码真的可以把你应用拖慢。更关键的是:修复也真的就这么直接、这么简单。
最后:说说你踩过的“单行地雷”
轮到你了:哪一行 JavaScript,曾经毁过你的性能、毁过你的应用、或者让你调试到怀疑人生?欢迎在 云栈社区 分享你的经历。
图:技术专栏与资源概览
