首页/文章/前端性能

内存泄漏排查

2023-12-27
365813 分钟
...

背景 (书接上回)

  • ui妹子的无理要求,我通通满足了。但是不出意外的话,意外就出来了。
  • 此功能在上线之后,我们的业务在客户app内使用刷脸的时候会因为内存过高导致app将webview杀死。
  • 然后我们leader爆了,让我排查问题。可是可是,我哪里会排查内存泄漏呀。
  • 我:我不会。你自己不会上吗?你tm天天端个茶,抽个烟,翘个二郎腿,色眯眯的看着ui妹妹。
  • 领导:污蔑,你纯粹就是污蔑。我tm现在就可以让你滚蛋,你信吗?
  • 我:我怕你个鸟哦,我还不知道你啥水平,你tm能写出来个防抖节流,我就给你磕头。
  • 领导:hi~ui妹妹,今天过的好吗,来,哥哥这里有茶喝。(此时ui妹妹路过)。你赶快给我干活,以后ui妹妹留给你。
  • 艹!你早这么说不就好了。

Chrome devTools查看内存情况

  • 打开Chrome的无痕模式,这样做的目的是为了屏蔽掉Chrome插件对我们之后测试内存占用情况的影响
  • 打开开发者工具,找到Performance这一栏,可以看到其内部带着一些功能按钮,例如:开始录制按钮;刷新页面按钮;清空记录按钮;记录并可视化js内存、节点、事件监听器按钮;触发垃圾回收机制按钮等

简单录制一下百度页面,看看我们能获得什么,如下动图所示:

从上图中我们可以看到,在页面从零到加载完成这个过程中JS Heap(js堆内存)、documents(文档)、Nodes(DOM节点)、Listeners(监听器)、GPU memoryGPU内存)的最低值、最高值以及随时间的走势曲线,这也是我们主要关注的点

看看开发者工具中的Memory一栏,其主要是用于记录页面堆内存的具体情况以及js堆内存随加载时间线动态的分配情况

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

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

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

在得知有内存泄漏的情况存在时,我们可以改用Memory来更明确得确认问题和定位问题

首先可以用Allocation instrumentation on timeline来确认问题,如下图所示:

内存泄漏的场景

  • 闭包使用不当引起内存泄漏
  • 全局变量
  • 分离的DOM节点
  • 控制台的打印
  • 遗忘的定时器

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

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

<button onclick="myClick()">执行fn1函数</button>
<script>
    function fn1 () {
        let a = new Array(10000)  

        let b = 3

        function fn2() {
            let c = [1, 2, 3]
        }

        fn2()

        return a
    }

    let res = []  

    function myClick() {
        res.push(fn1())
    }
</script>

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

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

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

  • 在我们每次点击按钮后,动态内存分配情况图上都会出现一个蓝色的柱形,并且在我们触发垃圾回收后,蓝色柱形都没变成灰色柱形,即之前分配的内存并未被清除
  • 所以此时我们就可以更明确得确认内存泄漏的问题是存在的了,接下来就精准定位问题,可以利用Heap snapshot来定位问题,如图所示:

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

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

2. 全局变量

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

function fn1() {
    
    name = new Array(99999999)
}

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

fn1()

3. 分离的DOM节点

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

<div id="root">
    <div class="child">我是子元素</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 id="root">
    <div class="child">我是子元素</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. 控制台的打印

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

        console.log(obj);
    })
</script>

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

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

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

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

        
    })
</script>

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

其实同理 console.log也可以用Memory来进一步验证

未注释 console.log

注释掉了console.log

最后简单总结一下:在开发环境下,可以使用控制台打印便于调试,但是在生产环境下,尽可能得不要在控制台打印数据。所以我们经常会在代码中看到类似如下的操作:


if(isDev) {
    console.log(obj)
}

这样就避免了生产环境下无用的变量打印占用一定的内存空间,同样的除了console.log之外,console.errorconsole.infoconsole.dir等等都不要在生产环境下使用

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的内存也自然不会被释放
  • 那么我们如何来解决这个问题呢,假设我们只需要让定时器执行三次就可以了,那么我们可以改动一下代码:
<button>开启定时器</button>
<script>
    function fn1() {
        let largeObj = new Array(100000)
        let index = 0

        let timer = setInterval(() => {
            if(index === 3) clearInterval(timer);
            let myObj = largeObj
            index ++
        }, 1000)
    }

    document.querySelector('button').addEventListener('click', function() {
        fn1()
    })
</script>

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

  • performance

这次的录制结果就能看出,最后的曲线高度和初始基准线的高度一样,说明并没有内存泄漏的情况

  • memory

