Skip to content

perf(Layer): a closing layer reads its animations only when its closed state declares an exit - #10368

Merged
cixzhang merged 1 commit into
mainfrom
fix/overlay-churn-layouts
Nov 7, 2026
Merged

cixzhang merged 1 commit into
mainfrom
fix/overlay-churn-layouts

Conversation

@cixzhang

@cixzhang cixzhang commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #10247

User impact

Every close of an anchored layer (Popover, DropdownMenu, Tooltip, HoverCard, ContextMenu, Selector lists, …) laid out the whole page one extra time. A page that opens and closes such layers often paid for each close twice. The nightly perf scene overlay-churn (150 opens and closes) went from 752 to 852 layouts per interaction when the trigger's anchor name started following the layer's shown state, and the open regression issue (#10247) tracks it.

Problem and solution fit

  • Why the extra layout: when a layer hides, the runtime takes its trigger's anchor name away once the layer stops painting. To know whether the layer still paints (a close fade), it called layer.getAnimations({subtree: true}) right after hidePopover(). That call updates style and layout. It ran between the hide's own writes and React's commit, so the page laid out once inside the close task and again in the frame that paints the close.
  • Why it matters: architecture:react-component-runtime INV18 keeps layout reads to the ones a record requires, in one batched pass per change, never between writes. This read sat between writes on every close, and it was needed only for a layer that fades out.
  • The fix: a hidden popover keeps showing only while its closed state holds display: through a discrete display transition (transition-behavior: allow-discrete), or through a keyframe animation that the close starts, which can animate display. Without either, the browser gives it display: none as it hides, and its CSS animations stop. The close takes its values from the closed state's style, so the runtime reads that style while the layer is still closed, at the start of show() and before the show writes anything. There the read lays nothing out. At the close, only a layer whose closed state declares such an exit has its animations read, exactly as before.
  • Why not read the hidden layer's display after the hide: measured on the same base, that read raised overlay-churn to 944 (samples 872 to 988), above main's 804. Like the animation read, it updates style between the writes of the close.

Expected behavior and authority

  • architecture:react-component-runtime INV18: layout is read as little as possible, and never between writes. A forced read on every close does not meet it.
  • architecture:layer-runtime INV18: only a shown layer's trigger is an anchor. This PR makes its fade clause consistent with react-component-runtime INV18. A layer counts as shown until the CSS transitions or keyframe animations that its closed state declares end; an animation a script starts is not tracked. That clause is the only one that needed the read on every close. TopNavMegaMenu's panel declares a discrete display transition, so its close still reads the fade and keeps the name until the fade ends.
  • Regression history: fix(Layer): a closed layer's trigger is no CSS anchor #9963 added the read (exitAnimations in releaseAnchor).

Smallest restoration

useLayer.tsx:

  • show() reads the closed layer's computed style before its writes: closeTransitionKeepsShown (transition-property, transition-behavior, transition-duration, transition-delay) and its animation-name. A layer styled with an instant open and a fading close (transitionDuration: {default: '200ms', ':popover-open': '0s'}) counts, because its closed state declares the fade.
  • After showPopover(), it reads the open state's animation-name. A keyframe animation that only the closed state declares starts at the close and can animate display, so it counts. One that both states declare is the entrance (layerAnimations), and the hide stops it, so it does not.
  • releaseAnchor reads the layer's animations only when one of those counts. Otherwise it takes the trigger's anchor name away at once, in the same task as the hide, as it already did when it found no animation. Our own hide() and the browser's own hide (toggle) both use it.

docs/architecture/layer-runtime.md: INV18's fade sentence now reads "A layer that animates out through a CSS transition or keyframe animation it declares for its closed state counts as shown until those animations end; an animation a script starts is not tracked." The verification table gains an INV18 row for the closed-state exits.

Evidence

overlay-churn interactionLayouts, from the repository's perf workflow (perf-nightly.yml, dispatched, 5 samples each, Chromium on the standard runner):

Build Median Samples
Before #9963 754 750, 766, 754, 752, 760
#9963 850 850, 852, 850, 850, 852
main at 6943968 (this PR's base) 800 800, 802, 802, 800, 800
This PR on 6943968 709 703, 703, 713, 715, 709
  • Reproduction before the change: a Chromium trace of 10 cycles on main shows one forced layout inside exitAnimations on every Popover and menu close (19 of 20 closes), then the frame's own layout.
  • Result after the change: the same trace shows no forced layout from the animation read. Each close lays out once, in its frame. The one forced layout left in 20 closes comes from hidePopover() itself, as before fix(Layer): a closed layer's trigger is no CSS anchor #9963.
  • Representative unchanged path: the other perf scenes are unchanged on the same runs (comments-panel 2, table 8, tree-list 5, markdown 1 interaction layouts). The Dialog in the same scene has no anchor and is unchanged.
  • Regression tests in useLayer.test.tsx: "reads no animations when nothing keeps the hidden layer shown" spies on getAnimations for both our hide and the browser's own hide. "reads the animations of a layer whose closed state runs a keyframe animation" covers the keyframe exit. "reads the close transition from the closed state's style" covers an instant open with a fading close. The fade cases declare TopNavMegaMenu's discrete display transition and still keep the name until the fade ends.

The nightly's 924 on the second night was run-to-run variation, not a second change: a dispatched run on the same commit measured 832.

Writing rubric results

Over docs/architecture/layer-runtime.md:

R1 narration: none; the new INV18 sentence and the verification row state what the runtime does, in the present tense.
R2 research: none in the record; the measurements are in this description.
R3 evidence: the new verification row names the unit test file, not a pull request.
R4 rejected alternatives: none.
R5 change references: none in the record.
R6 open questions: none.

Scope

  • One defect is restored.
  • Separable contradictory or unsettled tagalongs were removed or split.
  • No new public API, visual representation, default, or interaction model is hidden under the bug-fix label. The record edit is the INV18 consistency change that the fix needs, and nothing else.
  • Public text and artifacts contain no internal Meta context.

Testing

  • vitest run --project ui packages/core/src/{Layer,Popover,DropdownMenu,ContextMenu,Tooltip,HoverCard,TopNav}: 52 files, 1488 tests pass. Menu.a11y.test.tsx timed out once (30 s) under machine load and passes on a re-run.
  • check:knowledge and check:changesets pass.
  • Perf: ci:run-perf on this PR, and the dispatched runs above.
  • Real browser: TooltipClosedTriggerAnchor.interaction.browser.spec.ts covers TopNavMegaMenu's panel staying placed through its close fade in Chromium.

@vercel

vercel Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Nov 7, 2026 12:24am UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Nov 6, 2026
@astracat-bot

astracat-bot Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 7, 2026, 12:38 AM UTC

@cixzhang cixzhang added the ci:run-perf Run the perf scenes (perf-nightly.yml) on this pull request label Nov 6, 2026
@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

🔎 Preview · 59740bb

Storybook · Sandbox · built from this PR's head

Modified components

Component Size LoC Visual · A11y · RTL
LayerProvider 4.1 kB (0) · 20 B CSS (0) — ✓ · ✓ · ✗
lab/CommentPin 53.2 kB (+231 B) · 1.2 kB CSS (0) — — · — · —
TabList 40.7 kB (+227 B) · 6.4 kB CSS (0) — — · — · —
112 more components
Component Size LoC Visual · A11y · RTL
ChatComposerInput 67.0 kB (+223 B) · 10.7 kB CSS (0) — — · — · —
ChatComposerTokenElement 67.0 kB (+223 B) · 10.7 kB CSS (0) — — · — · —
Typeahead 75.1 kB (+221 B) · 12.2 kB CSS (0) — — · — · —
DateInput 177.3 kB (+220 B) · 16.5 kB CSS (0) — — · — · —
FileInput 64.8 kB (+219 B) · 10.7 kB CSS (0) — — · — · —
Thumbnail 56.7 kB (+219 B) · 11.0 kB CSS (0) — — · — · —
richtext/RichTextEditor 292.0 kB (+219 B) · 2.6 kB CSS (0) — — · — · —
SegmentedControl 90.1 kB (+218 B) · 6.8 kB CSS (0) — — · — · —
BreadcrumbItem 76.6 kB (+217 B) · 11.6 kB CSS (0) — — · — · —
Avatar 40.0 kB (+215 B) · 5.5 kB CSS (0) — — · — · —
DateTimeInput 199.7 kB (+215 B) · 18.2 kB CSS (0) — — · — · —
BaseTypeahead 62.6 kB (+214 B) · 10.4 kB CSS (0) — — · — · —
FieldLabel 47.7 kB (+214 B) · 5.6 kB CSS (0) — — · — · —
InputGroup 52.6 kB (+213 B) · 7.8 kB CSS (0) — — · — · —
SideNavItem 62.3 kB (+213 B) · 10.5 kB CSS (0) — — · — · —
CodeBlock 67.1 kB (+212 B) · 10.7 kB CSS (0) — — · — · —
TopNav 64.1 kB (+212 B) · 10.3 kB CSS (0) — — · — · —
richtext/RichTextView 271.8 kB (+212 B) · 2.3 kB CSS (0) — — · — · —
RadioList 52.3 kB (+211 B) · 7.2 kB CSS (0) — — · — · —
RadioListItem 63.2 kB (+211 B) · 9.8 kB CSS (0) — — · — · —
lab/SelectableCardGroup 60.2 kB (+211 B) · 162 B CSS (0) — — · — · —
lab/TourStep 86.4 kB (+211 B) · 590 B CSS (0) — — · — · —
Button 52.3 kB (+210 B) · 9.4 kB CSS (0) — — · — · —
Carousel 61.2 kB (+210 B) · 10.4 kB CSS (0) — — · — · —
MobileNavToggle 52.6 kB (+210 B) · 9.4 kB CSS (0) — — · — · —
Toolbar 40.0 kB (+210 B) · 7.9 kB CSS (0) — — · — · —
CheckboxList 54.8 kB (+209 B) · 8.0 kB CSS (0) — — · — · —
IconButton 52.3 kB (+209 B) · 9.4 kB CSS (0) — — · — · —
ListItem 65.4 kB (+209 B) · 11.0 kB CSS (0) — — · — · —
Stepper 54.0 kB (+209 B) · 9.6 kB CSS (0) — — · — · —
TopNavMegaMenu 61.6 kB (+209 B) · 11.3 kB CSS (0) — — · — · —
ChatLayout 55.6 kB (+208 B) · 9.9 kB CSS (0) — — · — · —
Lightbox 103.6 kB (+208 B) · 9.9 kB CSS (0) — — · — · —
TextArea 58.4 kB (+208 B) · 9.2 kB CSS (0) — — · — · —
Switch 96.9 kB (+207 B) · 9.9 kB CSS (0) — — · — · —
TextInput 63.4 kB (+207 B) · 10.9 kB CSS (0) — — · — · —
ChatLayoutScrollButton 52.8 kB (+206 B) · 9.6 kB CSS (0) — — · — · —
CheckboxInput 60.3 kB (+206 B) · 9.1 kB CSS (0) — — · — · —
SideNavCollapseButton 53.1 kB (+206 B) · 9.5 kB CSS (0) — — · — · —
SideNavHeading 67.4 kB (+206 B) · 10.7 kB CSS (0) — — · — · —
ToggleButton 53.2 kB (+206 B) · 9.5 kB CSS (0) — — · — · —
ChatComposer 68.9 kB (+205 B) · 11.3 kB CSS (0) — — · — · —
ChatSendButton 52.7 kB (+205 B) · 9.4 kB CSS (0) — — · — · —
ChatDictationButton 52.9 kB (+204 B) · 9.5 kB CSS (0) — — · — · —
Field 51.0 kB (+204 B) · 7.2 kB CSS (0) — — · — · —
Popover 80.2 kB (+204 B) · 13.5 kB CSS (0) — — · — · —
SideNav 82.0 kB (+204 B) · 10.8 kB CSS (0) — — · — · —
Table 101.1 kB (+204 B) · 17.3 kB CSS (0) — — · — · —
DialogHeader 55.1 kB (+203 B) · 10.9 kB CSS (0) — — · — · —
MobileNav 62.4 kB (+203 B) · 10.1 kB CSS (0) — — · — · —
lab/ChatEmojiPicker 98.2 kB (+203 B) · 773 B CSS (0) — — · — · —
AvatarGroupOverflow 45.0 kB (+202 B) · 5.5 kB CSS (0) — — · — · —
Link 49.3 kB (+202 B) · 7.0 kB CSS (0) — — · — · —
TabMenu 63.7 kB (+202 B) · 10.5 kB CSS (0) — — · — · —
Banner 54.4 kB (+201 B) · 9.9 kB CSS (0) — — · — · —
Pagination 104.7 kB (+201 B) · 17.3 kB CSS (0) — — · — · —
PowerSearch 277.1 kB (+201 B) · 23.2 kB CSS (0) — — · — · —
lab/DiffViewer 78.7 kB (+201 B) · 2.1 kB CSS (0) — — · — · —
AppShell 82.2 kB (+200 B) · 12.9 kB CSS (0) — — · — · —
AvatarGroup 42.7 kB (+200 B) · 5.6 kB CSS (0) — — · — · —
DateRangeInput 87.3 kB (+200 B) · 12.2 kB CSS (0) — — · — · —
DrawerHeader 55.3 kB (+200 B) · 12.3 kB CSS (0) — — · — · —
TableSelectionToolbar 61.8 kB (+200 B) · 12.5 kB CSS (0) — — · — · —
charts/Chart 180.9 kB (+199 B) · 494 B CSS (0) — — · — · —
Calendar 63.3 kB (+199 B) · 10.0 kB CSS (0) — — · — · —
TableHeaderCell 92.5 kB (+199 B) · 16.8 kB CSS (0) — — · — · —
TopNavMenu 66.5 kB (+199 B) · 10.6 kB CSS (0) — — · — · —
AlertDialog 96.5 kB (+198 B) · 13.8 kB CSS (0) — — · — · —
InfoTip 43.8 kB (+198 B) · 5.3 kB CSS (0) — — · — · —
lab/EmojiPicker 81.9 kB (+198 B) · 751 B CSS (0) — — · — · —
MultiSelector 89.8 kB (+197 B) · 16.4 kB CSS (0) — — · — · —
Selector 94.5 kB (+196 B) · 17.0 kB CSS (0) — — · — · —
TableCell 92.9 kB (+195 B) · 17.1 kB CSS (0) — — · — · —
lab/Schedule 100.3 kB (+195 B) · 3.9 kB CSS (0) — — · — · —
MoreMenu 88.2 kB (+194 B) · 16.8 kB CSS (0) — — · — · —
Slider 102.2 kB (+194 B) · 8.7 kB CSS (0) — — · — · —
charts/ChartTooltip 158.3 kB (+193 B) · 423 B CSS (0) — — · — · —
TopNavHeading 66.8 kB (+193 B) · 10.5 kB CSS (0) — — · — · —
lab/TransferListSelector 98.6 kB (+193 B) · 1.9 kB CSS (0) — — · — · —
richtext/RichTextEditorToolbar 245.7 kB (+193 B) · 454 B CSS (0) — — · — · —
ComplexSelector 66.4 kB (+192 B) · 11.5 kB CSS (0) — — · — · —
DropdownMenuSubMenu 65.2 kB (+192 B) · 9.4 kB CSS (0) — — · — · —
TimeInput 154.0 kB (+192 B) · 15.6 kB CSS (0) — — · — · —
Tokenizer 86.4 kB (+192 B) · 18.2 kB CSS (0) — — · — · —
lab/CommentThread 303.2 kB (+192 B) · 2.4 kB CSS (0) — — · — · —
ContextMenu 91.7 kB (+190 B) · 16.7 kB CSS (0) — — · — · —
HoverCard 24.9 kB (+190 B) · 4.3 kB CSS (0) — — · — · —
CheckboxListItem 66.3 kB (+187 B) · 11.0 kB CSS (0) — — · — · —
DropdownMenu 87.9 kB (+187 B) · 16.8 kB CSS (0) — — · — · —
Tooltip 26.4 kB (+187 B) · 4.3 kB CSS (0) — — · — · —
Markdown 163.5 kB (+186 B) · 19.6 kB CSS (0) — — · — · —
NumberInput 67.4 kB (+185 B) · 11.2 kB CSS (0) — — · — · —
lab/TransferList 91.6 kB (+185 B) · 1.7 kB CSS (0) — — · — · —
lab/ChatReactionBar 99.3 kB (+184 B) · 1.1 kB CSS (0) — — · — · —
lab/ListInput 96.9 kB (+181 B) · 1.6 kB CSS (0) — — · — · —
Toast 89.9 kB (+178 B) · 9.7 kB CSS (0) — — · — · —
lab/Comments 308.3 kB (+177 B) · 2.5 kB CSS (0) — — · — · —
StatusDot 28.1 kB (+173 B) · 5.3 kB CSS (0) — — · — · —
lab/CommentComposer 139.5 kB (+155 B) · 1.9 kB CSS (0) — — · — · —
CommandPaletteInput 27.9 kB (+1 B) · 7.5 kB CSS (0) — — · — · —
Text 7.6 kB (+1 B) · 5.4 kB CSS (0) — — · — · —
charts/ChartLegend 35.8 kB (0) · 224 B CSS (0) — — · — · —
ChatMessageList 23.9 kB (0) · 7.0 kB CSS (0) — — · — · —
ChatToolCalls 27.8 kB (0) · 8.3 kB CSS (0) — — · — · —
CommandPalette 67.2 kB (0) · 12.4 kB CSS (0) — — · — · —
Heading 8.2 kB (0) · 5.4 kB CSS (0) — — · — · —
ProgressBar 11.5 kB (0) · 6.2 kB CSS (0) — — · — · —
Spinner 20.5 kB (0) · 6.1 kB CSS (0) — — · — · —
Timer 9.7 kB (0) · 5.9 kB CSS (0) — — · — · —
Timestamp 23.2 kB (−1 B) · 9.7 kB CSS (0) — — · — · —
layer — — — · ✓ · —
Layer — — ✓ · ✓ · —

Package size: @astryxdesign/core 6.84 MB packed (+1.6 kB); no new dependencies.

API changes: none in this pull request

Committed API report behind this build: cli 19 rows, core 24 rows; run pnpm api:report.

Touched components checked; add ci:run-visual, ci:run-a11y or ci:run-rtl to run the full suites.


Generated by PR Enrichment workflow | View full report

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes: skipping the animation read for layers that cannot keep painting removes the extra layout on close, but the decision is read from the wrong style in one case.

  • useLayer.tsx:1506 and :1659 read keepsPaintingAfterHide from the open-state style while the browser runs a close transition from the closed-state style, so a layer whose discrete close transition is declared only on its closed state loses its trigger's anchor name while it still fades (architecture:layer-runtime INV18); decide after the hide from getComputedStyle(layer).display !== 'none', a non-geometry read, and add a test for that styling.

Two smaller notes are inline: the JSDoc and changeset say overlay alone keeps a hidden layer shown, which it does not, and one added comment restates the code. CI has not run on this draft head yet.

[Automated review]

Comment thread packages/core/src/Layer/useLayer.tsx Resolved
Comment thread packages/core/src/Layer/useLayer.tsx Resolved
Comment thread packages/core/src/Layer/useLayer.tsx Resolved
@cixzhang
cixzhang marked this pull request as ready for review November 6, 2026 20:46
@cixzhang
cixzhang marked this pull request as draft November 6, 2026 20:48
@cixzhang
cixzhang force-pushed the fix/overlay-churn-layouts branch from ebf360e to fe50cdb Compare November 6, 2026 21:55
@cixzhang cixzhang changed the title perf(Layer): a closing layer reads its animations only when a discrete transition can keep it shown perf(Layer): a closing layer reads its animations only when a discrete display transition can keep it shown Nov 6, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved pending CI: CI has not run on this head yet, and it starts when the pull request is marked ready for review.

A layer with no discrete display close transition now skips the animation read on close, and the earlier problem is fixed: a fade declared only on the closed state keeps its trigger's anchor name until it ends, because the answer is read from the closed state's style at the show. One non-blocking comment-wording suggestion is inline.

[Automated review]

Comment thread packages/core/src/Layer/useLayer.tsx Resolved
@cixzhang
cixzhang marked this pull request as ready for review November 6, 2026 22:19
@cixzhang
cixzhang marked this pull request as draft November 6, 2026 23:03
@cixzhang
cixzhang force-pushed the fix/overlay-churn-layouts branch from fe50cdb to ee087b2 Compare November 6, 2026 23:04

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes: skipping the animation read for a layer with no exit removes the extra layout on close, but the new check misses two kinds of exit that keep a hidden layer painting.

  • Miss: closeKeepsShown (useLayer.tsx:538-561) counts only a discrete display transition, so a layer that animates out through a closed-state keyframe animation or a script animation of display (both keep a hidden popover painting in Chromium) loses its trigger's anchor at the hide and jumps away from the trigger mid-fade, against architecture:layer-runtime INV18 ("A layer that animates out counts as shown until its animations end"); keep the read for those exits, or narrow INV18 through a record change an engineering owner approves.
  • Miss: the changeset's "A layer that fades out … still keeps its trigger's anchor name until the fade ends" and the JSDoc's "and so does every animation in it" (useLayer.tsx:532-534) are false for those exits, so scope them to a closed state that declares a discrete display transition unless the code fix makes them true.

Non-blocking: the comment at useLayer.tsx:1234 restates the ref's name. CI has not run on this draft head yet.

[Automated review]

Comment thread packages/core/src/Layer/useLayer.tsx Resolved
@cixzhang
cixzhang marked this pull request as ready for review November 6, 2026 23:23
…d state declares an exit

Closing an anchored layer read the layer's animations right after
hidePopover(), to keep the trigger's anchor name through a close fade.
That read updates style and layout between the close's writes, so every
close laid out the page twice.

A hidden popover keeps showing only while its closed state holds
display: through a discrete display transition, or through a keyframe
animation the close starts. The runtime reads that from the closed
state's style while the layer is still closed, and reads the animations
at the close only for such a layer. layer-runtime INV18's fade clause now
says so, consistent with react-component-runtime INV18; an animation a
script starts is not tracked. overlay-churn's interaction layouts go from
800 to 709 on CI.
@cixzhang
cixzhang marked this pull request as draft November 7, 2026 00:16
@cixzhang cixzhang changed the title perf(Layer): a closing layer reads its animations only when a discrete display transition can keep it shown perf(Layer): a closing layer reads its animations only when its closed state declares an exit Nov 7, 2026
@cixzhang
cixzhang force-pushed the fix/overlay-churn-layouts branch from ee087b2 to 59740bb Compare November 7, 2026 00:16
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 7, 2026
@cixzhang

cixzhang commented Nov 7, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec 59740bb

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Nov 7, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved pending CI: CI has not run on this head yet, and it starts when the pull request is marked ready for review.

Closing a layer with no declared exit no longer reads its animations, a closed-state keyframe exit keeps its trigger's anchor through the fade again, and the INV18 rewording that stops tracking script-started animations carries an engineering owner's approval for this head. Two non-blocking suggestions:

  • The changeset says the runtime reads when the closed state declares "a keyframe animation"; entrance keyframes declared for both states are not read, so "a keyframe animation that only its closed state declares" is accurate.
  • The JSDoc of closeTransitionKeepsShown (first sentence) and of animationNames restates the function names; keep only the reason.

[Automated review]

'@astryxdesign/core': patch
---

[perf] Closing a Popover, menu or other anchored layer no longer lays out the page an extra time. The layer runtime reads a closing layer's animations only when the layer's closed state declares a discrete `display` transition or a keyframe animation. It reads that style while the layer is still closed. Such a layer, as TopNavMegaMenu's panel is, still keeps its trigger's anchor name until its fade ends. An animation a script starts is not tracked.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: the entrance keyframes of Popover and the menus (layerAnimations) are declared on the closed state too, and the runtime does not read them. "…or a keyframe animation that only its closed state declares" matches what the code reads.

}

/**
* Whether the closed state's transition keeps `layer` shown after it hides.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking (INV29): this sentence restates closeTransitionKeepsShown, and the JSDoc at line 562 restates animationNames. Drop both; the two sentences after this one keep the reason.

@cixzhang
cixzhang marked this pull request as ready for review November 7, 2026 00:49
@cixzhang
cixzhang merged commit 2816569 into main Nov 7, 2026
122 checks passed
@github-actions
github-actions Bot deleted the fix/overlay-churn-layouts branch November 7, 2026 06:03

This branch was successfully deployed

1 active deployment
Preview — 59740bb4 Deployed Nov 7, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:run-perf Run the perf scenes (perf-nightly.yml) on this pull request CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Nightly perf regression

1 participant