mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-14 18:01:20 +08:00
3ca8f1018e
Ten project-switch cycles retained 0.24 MB of JS heap per cycle, monotonic and unbounded. Two causes, both in how memoized callbacks keep render scopes alive. A `useCallback` whose deps changed is a NEW closure in the current render's scope; every callback whose deps did NOT change is the closure from an earlier render, stored into that same fresh scope. That one edge chains render scope N to render scope N-1. A project switch changes projectId, activeCompPath and timelineElements at once, so each switch added a link — and each link pinned that render's lint result (~240 findings) and its element snapshot plus the cached index built from it, for the life of the session. `useLintModal` and `useDomSelection` now build their callbacks once and read render-scoped values through a ref, so no stale closure lands in a fresh scope. `groupFindings` moves to module scope and takes the findings as an argument instead of closing over them. Separately, the composition-source map was pinning a whole document per open: V8 represents a substring of 13+ characters as a SlicedString that keeps its parent alive, so a handful of `compositions/*.html` paths cut out of a 36 KB `index.html` held that document as long as the map lived. Measured on two generated fixtures, forced GC between samples, production React, same method before and after: cycles heap/cycle before after 11 0.237 MB 0.100 MB 21 0.202 MB 0.061 MB Nodes (+0) and listeners (+0) were already flat once the probe stopped holding puppeteer ElementHandles, which pin an element and its whole ancestor chain through a DevTools global handle and read as detached-DOM retention. The remaining slope is V8 tier-up, not retention: code space grows 81 KB per open over the first eight opens and 22 KB over the next eight, and the heap slope decays with it (0.100 -> 0.061) where before it did not (0.237 -> 0.202).