前端检查内存泄露

前言

前端应用的内存泄露,指不再使用内存未被释放,导致页面占用内存持续增长,轻则引发页面卡顿,加载缓慢,重则导致浏览器崩溃, 尤其在单页应用SPA中,路由切换频繁但内存不回收,问题会被无限放大,比如用户长时间使用某后台管理系统,可能出现操作响应式延迟,甚至需要强制刷新才能恢复,这很可能是内存泄露在"作祟"

一. 前端常见的内存泄露场景

1. 意外的全局变量:未声明的变量(如a = 10而非let a = 10)会挂载到window上,页面不刷新就不会释放;

2. 闭包滥用:闭包会保留对外部作用域的引用,若长期持有 DOM 或大型对象,会导致内存无法回收(如未清理的事件监听回调) ;

3. 未清理的 DOM 引用:删除 DOM 节点后,仍保留其引用(如let el =       document.getElementById('test'),删除el对应的 DOM 后未置el = null);

4. 定时器 / 事件监听未销毁:setInterval未用clearInterval清除、addEventListener未用removeEventListener解绑,尤其在组件挂载 / 卸载时容易遗漏;

5.第三方库 / 插件残留:部分第三方库(如图表库、播放器)使用后未调用销毁方法,导致内部资源无法释放;

6.数组 / 对象无限增长:全局缓存对象未设置过期机制,或数组持续 push 数据但未清理无效项。

二. 核心工具: 浏览器开发者工具(Chrome DevTools )

1.第一步: 复现内存泄露场景

首先需要稳定复现泄露行为,比如: 

路由切换(SPA 中反复切换 A→B→A→B);

点击按钮触发某操作(如打开弹窗后关闭);

长时间滚动或轮询请求数据。

2.第二步 

1. 打开Chrome 开发者工具

2. 勾选面板顶部的「Memory」选项(同时可保留默认的「CPU」「Network」);

3.点击左上角的「录制」按钮(圆形红点),然后执行复现步骤(如切换路由 5 次);

4. 执行完成后点击「停止」,等待面板生成报告。

关键观察点: 

报告中[Memory]曲线若持续上升且不回落(即使操作停止后,内存仍未下降),则大概率存在泄露

若曲线在操作后能回落至初始水平,说明内存已正常回收,无泄露

3.第三步 

Memory 面板定位泄露对象,主要是用于记录页面堆内存的具体情况以及js堆内存随加载时间线动态的分配情况

Performance 面板确认泄露后,用 Memory 面板进一步定位 “哪些对象未被回收”:

1. 常用快照类型

Memory面板支持3中快照类型,按需选择:

  • Heap snapshot(堆快照):捕获当前内存中所有对象的快照,可对比不同快照的差异,适合定位未回收的对象;
  • Allocation instrumentation on timeline(时间线分配记录):实时记录内存分配过程,适合观察某操作期间的内存分配细节;
  • Allocation sampling(分配采样):低开销的内存采样,适合快速排查大面积泄露,无需精确到单个对象。

2. 堆快照对比操作

  • 打开 DevTools → 切换到「Memory」面板;
  • 选择「Heap snapshot」,点击「Take snapshot」拍摄初始快照(记为快照 1,此时未执行任何操作);
  • 执行一次泄露复现步骤(如切换一次路由);
  • 再次点击「Take snapshot」拍摄快照 2;
  • 重复复现步骤 N 次(如再切换 3 次路由),拍摄快照 3;
  • 在 Memory 面板左侧的快照列表中,点击快照 2/3 的下拉框,选择「Comparison」(对比),并选择 “与快照 1 对比”; (下图对比我用的是 5 和 6 快照进行对比)

这时候就能看出变占用的内存大小,是那些操作造成了内存泄露

堆快照就像照相机一样,能记录你当前页面的堆内存情况,每快照一次就会产生一条快照记录

如上图所示,刚开始执行了一次快照,记录了当时堆内存空间占用为59.4MB,然后我们点击了页面中某些按钮,又执行一次快照,记录了当时堆内存空间占用为59.7MB。并且点击对应的快照记录,能看到当时所有内存中的变量情况(结构、占总占用内存的百分比...)

