Description
Preflight Checklist
- 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
unload
listener when try to make snapshot again - It's unclear why we should at all try to listen
preload
stylesheet 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";
record({
emit: () => void 0,
checkoutEveryNms: 5000,
});
// Run getEventListeners every 5 seconds in DevTools
// getEventListeners(document.querySelector('link'));

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