Skip to content

Commit 0b89a6d

Browse files
authored
Merge pull request #344 from github/fix/coalesce-scan-raf
fix: coalesce scan() into a single rAF to avoid paint delay on bulk DOM mutations
2 parents 6c6e1fa + f52fee6 commit 0b89a6d

File tree

2 files changed

+49
-10
lines changed

2 files changed

+49
-10
lines changed

src/lazy-define.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -57,26 +57,33 @@ const strategies: Record<string, Strategy> = {
5757

5858
type ElementLike = Element | Document | ShadowRoot
5959

60-
const timers = new WeakMap<ElementLike, number>()
60+
const pendingElements = new Set<ElementLike>()
61+
let scanTimer: number | null = null
62+
6163
function scan(element: ElementLike) {
62-
cancelAnimationFrame(timers.get(element) || 0)
63-
timers.set(
64-
element,
65-
requestAnimationFrame(() => {
64+
pendingElements.add(element)
65+
if (scanTimer != null) return
66+
scanTimer = requestAnimationFrame(() => {
67+
scanTimer = null
68+
const elements = new Set(pendingElements)
69+
pendingElements.clear()
70+
if (!dynamicElements.size) {
71+
return
72+
}
73+
outer: for (const el of elements) {
6674
for (const tagName of dynamicElements.keys()) {
67-
const child: Element | null =
68-
element instanceof Element && element.matches(tagName) ? element : element.querySelector(tagName)
75+
const child: Element | null = el instanceof Element && el.matches(tagName) ? el : el.querySelector(tagName)
6976
if (customElements.get(tagName) || child) {
7077
const strategyName = (child?.getAttribute('data-load-on') || 'ready') as keyof typeof strategies
7178
const strategy = strategyName in strategies ? strategies[strategyName] : strategies.ready
7279
// eslint-disable-next-line github/no-then
7380
for (const cb of dynamicElements.get(tagName) || []) strategy(tagName).then(cb)
7481
dynamicElements.delete(tagName)
75-
timers.delete(element)
82+
if (!dynamicElements.size) break outer
7683
}
7784
}
78-
})
79-
)
85+
}
86+
})
8087
}
8188

8289
let elementLoader: MutationObserver

test/lazy-define.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,38 @@ describe('lazyDefine', () => {
6868
expect(onDefine3).to.have.callCount(1)
6969
})
7070

71+
it('coalesces multiple added elements into a single rAF callback', async () => {
72+
const onDefine = spy()
73+
lazyDefine('coalesce-test-element', onDefine)
74+
75+
const rafSpy = spy(window, 'requestAnimationFrame')
76+
const callsBefore = rafSpy.callCount
77+
78+
await fixture(html`
79+
<div>
80+
<coalesce-test-element></coalesce-test-element>
81+
<coalesce-test-element></coalesce-test-element>
82+
<coalesce-test-element></coalesce-test-element>
83+
<coalesce-test-element></coalesce-test-element>
84+
<coalesce-test-element></coalesce-test-element>
85+
<coalesce-test-element></coalesce-test-element>
86+
<coalesce-test-element></coalesce-test-element>
87+
<coalesce-test-element></coalesce-test-element>
88+
<coalesce-test-element></coalesce-test-element>
89+
<coalesce-test-element></coalesce-test-element>
90+
</div>
91+
`)
92+
93+
await animationFrame()
94+
95+
const rafCallsFromScan = rafSpy.callCount - callsBefore
96+
rafSpy.restore()
97+
98+
// Should use at most a few rAF calls, not one per element
99+
expect(rafCallsFromScan).to.be.lessThan(5)
100+
expect(onDefine).to.be.callCount(1)
101+
})
102+
71103
it('lazy loads elements in shadow roots', async () => {
72104
const onDefine = spy()
73105
lazyDefine('nested-shadow-element', onDefine)

0 commit comments

Comments
 (0)