Skip to content

Commit bead172

Browse files
CopilotCopilot
andcommitted
Fix memory leak in visible() function
The visible() function had a memory leak when querySelectorAll returned no elements - the IntersectionObserver was created but never observed anything, so the promise never resolved and the observer/closure were retained forever. Fix by: - First checking if elements exist - If they exist, create IntersectionObserver (original behavior) - If they don't exist, create MutationObserver to watch for DOM insertions - When element appears, disconnect MutationObserver and create IntersectionObserver Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 563790e commit bead172

File tree

1 file changed

+59
-22
lines changed

1 file changed

+59
-22
lines changed

src/lazy-define.ts

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,31 +24,68 @@ const firstInteraction = new Promise<void>(resolve => {
2424
document.addEventListener('pointerdown', handler, listenerOptions)
2525
})
2626

27-
const visible = (tagName: string): Promise<void> =>
28-
new Promise<void>(resolve => {
29-
const observer = new IntersectionObserver(
30-
entries => {
31-
for (const entry of entries) {
32-
if (entry.isIntersecting) {
33-
resolve()
34-
observer.disconnect()
35-
return
27+
const visible = async (tagName: string): Promise<void> => {
28+
const makeViewportMonitor = (itemsToMonitor: Element[]) => {
29+
return new Promise<void>(signalComplete => {
30+
const viewMonitor = new IntersectionObserver(
31+
viewEvents => {
32+
for (const viewEvent of viewEvents) {
33+
if (viewEvent.isIntersecting) {
34+
signalComplete()
35+
viewMonitor.disconnect()
36+
return
37+
}
3638
}
39+
},
40+
{
41+
// Currently the threshold is set to 256px from the bottom of the viewport
42+
// with a threshold of 0.1. This means the element will not load until about
43+
// 2 keyboard-down-arrow presses away from being visible in the viewport,
44+
// giving us some time to fetch it before the contents are made visible
45+
rootMargin: '0px 0px 256px 0px',
46+
threshold: 0.01
3747
}
38-
},
39-
{
40-
// Currently the threshold is set to 256px from the bottom of the viewport
41-
// with a threshold of 0.1. This means the element will not load until about
42-
// 2 keyboard-down-arrow presses away from being visible in the viewport,
43-
// giving us some time to fetch it before the contents are made visible
44-
rootMargin: '0px 0px 256px 0px',
45-
threshold: 0.01
48+
)
49+
for (const monitoredItem of itemsToMonitor) {
50+
viewMonitor.observe(monitoredItem)
4651
}
47-
)
48-
for (const el of document.querySelectorAll(tagName)) {
49-
observer.observe(el)
50-
}
51-
})
52+
})
53+
}
54+
55+
const makeElementHunter = () => {
56+
return new Promise<Element[]>(deliverCapturedElements => {
57+
const domHunter = new MutationObserver(capturedChanges => {
58+
for (const capturedChange of capturedChanges) {
59+
const capturedNodes = Array.from(capturedChange.addedNodes)
60+
for (const capturedNode of capturedNodes) {
61+
if (!(capturedNode instanceof Element)) continue
62+
63+
const directHit = capturedNode.matches(tagName) ? capturedNode : null
64+
const nestedHit = capturedNode.querySelector(tagName)
65+
const successfulHit = directHit || nestedHit
66+
67+
if (successfulHit) {
68+
domHunter.disconnect()
69+
deliverCapturedElements(Array.from(document.querySelectorAll(tagName)))
70+
return
71+
}
72+
}
73+
}
74+
})
75+
76+
domHunter.observe(document.documentElement, {childList: true, subtree: true})
77+
})
78+
}
79+
80+
const immediateFinds = Array.from(document.querySelectorAll(tagName))
81+
82+
if (immediateFinds.length > 0) {
83+
return makeViewportMonitor(immediateFinds)
84+
}
85+
86+
const delayedFinds = await makeElementHunter()
87+
return makeViewportMonitor(delayedFinds)
88+
}
5289

5390
const strategies: Record<string, Strategy> = {
5491
ready: () => ready,

0 commit comments

Comments
 (0)