这里做一个解释,图中刚开始出现的蓝色柱形是因为我在录制后刷新了页面,可以忽略;然后我们点击了按钮,看到又出现了一个蓝色柱形,此时就是为fn1函数中的变量largeObj分配了内存,3s后该内存又被释放了,即变成了灰色柱形。所以我们可以得出结论,这段代码不存在内存泄漏的问题

简单总结一下: 大家在平时用到了定时器,如果在用不到定时器后一定要清除掉,否则就会出现本例中的情况。除了setTimeoutsetInterval,其实浏览器还提供了一个API也可能就存在这样的问题,那就是requestAnimationFrame

  • 好了好了,学完了,ui妹妹我来了

  • ui妹妹:去你m的,滚远点

好了兄弟们,内存泄漏学会了吗?

群已拉:群过期,请留言

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
前端性能2026-05-16
个人qwik项目优化
前言 最近把自己的 做了一轮比较彻底的性能与稳定性优化,核心目标不是“再堆几个小技巧”,而是把内容系统、文章详情链路和埋点链路重新梳顺,让它们更符合一句话: 能在构建阶段做完的事,就不要留到运行时反复做;能在服务端算好的数据,就不要再把原始材料丢给客户端自己算。 这一轮改造主要解决了 5 个问题: 1. 文章索引依然依赖运行时扫描 Markdown,重复做了构建阶段本可以完成的工作。 2. 文章详情页把原始 Markdown 一起带给前...
学习
前端性能2025-08-07
JS篇之性能优化
很多时候面试官,面试你的时候会问你:同学你在项目中做过哪些性能优化呢? 可能你是这样回答的: 1. 无用代码的删除,减小代码打的体积 2. 使用cdn加速 3. 图片压缩 4. 分包 5. 骨架屏 ........ 其实性能优化是个很宽泛的东西,可以从多方面进行入手 性能优化不是视觉体验打分,网上部分评判如下 | 核心指标 | 定义 | 优化方向 | | | | | | LCP(最大内容绘制) | 首屏最大内容加载完成时间 | 图片压缩...
学习面试
前端性能2025-08-03
前端PerformanceObserver
是 Web Performance API 的一部分,它提供了一种异步、高效地收集浏览器性能指标的方式。与传统的 或 方法不同, 允许你订阅特定类型的性能事件,并在这些事件发生时获得通知,而不是在某个时间点一次性获取所有已发生的事件。这对于实时监控和报告性能数据非常有用,尤其是在单页应用 (SPA) 中,因为页面加载后的用户交互也会产生新的性能事件。 的作用 主要用于: 1. 监控页面加载性能: 获取导航时间、资源加载时间等。 2. 跟...
学习
前端性能2025-07-27
前端常见的性能指标采集
前言 作为前端,我们的任务便是给用户一个好的产品体验感,不管是从首次进入页面的加载时长,还是对于交互时页面的响应流畅度,都是我们应该关注的点。 RAIL模型是由谷歌提出的,一种以用户为中心的性能模型,RAIL分别代表Web应用生命周期的四个方面:响应、动画、空闲、加载。 一般不管是页面打开还是流程交互,又或者是网络反馈,这也操作我们应该尽量在1000ms内完成,用户的体验感才不会差,用户留存率也将得到提升。 下面看一下我们常见的一些性能...
学习面试
前端性能2025-01-08
前端性能报告
数据说明 本报告的大部分内容均基于 Chrome 用户体验报告 ( ) 中的真实用户数据。 Chrome 用户体验报告(CrUX)反映了真实世界中 用户在热门网站上的体验,是 项目的官方数据集,展示了所有用户中心的核心 指标。 数据基于全球真实浏览器用户收集,并通过多种 和第三方工具公开提供,用于 搜索的页面体验排名因素。并非所有原点或页面都在数据集中,需公开可发现且访客数量足够多以达统计显著性。 核心网页指标(Core Web Vit...
学习
前端性能2024-05-28
Performance的使用攻略
Performance是Chrome浏览器自带的性能监测工具。根据我的使用,简单理解就是我们可以通过它录制一段时间的浏览器活动,通过活动的数据去分析页面是否存在提升的空间。想要获取页面的活动数据,那我们的第一步便是录制浏览器的活动。 打开F12,进入Perfomance标签页,我们可以看到出现了提示。我们不想看这个文本在说什么,我们只想马上开始用(不是),由于我已经百度翻译过了(不是,看过了),所以文本提示是在说箭头所指的地方,实心圆代...
学习

评论

请登录后发表评论

去登录
加载评论中...

目录