Promise永久Pending状态造成内存泄漏

6,764 阅读6分钟

摘要

promise一直保持pending状态 ,将会在内存中保存相应的上下文,无法释放,这可能导致内存泄漏

尽管调用promise的 react 组件已经销毁,由于promise的状态未更新,导致保存React组件上下文不会释放 ,造成内存占用。

通过Promise.race设置超时的方式并不会解决Promise长时间pending占用内存的问题, Promise不会取消,直到它更改为fullfilled或者rejected状态

应避免写出Promise永远处于pending状态的代码。

本文将会用简单的几个demo来看下内存泄漏的表现,避免在业务中意外写出泄漏的代码。

应避免Promise永久pending状态

React组件销毁后,Promise仍然pending状态

尽管React组件已经销毁,但是其调用的promise仍然是pending状态,将组件的上下文保存在内存中,不会释放, 直到promise的状态修改为fullfilled或者rejected。

复现demo

通过下面这个例子,我们发现

  • 即使在Parent组件销毁后,aPromise返回的promise对象和Parent组件的上下文仍然保存在内存中;

  • 直到10min后,promise的状态修改为fullfilled后,控制台打印test,内存中的promise对象和parent组件的上下文被释放。

import { useEffect, useState } from 'react';

const Parent = () => {
  const [data, setData] = useState('this is a test for memory');
  const aPromise = async () => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('');
      }, 600000); // 10min
    });
  };

  useEffect(() => {
    aPromise().then(() => {
      console.log('test');
      setData('this is a test for update for memory leak');
    });
  }, []);
  return <div>{data}</div>;
};
export const CasePromisePending = () => {
  const [show, setShow] = useState(true);
  return (
    <div>A
      <div>测试promise pending内存泄漏</div>
      <button onClick={() => setShow(!show)}>click</button>
      {show && <Parent></Parent>}
    </div>
  );
}

复现步骤:Allocation instrumentation on timeline

react组件已经卸载,该泄漏不会导致dom的泄漏,无法通过内存快照寻找泄漏的dom元素,需要利用Allocation instrumentation on timeline查看内存中的对象。

Allocation instrumentation on timeline:录制一段时间内的javascipt内存分配情况,可以查看到录制的时间结束时为止,内存中仍然存留的对象

  1. 第一步:F12打开控制台,选择Memory选项中的Allocation instrumentation on timeline

image

  1. 第二步:点击recording按钮,开始记录内存分配状态,不断点击click按钮,查看内存的变化

image.png

内存排查:大量的Promise对象和React组件的上下文被保存在内存中

排查捕捉到的Promise对象和Object对象发现:

发现1: 36个Promise对象内存未释放和26个Object对象;

查看Promise对象中,有大量的我们定义的仍然处于pending状态的Promise;通过文件路径可以定位到具体的代码。

image

内存中的promise

发现2: 内存中26个Object对象未释放

image

image

内存中的Object对象(组件的上下文)

image

发现3: promise状态修改后,promise内存和组件内存释放

promise状态修改为fullfilled或者rejected后,promise内存和组件内存释放

10min后,控制台打印了test,再次重复上面的复现步骤发现:(点击了两次click按钮)Promise对象只有4个,Object对象只有16个(其中部分不相关的对象)

image

Promise关联的react组件中使用了ref引用,可能造成dom泄漏

复现demo

在promise所在组件的父组件中使用useRef,当promise一直未修改pending状态时,将出现dom内存泄漏

import { useEffect, useRef, useState } from 'react';

const Parent = (props) => {
  const [data, setData] = useState('this is a test for memory');
  const aPromise = async () => {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('');
      }, 500000);
    });
  };
  useEffect(() => {
    aPromise().then((value) => {
      console.log('test');
      setData('this is a test for update for memory leak');
    });
  }, []);
  return (
    <div>
      {data}
    </div>
  );
};
const Child = () => {
  const ref = useRef(null);
  return (
    <div ref={ref}>
      <Parent target={ref}></Parent>
    </div>
  );
};
const CasePromisePendingRef = () => {
  const [show, setShow] = useState(true);
  return (
    <div>
      测试promise pending内存泄漏
      <button onClick={() => setShow(!show)}>click</button>
      {show && <Child></Child>}
    </div>
  );
};
export { CasePromisePendingRef };

复现步骤:heap snapshot内存快照记录dom泄漏

  1. 第一步:F12打开控制台,选择Memory选项中的Heap snapshot

image

  1. 第二步:点击recording按钮,记录内存快照,多次点击click按钮,再次记录内存快照,对比两次内存快照的内存变化

image.png

内存排查:发现18个游离的HTMLDivElement元素

发现1: 游离的promise相关的dom数量和点击次数相关

image

发现2: 内存中保存了react组件的状态

image

发现3:promise状态修改后,promise内存和组件内存释放(全局promise仍然可以被回收)

在promise状态修改为fullfilled和rejected后,对比一开始的内存快照,发现游离的dom已经消失。

若promise状态一直为pending状态,则会出现内存泄漏。

image

【全局promise】执行结束之后ref中游离的dom detach不会被垃圾回收

解决方案

利用Promise.race设置超时能够解决吗?

不能!

Promise.race只是将最先返回的结果作为Promise.race的返回值,但并不会取消超时未返回的promise.

未返回的promise仍然在异步队列中等到结果的返回,过程中,对组件仍然引用。

可以在控制台中做如下例子验证:

const a = () => new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log("a");
        resolve("a")
    }, 50)
})
const b = () => new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log("b");
        resolve("b")
    }, 10000)
})
Promise.race([a(), b()]);

可以发现10s后,控制台中仍然会打印b。所以超时未返回的promise仍然会对react组件有引用。

避免Promise一直pending

Promise一旦创建是无法取消的,本质上,Promise是无法被终止的。它永远会等待结果的返回。

需要我们自己保证promise并不会一直pending,导致内存无法释放

排查内存的工具🔧

Chrome Memory Timeline

利用该工具可以捕捉一段时间的内存分配情况,截止到录制结束时间为止,每个时刻内存的占用情况,以及相应的占用内存的对象,也可以捕捉到游离(detach)的dom等元素

image

Chrome Memory Heap Snapshot

利用该工具可以捕捉某个时刻的内存,可以将两个时刻的内存进行对比,发现两个阶段增加的游离的dom,以此排查内存的泄漏情况。

image

image

Performance Monitor

可以实时观测内存的变化情况,主要观察DOM Nodes和JS heap size;

如果组件比较小,JS heap size的数据粒度比较小,观测比较困难;

  1. 利用performance.memory.usedJSHeapSize来看具体的内存数据(有一定的波动范围)
  1. 增加组件内存粒度:批量设置较大组件进行测试,比如Array.from({length: 10000}, (_, i) => i).map(i => <div></div>)

image

最后

欢迎大佬们投递字节飞书Desktop团队,招聘前端、C++、后端、客户端等,有意向可投递简历到飞书邮箱:benyafang.arya@bytedance.com 备注说明:目标城市和意向岗位