我是如何用一行 JS 代码,让你的浏览器内存瞬间崩溃的?

下午茶时间,我给隔壁的后端老哥发了个链接。

他毫无防备地点开了。

三秒钟后,我听到他那台 16G 内存的 MacBook Pro 风扇开始狂啸,仿佛要起飞🤣。

五秒钟后,他的鼠标指针变成了彩虹圈,卡在屏幕中间动弹不得。

十秒钟后,Chrome 惨叫一声,弹出了那个让无数前端人闻风丧胆的页面------(崩溃啦)。

后端老哥过来问我:你刚才发的是什么病毒?!😡

我淡定地喝了一口咖啡:没有病毒,只是一行 JavaScript 代码而已。

今天,我就带大家拆解一下这个崩溃背后的原理。这不仅是个恶作剧,更是深入理解 V8 引擎内存机制 的绝佳机会。


这行代码长什么样呢?

⚠️ 高能预警:请勿在未保存工作的浏览器标签页中运行以下代码!

⚠️ 高能预警:请勿在未保存工作的浏览器标签页中运行以下代码!

⚠️ 高能预警:请勿在未保存工作的浏览器标签页中运行以下代码!

重要的事,先说三遍😃

如果你想让浏览器瞬间暴毙,其实不需要什么复杂的黑客技术。最简单粗暴的方式,就是 堆溢出 (Heap Overflow)

核心代码只有这一行(为了方便展示,我稍微展开一点):

JavaScript 复制代码
// 就是它!
const boom = () => {
  window.leakArr = [];
  setInterval(() => {
    // 分配 20MB 的 TypedArray
    window.leakArr.push(new Uint8Array(20 * 1024 * 1024));
  }, 10);
};

boom();

原理极其简单:

  1. 我们开了一个 setInterval 定时器,每 10 毫秒执行一次。
  2. new Uint8Array(20 * 1024 * 1024) 会创建一个约 20MB 的 TypedArray。
  3. 最关键的一步:我们将这个TypedArray push 进了一个全局变量 window.leakArr 中。

因为 leakArr 是挂在 window 上的,浏览器会认为:这个数据用户还要用,我不能回收!

于是,GC(垃圾回收器)不敢动它。

内存就像被注水一样瞬间飙升。

1秒钟增加 1GB,几秒钟后,Chrome 的 V8 引擎就会因为达到 Heap Limit(堆上限) 而崩溃。


浏览器是怎么死的?

很多兄弟会问:我电脑 32G 内存,这才哪到哪?为什么浏览器这么脆?

这里就要讲到 Node.js 和 Chrome V8 的一个限制。

V8 的内存限制(Heap Limit)

Chrome 并不是你有多少物理内存,它就敢用多少。

为了防止网页占用过多资源导致系统卡死,V8 对每个页面的 JS 堆内存是有硬性限制的。

  • 在 64 位系统下,默认限制通常在 1.4GB - 4GB 之间(取决于 Chrome 版本和设备)。

当你那行代码疯狂申请内存时,V8 的处理是这样的:

  • 第 1 秒:内存占用 500MB。GC:还行,能撑住。
  • 第 2 秒:内存占用 1.5GB。GC:卧槽?快满了!赶紧 Full GC(全量回收)一下!
  • 第 2.1 秒 :GC 发现 leakArrwindow 引用着,一个字节都回收不掉
  • 第 3 秒 :内存突破限制。V8:毁灭吧,累了😖。 -> Crash。

主线程阻塞 (The UI Freeze)

在崩溃前,为什么鼠标会卡死?

因为 JS 是单线程的。

当内存快满时,GC 会疯狂介入,试图腾出空间。GC 的执行是会阻塞主线程的。

CPU 都在忙着做垃圾回收,根本没空去响应你的鼠标移动和页面渲染。


你可能每天都在写 Bug🤔

