Repository navigation
perf(ui): overlays in #overlay-root and an attribute scroll lock (no whole-page restyle) - #217
Merged
Merged
Conversation
Every overlay (BaseDialog, Select, the accounts tools panel and row menu, tooltips, drawers, toasts: 31 <Teleport to="body"> in 25 files) inserted its content among <body>'s own children, next to the anchors Vue appends at mount time. <body> carries the positional (:first/:last-child) and sibling (space-y / divide-y ~) invalidation flags, so each open or close restyled the whole document: 130-290 ms event->paint on /admin/accounts. index.html now ends <body> with an empty #overlay-root and every Teleport targets it, so opening an overlay only touches that container (~0.5 ms forced recalc instead of ~170 ms). The container has no styles and makes no stacking context: overlays keep their z-order. The login agreement dialog selectors (body > div.z-[140]) follow the overlay into #overlay-root; :where() keeps them at their old specificity. The test setup recreates #overlay-root before each test, since many specs reset document.body. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With overlays out of <body>'s child list, dialogs still cost ~170 ms per open/close on /admin/accounts: the scroll lock toggled body.modal-open (BaseDialog) or an inline body overflow (announcement list and popup), and a class or style change on <body> invalidates every element (the console theme matches [class~=]/[class*=] and :not([style]) widely). The lock is now an attribute no other selector uses, which restyles only <body>: body[data-modal-open] (dialogStack, still reference counted) and body[data-announcement-open] (announcement bell and popup, same set/clear points as before). Same overflow: hidden; the scroll-lock specs assert the attribute instead of the class / inline style. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
BackupView (2) and SubscriptionsView (1) spell the tag in lowercase, so the first pass missed them; every teleport now targets #overlay-root. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Summary
Opening or closing any overlay (more-actions panel, row menus, dialogs, confirm boxes, the announcement popup) re-styled the whole page and took 130-300 ms from click to paint. There were two causes:
<Teleport to="body">inserts its content among<body>'s own children.<body>carries positional and sibling-combinator invalidation flags, from Tailwindspace-y/divide-yand:first-child/:last-childvariants that no stylesheet can avoid, so each insertion invalidates the whole document. The fix:index.htmlgets a fixed<div id="overlay-root">after#app, and all 34 teleports target it. The test setup recreates the container for each spec.body.modal-open, and announcements set an inline overflow. A class or inline-style change on<body>re-styles the page; a data attribute does not. The lock is nowbody[data-modal-open](reference-counted indialogStack.ts) andbody[data-announcement-open], and both map tooverflow: hiddeninstyle.css.The console theme's selectors that assumed overlays are direct children of body now go through
#overlay-root, with the same specificity.Measured
Production builds, real clicks, Event Timing click → next paint, medians:
The 130-290 ms long tasks on every overlay are gone.
Visual and behaviour
12 overlays, light and dark: identical computed styles on all 1514 overlay elements, identical rects and z-order, and 0 px diff inside the overlays. Scroll lock and focus trap behave the same.
Tests
vue-tsc --noEmitexit 0.🤖 Generated with Claude Code