Remove the scroll listener with the capture flag it was registered with - #284
Open
reorx wants to merge 1 commit into
Open
Remove the scroll listener with the capture flag it was registered with#284reorx wants to merge 1 commit into
reorx wants to merge 1 commit into
Conversation
`destroy()` called `document.removeEventListener('scroll', onScroll)` while
the listener had been registered with `{ capture: true }`. removeEventListener
only matches a registration whose capture flag is equal, so the call was a
no-op and every destroyed renderer left its scroll listener on the document.
Each orphan keeps running `redraw(true)` — a forced, full highlight-layer
rebuild — on every scroll event in the page, so the cost accumulates with
every mount/destroy cycle (collapsing a panel, re-creating an annotator,
client-side route changes).
Adds a regression test that scrolls after destroy and asserts the painter is
not asked to redraw. It fails without the one-line change above.
Fixes recogito#283
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
|
Awesome finding! Thanks for the contribution! |
oleksandr-danylchenko
approved these changes
Aug 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #283.
The bug
base-renderer.tsregisters its scroll listener in the capture phase but removes it without the flag:removeEventListeneronly matches a registration whose capture flag is equal, so that call was a no-op: everydestroy()ed renderer left its scroll listener attached to the document. Each orphan keeps callingredraw(true)— a forced, full highlight-layer rebuild — on every scroll event in the page, so the cost accumulates over a session with every mount/destroy cycle (collapsing a panel, re-creating an annotator, client-side route changes, several annotators on one page).The change
One line, plus a note so it does not regress:
The test
test/rendering/base-renderer.test.tsdrivescreateRendererwith a stub painter/state and asserts the observable behaviour rather than the listener bookkeeping:destroy(), a scroll event must not reach the painter.Verified in both directions: the regression test fails on
main(painter.redrawis called once afterdestroy()) and passes with the fix. The control test passes either way.npx vitest runinpackages/text-annotator→ 5 passed.tsc -p tsconfig.build.jsonclean.package-lock.jsondeliberately untouched.Not included
onResize.clear()is commented out two lines below (base-renderer.ts:234), so a resize debounced just beforedestroy()can still fire afterwards and runstore.recalculatePositions()+redraw()against a destroyed painter. That one needs aclear()on the localdebouncehelper (the current implementation does not expose one), so it felt out of scope here — happy to follow up if you want it fixed too.