在开始记录后,我们可以看到图中右上角有起伏的蓝色与灰色的柱形图,其中蓝色表示当前时间线下占用着的内存; 灰色表示之前占用的内存空间已被清除释放

用Allocation on timeline 

三. 示例 

1. 闭包使用不当引起内存泄漏

使用Performance和Memory来查看一下闭包导致的内存泄漏问题在退出fn1函数执行上下文后,该上下文中的变量a本应被当作垃圾数据给回收掉,但因fn1函数最终将变量a返回并赋值给全局变量res,其产生了对变量a的引用,所以变量a被标记为活动变量并一直占用着相应的内存,假设变量res后续用不到,这就算是一种闭包使用不当的例子

设置了一个按钮,每次执行就会将fn1函数的返回值添加到全局数组变量res中,是为了能在performacne的曲线图中看出效果 

使用Performance和Memory来查看一下闭包导致的内存泄漏问题

录制的时候去执行fn1 函数

  • 在每次录制开始时手动触发一次垃圾回收机制,这是为了确认一个初始的堆内存基准线,便于后面的对比,然后我们点击了几次按钮,即往全局数组变量res中添加了几个比较大的数组对象,最后再触发一次垃圾回收,发现录制结果的JS Heap曲线刚开始成阶梯式上升的,最后的曲线的高度比基准线要高,说明可能是存在内存泄漏的问题
  • 在得知有内存泄漏的情况存在时,我们可以改用Memory来更明确得确认问题和定位问题
  • 首先可以用Allocation instrumentation on timeline来确认问题,如下图所示:

先选择这个 

然后去录制 

在我们每次点击按钮后,动态内存分配情况图上都会出现一个蓝色的柱形,并且在我们触发垃圾回收后,蓝色柱形都没变成灰色柱形,即之前分配的内存并未被清除,即之前分配的内存并未被清除

所以此时我们就可以更明确得确认内存泄露的问题是存在的了,,接下来就精准定位问题,可以利用Heap snapshot来定位问题,如图所示:

  • 第一次先点击快照记录初始的内存情况,然后我们多次点击按钮后再次点击快照,记录此时的内存情况,发现从原来的1.2 M内存空间变成了1.7 M内存空间,然后我们选中第二条快照记录,可以看到右上角有个All objects的字段,其表示展示的是当前选中的快照记录所有对象的分配情况,而我们想要知道的是第5条快照与第一条快照的区别在哪,所以选择Object allocated between Snapshot1 and Snapshot2即展示第一条快照和第二条快照存在差异的内存对象分配情况,此时可以看到Array的百分比很高,初步可以判断是该变量存在问题,点击查看详情后就能查看到该变量对应的具体数据了

以上就是一个判断闭包带来内存泄漏问题并简单定位的方法了

2. 全局变量

全局的变量一般是不会被垃圾回收掉的当然这并不是说变量都不能存在全局,只是有时候会因为疏忽而导致某些变量流失到全局,例如未声明变量,却直接对某变量进行赋值,就会导致该变量在全局创建,如下所示:

