摘要
若**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
- 第二步:点击recording按钮,开始记录内存分配状态,不断点击click按钮,查看内存的变化
内存排查:大量的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 (
<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泄漏
- 第一步:F12打开控制台,选择Memory选项中的Heap snapshot
- 第二步:点击recording按钮,记录内存快照,多次点击click按钮,再次记录内存快照,对比两次内存快照的内存变化
内存排查:发现18个游离的HTMLDivElement元素
发现1: 游离的promise相关的dom数量和点击次数相关
发现2: 内存中保存了react组件的状态
发现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>)