上面的代码是明目张胆的抢劫,但在我们的日常业务代码中,更多的是 慢性偷窃 ------也就是内存泄漏 (Memory Leak)

虽然你不会写出 while(true),但以下这几种操作,和上面的性质是一样的:

忘记清除的定时器 / 监听器

JavaScript 复制代码
// React 组件 useEffect
useEffect(() => {
  const timer = setInterval(() => {
    console.log('Fetching data...');
    // 闭包引用了大量数据
  }, 1000);

  // 致命错误:忘记写 return () => clearInterval(timer);
}, []);

如果你在 SPA(单页应用)里频繁切换路由,这个组件虽然卸载了,但 timer 还在跑,引用的数据还在内存里。切几十次路由,页面就卡成 PPT 了。

游离的 DOM 节点

JavaScript 复制代码
let detachedNodes = [];
function create() {
  const ul = document.createElement('ul');
  for (let i = 0; i < 1000; i++) {
    ul.appendChild(document.createElement('li'));
  }
  // 虽然这个 ul 没有插入到 document.body 里
  // 但它被全局变量 detachedNodes 引用着,无法回收!
  detachedNodes.push(ul); 
}

很多图表库(ECharts 等)如果销毁时不调用 dispose(),就会留下这种内存幽灵。

3. 闭包陷阱

JavaScript 复制代码
function outer() {
  const heavyData = new Array(1000000).join('*'); // 大数据
  return function inner() {
    console.log('I am keeping heavyData alive!');
    // 虽然 inner 只用到了 console,但因为它在 outer 内部
    // 有些旧引擎可能会连带把 heavyData 一起保留在闭包上下文中
  }
}

如何防止呢?

如果不幸遇到了页面越用越卡,怎么排查是不是内存泄露?

Chrome DevTools 是你最好的排查工具:

  1. 打开 Performance Monitor(性能监视器):

    • F12 -> Esc -> 选择 Performance monitor
    • 盯着 JS 堆DOM 节点
    • 如果你做过一遍 GC(点击 Memory 面板的小垃圾桶),这两个数值只增不减,恭喜你,漏了。
  1. Memory 面板抓快照:

    • 录制 Heap Snapshot
    • 搜索 Detached,看看有没有红色的游离 DOM 节点。
    • 或者对比两次快照(Allocation instrumentation on timeline),看看到底是哪个对象在疯狂增长。

那行让浏览器崩溃的代码,大家玩玩就好,千万别写到生产环境里(除非你渴望被运维提着刀追杀🤣)。

前端工程师的价值,不只是把页面画出来,更是要保证页面在长时间运行下依然丝滑。

下次再遇到页面卡顿,别光顾着怪网络慢,打开控制台看看,说不定是你写的某个内存泄漏代码正在悄悄吞噬用户的内存。

相关推荐
努力犯错玩AI2 小时前
如何在ComfyUI中使用Qwen-Image-Layered GGUF:完整安装和使用指南
前端·人工智能
Lefan2 小时前
在浏览器中运行大模型:基于 WebGPU 的本地 LLM 应用深度解析
前端
五仁火烧2 小时前
npm run build命令详解
前端·vue.js·npm·node.js
哈__2 小时前
React Native 鸿蒙跨平台开发:简易记事本 APP
javascript·react native·react.js
贺今宵2 小时前
electron-vue无网络环境,读取本地图片/文件展示在页面vue中protocol
前端·javascript·electron
IT_陈寒2 小时前
SpringBoot 3.x实战:5个高效开发技巧让我减少了40%重复代码
前端·人工智能·后端
noodles10242 小时前
iOS下怎么就找不到好用的新手引导组件呢?还是得自己动手
前端
不务正业的前端学徒2 小时前
vue2/3 watch原理
前端
老前端的功夫2 小时前
TypeScript索引访问类型深度解析:类型系统的动态访问与模式匹配
前端·javascript·ubuntu·架构·typescript·前端框架