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

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

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

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

相关推荐
FungLeo3 小时前
成为全栈·Node 后端篇·全文搜索:从 LIKE 到全文索引
node.js·全文索引·模糊搜索·全文搜索·成为全栈·like 搜索
mldong5 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排6 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60616 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角7 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!7 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰8 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi9 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen10 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马11 小时前
AI源码分析:拆解模型底层实现逻辑
前端