Skip to content

[test] Snapshot commit counts for benchmarked scenarios - #5709

Draft
Janpot wants to merge 2 commits into
mui:masterfrom
Janpot:render-count-snapshots
Draft

Janpot wants to merge 2 commits into
mui:masterfrom
Janpot:render-count-snapshots

Conversation

@Janpot

@Janpot Janpot commented Sep 11, 2026

Copy link
Copy Markdown
Member

Ports the scenarios from test/performance into the main test suite as inline snapshot tests. A render count change then shows up as a changed line in the PR diff, reviewed like any other code change, with no CI machinery behind it.

Context: #5403 builds a per-PR render count check out of three workflows, a merge base collector run, a cross workflow artifact trust model and a comment upsert. This is the same signal without any of that. It covers only the render count half; timing is a separate question.

The helper

createCommitPhases() in packages/react/test/commitPhases.tsx, exported from #test-utils:

const phases = createCommitPhases();
await render(phases.wrap(<Scenario />));
await phases.waitForQuiescence();
expect(phases.get()).toMatchInlineSnapshot();

It replaces three hand rolled Profiler counters already in the suite (DrawerProvider, CheckboxIndicator, useAnimationsFinished each roll their own counter, array push or vi.fn()).

waitForQuiescence() matters: mounting schedules follow up work that lands a frame or more later, so without it a snapshot records however much happened to arrive before the assertion and varies run to run.

What this measures

Profiler.onRender fires once per commit, so these are commit counts, not per component render counts. React exposes no public API for the latter. That catches effect driven re-render cascades, double commits and extra commits per keystroke. It does not catch "every item re-rendered inside one commit", the regression class behind #4964. Neither does #5403, worth saying plainly so the check is not oversold.

Sample of what gets recorded:

Scenario Commits
Checkbox, Dialog, Popover, Tooltip, Menu mount ["mount"]
Slider mount ["mount", "nested-update"]
Select mount ["mount", "update", "nested-update"]
Menu open, 20 items 11 commits
Select open, 20 options 15 commits
Combobox, type a prefix keeping all 20 items 7 commits
Combobox, type a query narrowing the list 9 commits

Choices

  • Chromium and React 19 only, via describe.skipIf(isJSDOM || reactMajor < 19). jsdom misses the layout driven commits that most of these components do. The recorded sequences are specific to React 19 scheduling, and the react-18 workflow re-runs this whole suite with react and react-dom swapped through pnpm overrides, so it has to skip them. commitPhases.test.tsx stays ungated: it asserts helper behaviour, not Base UI commit counts.
  • Co-located in each component's existing root test file.
  • 10 instances rather than the benchmark's 200 to 500. Commit counts do not scale with instance count.

Not ported

  • Mixed surface mount (app-like density) measures total mount cost across 330 components, which is a timing question.
  • The scroll area scenario. Its scrollbars measure from a ResizeObserver, so the commit count depends on machine load: stable 15/15 in isolation but failing 2 of 10 combined runs, at any settle length tried. Tabs.Indicator is left out of the tabs scenario for the same reason (8/10).

Both are called out in comments where they would otherwise look like oversights.

Verification

  • Every snapshot recorded over 10 consecutive full file runs with no variance.
  • Mutation checked: adding one extra commit to a scenario fails its snapshot.
  • Full chromium suite green, 8967 tests.
  • jsdom lane skips the new blocks rather than failing.

test/performance is untouched.

Ports the scenarios from test/performance into the main test suite as inline
snapshot tests, so a render count change shows up as a changed line in the PR
diff instead of needing CI machinery to report it.

createCommitPhases() wraps a tree in React.Profiler and records its commit
phase sequence. React fires onRender once per commit, so this counts commits,
not individual component renders. waitForQuiescence() lets follow-up work
settle first, so a snapshot means the commits a scenario settles into rather
than however many happened to land before the assertion ran.

Chromium only: jsdom misses the layout driven commits that most of these
components do. 10 instances rather than the benchmark's 200 to 500, since
commit counts do not scale with instance count.

Two scenarios are not ported. The mixed app density benchmark only measures
total mount cost, which is a timing question. The scroll area benchmark commits
a load dependent number of times because its scrollbars measure from a
ResizeObserver, failing 2 of 10 runs under load no matter how long the tree is
left to settle. Tabs is ported without Tabs.Indicator for the same reason.

The timing suite under test/performance is untouched.
@pkg-pr-new

pkg-pr-new Bot commented Sep 11, 2026

Copy link
Copy Markdown

commit: 2348b7a

@code-infra-dashboard

code-infra-dashboard Bot commented Sep 11, 2026

Copy link
Copy Markdown

Bundle size

Bundle Parsed size Gzip size
@base-ui/react 0B(0.00%) 0B(0.00%)

Details of bundle changes

Performance

Total duration: 1,161.29 ms -67.92 ms(-5.5%) | Renders: 76 (+0) | Paint: 1,845.97 ms -122.32 ms(-6.2%)

Test Duration Renders
Checkbox mount (500 instances) 86.20 ms 🔺+20.31 ms(+30.8%) 1 (+0)

14 tests within noise — details


Check out the code infra dashboard for more information about this PR.

@netlify

netlify Bot commented Sep 11, 2026

Copy link
Copy Markdown

Deploy Preview for base-ui ready!

Name Link
🔨 Latest commit 2348b7a
🔍 Latest deploy log https://app.netlify.com/projects/base-ui/deploys/6aa40ace06f0c70008ea1f90
😎 Deploy Preview https://deploy-preview-5709--base-ui.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

The recorded commit sequences are specific to React 19's scheduling, and the
react-18 CircleCI workflow re-runs this whole suite with react and react-dom
swapped through pnpm overrides, so the snapshots would fail there.

Gates the nine scenario blocks with reactMajor < 19, the idiom already used for
the React.Activity blocks in AccordionPanel and CollapsiblePanel.

commitPhases.test.tsx stays ungated. It asserts helper behaviour rather than
Base UI's internal commit counts, so it should hold on any supported React.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant