Skip to content

[useScrollLock] Reduce style recalculation with classic scrollbars#3854

Merged
atomiks merged 9 commits intomui:masterfrom
mdm317:perf/classic-scrollbar-reduce-layout-thrashing
Feb 4, 2026
Merged

[useScrollLock] Reduce style recalculation with classic scrollbars#3854
atomiks merged 9 commits intomui:masterfrom
mdm317:perf/classic-scrollbar-reduce-layout-thrashing

Conversation

@mdm317
Copy link
Contributor

@mdm317 mdm317 commented Jan 24, 2026

Current

When using classic (inset) scrollbars with scroll locking, full document recalc can occur.

[Chromium 143]

3 full document recalc
Screenshot 2026-01-24 at 11 08 39 PM

[Chromium 144]

1 full document recalc
Screenshot 2026-01-24 at 11 06 22 PM

Cause

Directly changing <html> styles causes a full document style recalculation.

Solution

When checking scrollbar gutter stable, use body by default, except in cases where overflow-y: scroll is set on <html>.

Test

  • Verified scroll locking works correctly.
    • Preview link
      • should check both classic and overlay scrollbars.

Improve

[Chromium 143]

1 full document recalc (Because we shoud set scrollbar-gutter in html)

[Chromium 144]

0 full document recalc
Screenshot 2026-01-25 at 11 14 11 AM

Limitation

Improving this behavior is not feasible for users who set overflow-y: scroll on <html>.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 24, 2026

commit: 46bbbcd

@mui-bot
Copy link

mui-bot commented Jan 24, 2026

Bundle size report

Bundle Parsed size Gzip size
@base-ui/react 🔺+104B(+0.02%) 🔺+34B(+0.03%)

Details of bundle changes


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

@netlify
Copy link

netlify bot commented Jan 24, 2026

Deploy Preview for base-ui ready!

Name Link
🔨 Latest commit 46bbbcd
🔍 Latest deploy log https://app.netlify.com/projects/base-ui/deploys/6982e28449842800087edc20
😎 Deploy Preview https://deploy-preview-3854--base-ui.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

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

overflowY: scrollContainer.style.overflowY,
};

html.style.scrollbarGutter = 'stable';
Copy link
Contributor Author

@mdm317 mdm317 Jan 25, 2026

Choose a reason for hiding this comment

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

scrollbar-gutter works as expected on the html element.
check blog css-overflow-3-spec

export default function ScrollLock() {
const [enabled, setEnabled] = React.useState(false);
const [bodyScrollY, setBodyScrollY] = React.useState(false);
const [htmlScrollY, setHtmlScrollY] = React.useState(false);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Add htmlScrollY to ensure scroll blocking works correctly when overflow-y: scroll is set on .

@mdm317 mdm317 marked this pull request as ready for review January 25, 2026 12:10
greptile-apps[bot]

This comment was marked as outdated.

@oliviertassinari oliviertassinari changed the title [useScrollLock]: reduce style recalculation with classic scrollbars [useScrollLock] Reduce style recalculation with classic scrollbars Jan 25, 2026
@atomiks
Copy link
Contributor

atomiks commented Jan 27, 2026

@greptileai

@atomiks atomiks added the package: utils Specific to the utils package. label Jan 27, 2026
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Jan 27, 2026

Greptile Overview

Greptile Summary

Improved performance of scroll locking with classic scrollbars by minimizing full document style recalculations. The optimization detects the appropriate scroll container (body vs html) and applies changes there instead of always modifying html directly, reducing recalculations from 3 to 1 (Chromium 143) or 1 to 0 (Chromium 144).

Key changes:

  • Modified supportsStableScrollbarGutter() to test scrollbar behavior on the actual scroll container (body by default, html only when it has overflow)
  • Updated preventScrollOverlayScrollbars() to save/restore overflowX and overflowY separately instead of combined overflow property
  • Added test controls for html overflow case to verify the documented limitation

The implementation correctly handles state restoration for both html and body elements.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The changes are well-targeted performance optimizations with clear benefits. The logic correctly handles state restoration for both scroll containers, the previous thread issue has been addressed, and the implementation maintains backward compatibility while reducing layout thrashing.
  • No files require special attention

Important Files Changed

Filename Overview
packages/utils/src/useScrollLock.ts Optimized scrollbar gutter detection to use body instead of html when possible, reducing full document recalculations
docs/src/app/(private)/experiments/scroll-lock.tsx Added html overflow checkbox to test edge case where optimization doesn't apply

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

overflowY: scrollContainer.style.overflowY,
};

html.style.scrollbarGutter = 'stable';
Copy link
Contributor

Choose a reason for hiding this comment

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

scrollbarGutter set on html but saved/restored on scrollContainer. When scrollContainer is body, this leaves html.style.scrollbarGutter = 'stable' permanently set.

Suggested change
html.style.scrollbarGutter = 'stable';
scrollContainer.style.scrollbarGutter = 'stable';
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/utils/src/useScrollLock.ts
Line: 42:42

Comment:
`scrollbarGutter` set on `html` but saved/restored on `scrollContainer`. When `scrollContainer` is `body`, this leaves `html.style.scrollbarGutter = 'stable'` permanently set.

```suggestion
  scrollContainer.style.scrollbarGutter = 'stable';
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've fixed it.

@atomiks
Copy link
Contributor

atomiks commented Jan 30, 2026

In the demo when Long content is unchecked, the scrollbar disappears (differing from master) - enabling scroll lock then shifts the content

@mdm317
Copy link
Contributor Author

mdm317 commented Jan 31, 2026

I missed that case — thanks for pointing it out. 😔


In the demo when Long content is unchecked, the scrollbar disappears

This difference comes from how the demo is configured:

  • In the demo(master), overflow-y: scroll is applied via a class on the html element.
  • In the demo(current), overflow-y: scroll is instead added as a class on the body.
  • Also, for testing purposes, when neither body nor html has an overflow property, i temporarily set overflow-y: visible inline style. diff

This happens in the demo(master) as well if you manually disable overflow-y: scroll on html.
So when both body and html have no overflow: scroll and the Long content is unchecked, there is no scrollbar.


enabling scroll lock then shifts the content

The layout shift issue occurs when preventScrollOverlayScrollbars is used.
When scroll lock is released, the previously set inline overflow-y: visible is not restored correctly, which results shift.

I fixed this issue in this commit.

So in the current demo, whenLong content is unchecked and neither body nor html has overflow: scroll set, the scrollbar disappears.
Unlocking scroll lock also keeps the scrollbar hidden, so the content doesn’t shift.

Copy link
Contributor

@atomiks atomiks left a comment

Choose a reason for hiding this comment

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

Nice, wasn't able to find any new issues with the latest implementation

@atomiks
Copy link
Contributor

atomiks commented Feb 4, 2026

@greptileai

@atomiks atomiks merged commit 93a3862 into mui:master Feb 4, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

package: utils Specific to the utils package. performance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants