rrweb
rrweb copied to clipboard
[Bug]: Memory leak and high CPU usage for link with ref preload
Preflight Checklist
- [x] I have searched the issue tracker for a bug report that matches the one I want to file, without success.
What package is this bug report for?
rrweb
Version
2.0.0-alpha.18
Expected Behavior
Prevent memory leak.
- First of all we should cleanup
unloadlistener when try to make snapshot again - It's unclear why we should at all try to listen
preloadstylesheet doesn't contains sheet at all and only preload it so maybe we should just drop this check or upgrade it torel="preload stylesheet"which preload an apply styles.
Actual Behavior
Memory Leak:
<link rel="preload" href="./style.css" as="style"> always returns link.sheet equals to null. So we repeatedly call setTimeout and addListener for such link and don't cleanup added event listener that lead to memory leak.
Hight CPU usage:
The previous case with memory leak lead to degradation of performance if repeatedly stop and start recording. For example we stop recording when page invisible and start again when visible. In this case leaked timer hold previous record and now we have N(number of restarts) timers which constantly call setTimeout and add listeners so every stylesheetLoadTimeout we call N timers which add N listeners. In worst case the page is freez and consume 100% of CPU because we have a lot of timers and a lot of listeners.
Steps to Reproduce
index.html
<html>
<link rel="preload" href="./style.css" as="style">
<body>
<script type="module" src="./index.js"></script>
</body>
</html>
index.js
// Just Memory Leak
import { record } from "rrweb";
const stopRef = { current: null };
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
console.log("Recording stopped due to visibility change");
stopRef.current?.();
} else {
console.log("Recording started due to visibility change");
stopRef.current = record({
emit: () => void 0,
checkoutEveryNms: 1000,
});
}
});
index.js
// Memory leak + high CPU usage
import { record } from "rrweb";
async function main() {
const stopRef = { current: null };
const pageVisibilityChangeNTimes = 5000;
for (let i = 0; i < pageVisibilityChangeNTimes; i++) {
await new Promise((resolve) => {
startRecording(resolve);
});
}
function startRecording(resolve) {
stopRef.current?.();
stopRef.current = record({
emit: () => resolve(),
checkoutEveryNms: 1000,
});
}
}
main();
Testcase Gist URL
No response
Additional Information
No response