Skip to content

[select] Avoid re-rendering scroll arrows on position updates - #6196

Open
Birzool wants to merge 2 commits into
mui:masterfrom
Birzool:select-scroll-arrow-subscriptions
Open

Birzool wants to merge 2 commits into
mui:masterfrom
Birzool:select-scroll-arrow-subscriptions

Conversation

@Birzool

@Birzool Birzool commented Nov 6, 2026 •

Copy link
Copy Markdown

Select.ScrollUpArrow and Select.ScrollDownArrow re-render on things they don't draw:

  • They read SelectPositionerContext, whose value changes with every position update, but they only use side and two refs that never change.
  • They subscribe to openMethod, which changes on every open and close, but they only need to know whether it is touch while the arrow is visible.

When the list doesn't overflow, the arrows stay hidden, so these renders can't change anything.

Reproduction

The new tests in SelectScrollArrow.test.tsx render a Select once, with keepMounted arrows and a counting render, then open and close it. They set alignItemWithTrigger={false}, so side, the one position value the arrow draws, stays the same.

On master, opening alone takes each arrow from 4 renders to the counts below, with Strict Mode rendering twice. With this PR the count stays at 4 through the open and the close. These are React 19 numbers; on React 18 the baseline is 6 and it stays at 6.

Hidden arrow Renders on master Renders with this PR
List that fits, opened with a mouse 12 4
List that overflows, opened by touch 16 4

Changes

  • A small SelectScrollArrowContext holds side and the two refs. Its value only changes with side. The refs move there, because the arrows are their only readers.
  • New scrollUpArrowShown and scrollDownArrowShown selectors compute the visibility, so the arrow re-renders when the result changes, and not on each openMethod change.

There is no API change. The cost is one more provider per Select.Positioner.

Evidence

The counts below come from a production build of an app on Base UI 1.8.0, with an equivalent edit applied to the published files, not from this branch. A script ran the same interactions on each build. The counter takes the re-renders of the arrows that changed no DOM or landed in a hidden node:

Build SelectScrollArrow re-renders
Without this patch (3 builds) 78, 78, 78
With this patch (2 builds) 0, 0

Some of these builds also carried an unrelated Base UI edit. I only quote the component's own row, because whole-run totals vary too much between builds to show a change this small.

Tests

  • The render-count tests above fail on master. They also fail when the arrow subscribes to openMethod again, or when its context value changes with the position.
  • A new mouse versus touch test opens a list that overflows both ways. Both arrows show after a mouse open and stay hidden after a touch open. It passes on master too, as this behavior doesn't change, and fails when either selector drops its touch check.
  • src/select passed in JSDOM before the last commit, which changes only this test file; that file passes in JSDOM on React 18 and 19.
  • In Chromium on macOS, src/select passed once and failed twice on this commit. Each failure was only dismisses the popover with one outside press after reopening an aligned select, on an act() warning from PopoverPositioner. That test isn't changed here and fails the same way without the patch's sources (1 of 2 runs). An earlier Chromium run of the suite on Linux, before the tests were added, passed.
  • prettier --check and eslint --max-warnings 0 pass on the changed files. For types I ran tsc -p packages/react/tsconfig.json, not pnpm typescript, which needs more memory than my 2 GB job limit.

The arrows read SelectPositionerContext, whose value changes with every position update, and subscribed to openMethod. Give them a small context of what they use and subscribe to the computed visibility.
@pkg-pr-new

pkg-pr-new Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

commit: 94a9bb0

@code-infra-dashboard

code-infra-dashboard Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Bundle size

Bundle Parsed size Gzip size
@base-ui/react 🔺+258B(+0.05%) 🔺+50B(+0.03%)

Details of bundle changes

Performance

No regressions in 36 benchmarks · ran 3m 25s · details


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

@netlify

netlify Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for base-ui ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit 94a9bb0
🔍 Latest deploy log https://app.netlify.com/projects/base-ui/deploys/6aede8a6350dfbf2dde7a573
😎 Deploy Preview https://deploy-preview-6196--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.

A mouse press opens the popup in one frame, and the popup reads its scroller in the next. React 18 commits the open only as act exits, so one act never reached that second frame: the mouse test failed, and the touch tests passed without the arrows' visibility being computed. Give the second frame its own act.
@zannager zannager added the component: select Changes related to the select component. label Nov 10, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component: select Changes related to the select component.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants