我是如何用一行 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),看看到底是哪个对象在疯狂增长。

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

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

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

相关推荐
掘金安东尼1 天前
让 JavaScript 更容易「善后」的新能力
前端·javascript·面试
掘金安东尼1 天前
用 HTMX 为 React Data Grid 加速实时更新
前端·javascript·面试
灵感__idea1 天前
Hello 算法:众里寻她千“百度”
前端·javascript·算法
yinuo1 天前
轻松接入大语言模型API -04
前端
袋鼠云数栈UED团队1 天前
基于 Lexical 实现变量输入编辑器
前端·javascript·架构
cipher1 天前
ERC-4626 通胀攻击:DeFi 金库的"捐款陷阱"
前端·后端·安全
UrbanJazzerati1 天前
非常友好的Vue 3 生命周期详解
前端·面试
AAA阿giao1 天前
从零构建一个现代登录页:深入解析 Tailwind CSS + Vite + Lucide React 的完整技术栈
前端·css·react.js
亦妤1 天前
JS执行机制、作用域及作用域链
javascript
兆子龙1 天前
像 React Hook 一样「自动触发」:用 Git Hook 拦住忘删的测试代码与其它翻车现场
前端·架构