function fn1() { // 此处变量name未被声明 name = new Array(99999999) } fn1()
  • 此时这种情况就会在全局自动创建一个变量name,并将一个很大的数组赋值给name,又因为是全局变量,所以该内存空间就一直不会被释放
  • 解决办法的话,自己平时要多加注意,不要在变量未声明前赋值,或者也可以开启严格模式,这样就会在不知情犯错时,收到报错警告,例如

3.分离的DOM节点

假设你手动移除了某个dom节点,本应释放该dom节点所占用的内存,但却因为疏忽导致某处代码仍对该被移除节点有引用,最终导致该节点所占内存无法被释放,例如这种情况

<div> <div>我是子元素</div> <button>移除</button> </div> <script> let btn = document.querySelector('button') let child = document.querySelector('.child') let root = document.querySelector('#root') btn.addEventListener('click', function() { root.removeChild(child) }) </script>

该代码所做的操作就是点击按钮后移除.child的节点,虽然点击后,该节点确实从dom被移除了,但全局变量child仍对该节点有引用,所以导致该节点的内存一直无法被释放,可以尝试用Memory的快照功能来检测一下,如图所示

同样的先记录一下初始状态的快照,然后点击移除按钮后,再点击一次快照,此时内存大小我们看不出什么变化,因为移除的节点占用的内存实在太小了可以忽略不计,但我们可以点击第二条快照记录,在筛选框里输入detached,于是就会展示所有脱离了却又未被清除的节点对象

解决办法如下图所示:

<div> <div>我是子元素</div> <button>移除</button> </div> <script> let btn = document.querySelector('button') btn.addEventListener('click', function() { let child = document.querySelector('.child') let root = document.querySelector('#root') root.removeChild(child) }) </script>
改动很简单,就是将对.child节点的引用移动到了click事件的回调函数中,那么当移除节点并退出回调函数的执行上文后就会自动清除对该节点的引用,那么自然就不会存在内存泄漏的情况了,我们来验证一下,如下图所示:

结果很明显,这样处理过后就不存在内存泄露的情况了 

4. 控制台打印

我们在按钮的点击回调事件中创建了一个很大的数组对象并打印,用performance来验证一下

开始录制,先触发一次垃圾回收清除初始的内存,然后点击三次按钮,即执行了三次点击事件,最后再触发一次垃圾回收。查看录制结果发现JS Heap曲线成阶梯上升,并且最终保持的高度比初始基准线高很多,这说明每次执行点击事件创建的很大的数组对象obj都因为console.log被浏览器保存了下来并且无法被回收

接下来注释掉console.log,再来看一下结果

<button>按钮</button> <script> document.querySelector('button').addEventListener('click', function() { let obj = new Array(1000000) // console.log(obj); }) </script>

可以看到没有打印以后,每次创建的obj都立马被销毁了,并且最终触发垃圾回收机制后跟初始的基准线同样高,说明已经不存在内存泄漏的现象了

5. 遗忘的定时器

定时器也是平时很多人会忽略的一个问题,比如定义了定时器后就再也不去考虑清除定时器了,这样其实也会造成一定的内存泄漏。来看一个代码示例:

<button>开启定时器</button> <script> function fn1() { let largeObj = new Array(100000) setInterval(() => { let myObj = largeObj }, 1000) } document.querySelector('button').addEventListener('click', function() { fn1() }) </script>

这段代码是在点击按钮后执行fn1函数,fn1函数内创建了一个很大的数组对象largeObj,同时创建了一个setInterval定时器,定时器的回调函数只是简单的引用了一下变量largeObj,我们来看看其整体的内存分配情况吧:

按道理来说点击按钮执行fn1函数后会退出该函数的执行上下文,紧跟着函数体内的局部变量应该被清除,但图中performance的录制结果显示似乎是存在内存泄漏问题的,即最终曲线高度比基准线高度要高,那么再用Memory来确认一次:

  • 在我们点击按钮后,从动态内存分配的图上看到出现一个蓝色柱形,说明浏览器为变量largeObj分配了一段内存,但是之后这段内存并没有被释放掉,说明的确存在内存泄漏的问题,原因其实就是因为setInterval的回调函数内对变量largeObj有一个引用关系,而定时器一直未被清除,所以变量largeObj的内存也自然不会被释放
  • 那么我们如何来解决这个问题呢,假设我们只需要让定时器执行三次就可以了,那么我们可以改动一下代码:

现在我们再通过performance和memory来看看还不会存在内存泄漏的问题

  • performance
  • memory
  • 后我们点击了按钮,看到又出现了一个蓝色柱形,此时就是为fn1函数中的变量largeObj分配了内存,3s后该内存又被释放了,即变成了灰色柱形。所以我们可以得出结论,这段代码不存在内存泄漏的问题

Read more

高性能OCR落地利器|DeepSeek-OCR-WEBUI开源实践

高性能OCR落地利器|DeepSeek-OCR-WEBUI开源实践 1. 引言:OCR技术的工程化挑战与新解法 在数字化转型加速的背景下,光学字符识别(OCR)已成为文档自动化、信息提取和智能审核等场景的核心技术。然而,传统OCR方案在面对复杂版面、模糊图像或多语言混合文本时,往往表现不佳,且部署流程繁琐,严重制约了其在实际业务中的广泛应用。 DeepSeek-OCR 的出现为这一难题提供了全新思路。作为一款基于深度学习的大模型驱动OCR系统,它不仅具备高精度、强鲁棒性的识别能力,更通过 DeepSeek-OCR-WEBUI 这一开源项目实现了“零代码+网页端交互”的极简使用模式。本文将围绕该镜像的技术特性、部署流程与核心功能展开详细解析,帮助开发者快速掌握其工程化落地方法。 2. 技术架构解析:DeepSeek-OCR的核心优势 2.1 模型设计原理 DeepSeek-OCR 采用 CNN + Transformer注意力机制 的混合架构: * 前端卷积网络(CNN) 负责图像特征提取,对倾斜、模糊、低分辨率等退化图像具有良好的适应性; * 中段序列建模模块 利

ofa_image-caption代码实例:扩展支持WebP格式与EXIF元数据保留功能

ofa_image-caption代码实例:扩展支持WebP格式与EXIF元数据保留功能 1. 引言 你有没有遇到过这种情况?从手机或相机里导出一堆照片,想快速整理归档,却要一张张手动写描述,费时又费力。或者,在做内容创作时,需要为大量图片配上精准的英文说明,人工处理效率极低。 今天要介绍的这个工具,就是来解决这个痛点的。它叫 ofa_image-caption,是一个纯本地运行的图像描述生成工具。简单来说,你给它一张图,它就能用英文告诉你这张图里有什么。 这个工具的核心是基于一个叫 OFA 的模型,这个模型在图像描述生成领域表现很不错。我们之前发布的版本已经能很好地处理 JPG、PNG 这些常见格式了。但最近,越来越多的用户开始使用 WebP 这种更高效的图片格式,同时,很多摄影师和内容创作者也希望生成的描述能保留图片拍摄时的原始信息(比如拍摄时间、相机型号)。 所以,我们对这个工具进行了一次重要的升级。这篇文章,我就带你手把手看看,我们是如何在原有代码基础上,扩展了对 WebP 格式的支持,并实现了 EXIF 元数据的保留功能。

JWT 技术(JSON Web Token) 全解:原理、应用与生产级避坑指南

笔者阅读很多 JWT 技术的博文,发现大多只是讲 “JWT 是什么”,而这篇文章重点介绍它为什么出现、结构细节、以及生产环境中最棘手的“注销与续签”问题。 在前后端分离、微服务架构大行其道的今天,JWT(JSON Web Token)几乎成为了身份认证的代名词。 很多开发者只知道它是一个“长长的字符串”,用来做登录校验,但并不清楚它内部的运作机制,以及它在安全性上的潜在风险。本文将从原理、结构、流程、以及最核心的生产陷阱四个维度进行详细拆解。 一、为什么需要 JWT?(Session vs Token) 在 JWT 出现之前,我们主要使用 Session + Cookie 的方式。 1.传统Session的认证痛点 * 服务端有状态:服务端需要保存 Session 数据(内存或Redis)。 * 扩展性差:集群环境下,必须做

零代码体验!BAAI/bge-m3 WebUI一键分析文本相似度

零代码体验!BAAI/bge-m3 WebUI一键分析文本相似度 1. 为什么你需要一个“不用写代码”的语义相似度工具? 你有没有遇到过这些场景: * 写完一段产品文案,想确认它和竞品描述是否太雷同? * 做知识库检索时,发现用户搜“怎么重置密码”却没召回“忘记登录密码怎么办”这条答案? * 客服机器人总把“退款”和“换货”当成一回事,导致工单分错类? * 教育平台里,学生提交的简答题答案五花八门,人工批改耗时又难统一标准? 这些问题背后,本质都是同一个技术需求:判断两段文字在意思上到底有多像——不是看字面是否重复,而是理解它们表达的语义是否一致。 传统方法靠关键词匹配、编辑距离或TF-IDF,结果常常很尴尬: “苹果手机续航差” 和 “iPhone电池不耐用” → 应该高分 但关键词完全不重合,TF-IDF打0.1分,系统直接忽略 这时候,就需要真正懂“意思”的模型。而BAAI/bge-m3,正是当前开源领域中少有的、能稳定处理中文长句+