相关文章推荐
好帅的西装  ·  JR63406: Hierarchical ...·  2 年前    · 
细心的茶叶  ·  Apipost使用教程 - 知乎·  2 年前    · 
谦和的灌汤包  ·  Python-datetime — ...·  2 年前    · 
听话的眼镜  ·  python - How to read ...·  2 年前    · 

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内存分配情况,可以查看到录制的时间结束时为止,内存中仍然存留的对象

  • 第一步:F12打开控制台,选择Memory选项中的Allocation instrumentation on timeline
  • 内存排查:大量的Promise对象和React组件的上下文被保存在内存中

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

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

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

    内存中的promise

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

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

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

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

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

    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 (
          {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 (
          测试promise pending内存泄漏
          <button onClick={() => setShow(!show)}>click</button>
          {show && <Child></Child>}
        </div>
    export { CasePromisePendingRef };
    

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

  • 第一步:F12打开控制台,选择Memory选项中的Heap snapshot
  • 发现3:promise状态修改后,promise内存和组件内存释放(全局promise仍然可以被回收)

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

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

    【全局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等元素

    Chrome Memory Heap Snapshot

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

    Performance Monitor

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

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

  • 利用performance.memory.usedJSHeapSize来看具体的内存数据(有一定的波动范围)
  • 增加组件内存粒度:批量设置较大组件进行测试,比如Array.from({length: 10000}, (_, i) => i).map(i => <div></div>)
  • 欢迎大佬们投递字节飞书Desktop团队,招聘前端、C++、后端、客户端等,有意向可投递简历到飞书邮箱:benyafang.arya@bytedance.com 备注说明:目标城市和意向岗位

    分类:
    前端
    标签: