前端性能监控之内存泄漏 | 青训营笔记

128 阅读5分钟

这是我参与「第四届青训营 」笔记创作活动的的第 14 天。

前言

什么是内存泄漏?

内存泄漏是指程序中已动态分配的堆内存由于某种原因未释放或无法释放,造成系统内存的浪费,会导致程序运行速度减慢甚至系统崩溃等严重后果。

为什么会发生内存泄漏?

尽管浏览器具有垃圾回收机制,可以通过标记清除等方法清除垃圾,但在某些情况下,如:一个对象已经不再需要使用,应当被回收时,另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而保留在堆内存中,这就产生了内存泄漏。

导致内存泄漏的几个场景

1. 意外的全局变量

没有被手动回收的全局变量

function fn(){
    variable = "variable" // 给未声明的变量直接赋值,会变成一个全局变量。
}
fn();

this引起的全局变量

function fn(){
    this.variable = "variable" 
}
fn(); // 由于此时fn函数的作用域是全局作用域,所以this指向的是window,相当于给window对象加了个属性variable

全局变量的生命周期最长,直至页面关闭前都会存活着,所以全局变量上的分配的内存在程序运行时一直都不会被回收。当全局变量使用不当,没有及时手动回收(如:手动赋值 null),或者代码写错将某个变量挂载到全局作用域时,也就会发生内存泄漏了。

2. 未清除的定时器

setTimeoutsetInterval是由浏览器专门线程来维护它的生命周期的,所以当在某个页面使用了定时器,而定时器的回调函数又持有当前页面某个变量或某些DOM元素,当该页面销毁前,没有手动去释放清理这些定时器的话,就会导致即使页面关闭了,由于定时器持有该页面的部分引用而造成页面内存无法被正常回收,从而造成内存泄漏。

<div id="box"></div>

<script>
  const box = document.getElementById("box");

  setInterval(() => {
    const span = document.createElement("span");
    span.innerHTML = "添加一个span";
    box.appendChild(span);
  }, 1000);
  // 定时器持有页面元素的引用
</script>

3. 使用不当的闭包

MDN:闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。

通常情况下,函数被调用完后,该函数申请的内存空间会被浏览器释放。但当函数A的返回值是一个函数B时,而函数B使用了函数A的变量,则函数B会持有函数A的词法环境,函数B又被具有不同生命周期的东西持有,导致函数A虽然执行完了,但内存A却无法被回收。

function outer(){
    let outerVariable = ""
    
    function inner(){
        console.log(outerVariable)
    }
    
    return inner; // 返回一个函数
}

const fn = outer(); // 函数outer申请的内存理应在执行完后被回收,但由于返回的inner函数持有了outer函数的变量,所以导致outer的内存无法被正常回收。
fn()

尽管闭包的特性本就是让内部函数持有外部函数的词法环境,目的就是为了让这块内存不被回收(因为可能未来还需要用到),但这无疑会增加内存的消耗,所以不可滥用闭包。

4. 被遗忘的DOM元素

DOM元素的生命周期取决于是否挂载在DOM树上,但在JS中持有了页面上某个DOM元素的引用时,那么它的生命周期取决于JS和DOM树。如果我们需要删除这个DOM元素,则需要在两个地方去清理才能正常将其内存回收。

5. 未清除的事件监听器

我们在使用前端框架进行程序开发时,通常会封装一些组件,如果在组件中使用了window.addEventListener()等监听一些事件(load、change、resize),但在组件销毁时不将这些事件清除,则这些监听器会一直存在于内存当中,直到程序关闭。对于一些复杂的单页面应用,若存在大量的监听器未被清除,则会出现页面卡顿甚至程序崩溃等情况。

6. 网络请求的回调

在某些场景中,页面发起网络请求并注册了一个回调,且回调函数内持有该页面的变量或DOM元素的引用,那么当页面销毁时,应当注销网络请求的回调,否则因为网络请求的回调持有页面的部分引用,也会导致页面部分内存无法被回收。

7. Map、Set等强引用

let obj = {name: 'obj'}
let set = new Set()
set.add(obj)

obj = null;
console.log(set.size) // 1
let obj = {name: 'obj'}
let ws = new WeakMap()
ws.add(obj)

obj = null;
console.log(ws) // {}

如果浏览器输出ws还是有值,说明浏览器还没执行GC,因为垃圾回收并不是每时每刻都在进行,可以等待一段时间再输出看看,或者是强制执行一下垃圾回收。

内存相关API

window.performance.memory