Prepare cards ahead of the scroll, and insert them in batches
DOM work cannot leave the main thread -- a worker has no document, and nodes are not transferable -- so a card can never be compiled elsewhere. It can be compiled *earlier*. The page already prefetches the next page's JSON and warms its thumbnails; this does the same for the cards those items will need, building and binding them while the browser is idle and handing them over ready when the reader arrives. Alongside that, three things that keep a frame from being held too long, which is what smoothness actually reduces to when the work has nowhere else to go: Mounting and filling are drained against a 4ms budget rather than all at once. Filling in one pass was a regression I introduced with the fling deferral: it moved the stall from during the fling to the end of it. A frame's mounts go into a DocumentFragment and enter the document in one insertion, with filling afterwards so nothing reads layout mid-insert. The pool is topped up with card shells during idle, so a mount during a scroll is a rebind and not a construction: cards built mid-scroll fell from 24 to 5 across profiling runs. Also reverted, with its numbers kept in a comment: narrowing the overscan during a fling. It reads like an obvious saving and measures as the opposite -- a tight window makes cards leave and re-enter it, and mount churn went from 88 to 155 with blocked time from 3.6s to 4.3s. What this does not do is reduce total blocked time. Roughly three quarters of it is browser style, layout, paint and decode for each card shown, which no amount of scheduling removes. The deep stalls get shorter; the thread stays busy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QBDkEXP4htyXTCZUwMLphd
This commit is contained in:
@@ -229,13 +229,19 @@ def main():
|
||||
() => (App.virtualGrid.stats && App.virtualGrid.stats()) || null
|
||||
""")
|
||||
if stats:
|
||||
total = (stats.get("built", 0) + stats.get("recycled", 0)) or 1
|
||||
rate = 100 * stats.get("recycled", 0) // total
|
||||
print(f"\npool: {stats.get('recycled', 0)} recycled / {total} mounts "
|
||||
f"({rate}% hit rate), {stats.get('pooled', 0)} idle in pool")
|
||||
# A hit rate near zero means cards are still being built per mount,
|
||||
# so none of the checks above actually exercised a recycled card.
|
||||
c.ok("cards are actually being recycled", rate >= 50, f"{rate}% hit rate")
|
||||
built = stats.get("built", 0)
|
||||
recycled = stats.get("recycled", 0)
|
||||
readymade = stats.get("prepared", 0)
|
||||
total = (built + recycled + readymade) or 1
|
||||
reused = 100 * (recycled + readymade) // total
|
||||
print(f"\nmounts: {total} -- {built} built, {recycled} recycled, "
|
||||
f"{readymade} prepared ahead; {stats.get('pooled', 0)} idle in pool, "
|
||||
f"{stats.get('readied', 0)} still readied")
|
||||
# If almost everything is still built per mount, none of the checks
|
||||
# above actually exercised a reused card.
|
||||
c.ok("cards are reused rather than rebuilt", reused >= 50, f"{reused}% reused")
|
||||
c.ok("some cards were prepared before they were needed", readymade > 0,
|
||||
"prepare-ahead never served a mount")
|
||||
|
||||
browser.close()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user