-
Notifications
You must be signed in to change notification settings - Fork 647
Improve PageLayout pane drag performance with pointer capture and GPU-accelerated transforms #7251
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
🦋 Changeset detectedLatest commit: e193e7d The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
👋 Hi, this pull request contains changes to the source code that github/github-ui depends on. If you are GitHub staff, test these changes with github/github-ui using the integration workflow. Or, apply the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 9 comments.
| // We intentionally skip setPaneWidth() to avoid triggering expensive React | ||
| // reconciliation with large DOM trees. The ref is the source of truth for | ||
| // subsequent drag operations. |
Copilot
AI
Dec 2, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After a pointer drag ends, the pane width state (paneWidth) is not updated with the final width value. Only currentWidthRef is kept in sync and localStorage is updated. This creates a state inconsistency where:
paneWidth(React state) may differ fromcurrentWidthRef.current(actual width)- If the component re-renders for any other reason, the effect at line 707-709 will sync
currentWidthRef.current = paneWidth, reverting to the old value - The CSS variable will be set to the old value via the layout effect at line 714-716
This will cause the pane to "snap back" to its previous width on any re-render. Either update setPaneWidth(currentWidthRef.current) in onDragEnd, or ensure the ref remains the source of truth consistently.
| // We intentionally skip setPaneWidth() to avoid triggering expensive React | |
| // reconciliation with large DOM trees. The ref is the source of truth for | |
| // subsequent drag operations. | |
| // Update React state to keep it in sync with the ref and prevent snapping back on re-render. | |
| setPaneWidth(currentWidthRef.current) |
| // the CSS variable reflects currentWidthRef (which may differ from state after drag). | ||
| // We intentionally have no deps array - we want this to run on every render. | ||
| useIsomorphicLayoutEffect(() => { | ||
| paneRef.current?.style.setProperty('--pane-width', `${currentWidthRef.current}px`) |
Copilot
AI
Dec 2, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The effect at lines 242-245 runs on every render without a dependency array, which means stableOnDrag.current and stableOnDragEnd.current are updated on every render. This is intentional to ensure the callbacks are always up-to-date, but it's unconventional.
However, the effect at line 714-716 also runs on every render to set the CSS variable, which creates a performance bottleneck. Each render will trigger DOM manipulation even if the value hasn't changed. Consider adding a guard check:
useIsomorphicLayoutEffect(() => {
const newValue = `${currentWidthRef.current}px`
const currentValue = paneRef.current?.style.getPropertyValue('--pane-width')
if (currentValue !== newValue) {
paneRef.current?.style.setProperty('--pane-width', newValue)
}
})| paneRef.current?.style.setProperty('--pane-width', `${currentWidthRef.current}px`) | |
| const newValue = `${currentWidthRef.current}px` | |
| const currentValue = paneRef.current?.style.getPropertyValue('--pane-width') | |
| if (currentValue !== newValue) { | |
| paneRef.current?.style.setProperty('--pane-width', newValue) | |
| } |
superceedes #7249
Using that branch as a base, I went a lot deeper on some optimizations, and expect this to perform better in all cases.
This pull request significantly improves the drag performance and responsiveness of the
PageLayoutcomponent in@primer/react. The changes focus on optimizing CSS containment strategies, reducing unnecessary re-renders during drag operations, and efficiently handling ARIA accessibility attributes for draggable elements. These updates result in smoother resizing, better performance for large content, and enhanced accessibility.Performance and Responsiveness Improvements:
contain,content-visibility,will-change, etc.) and optimized styles for.PageLayoutContent,.ContentWrapper,.PaneWrapper,.Pane, and.DraggableHandleto isolate layout and paint operations, skip off-screen rendering, and freeze layout during drag. This reduces layout thrashing and improves perceived performance during pane resizing. [1] [2] [3] [4] [5] [6] [7] [8]body[data-page-layout-dragging='true']styles and replaced them with local, component-scoped optimizations, further reducing unnecessary style recalculations.Drag Handling and Accessibility:
onPointerDown,onPointerMove, and related events, with ARIA attributes updated directly on the DOM for better accessibility and performance. [1] [2] [3]Responsive Constraints and State Management:
ResizeObserverand custom hook to efficiently track viewport width changes and update pane constraints responsively, ensuring correct min/max pane widths across breakpoints. [1] [2]useIsomorphicLayoutEffectand robust localStorage handling. [1] [2] [3] [4]Changelog:
@primer/reactdescribing the improved drag performance forPageLayout.Closes #
PageLayout Performance + Accessibility Upgrade
Summary
Key Changes
Viewport subscription & constraint caching
useViewportWidth()backed by a sharedResizeObserveranduseSyncExternalStore.--pane-max-width-diffviamaxPaneWidthRef, reducinggetComputedStyle()calls to a single check per viewport change.Pane width management
--pane-widththrough refs anduseIsomorphicLayoutEffect, skipping React state for instant feedback.localStorage, and update slider ARIA attributes without re-rendering.Accessibility improvements
updateAriaValues()writesaria-valuemin,aria-valuemax,aria-valuenow, andaria-valuetextdirectly to the slider handle.Keyboard & ARIA Testexposes live ARIA values to simplify screen-reader QA.Storybook performance coverage
PageLayout.performance.stories.tsxwith scenarios ranging from ~100 to ~5,000 DOM nodes, plus an Empty Baseline diagnostic.CSS containment & compositor tuning (
PageLayout.module.css).Contentdrag containment tocontain: layout style paint;, avoiding width collapse while isolating layout and paint work..PageLayoutContent:has(.DraggableHandle[data-dragging='true'])temporarily disables transitions, pointer hits, and applieswill-change/translateZon.ContentWrapper&.Pane, ensuring hardware acceleration only when dragging.contain: layout styleandcontent-visibility: autoto.Content/.Pane, letting browsers skip off-screen work without breaking tooltips/modals.touch-action: none,user-select: none, and keepscursor: col-resizewithout depending on the oldbody[data-page-layout-dragging]override.Performance Impact (Storybook measurements)
mainAccessibility Notes
data-draggingonly when needed, preventing CSS flicker while preserving focus cues.--
Videos
Before
Janky, jumpy
Screen.Recording.2025-12-01.at.5.27.38.PM.mov
After
Stays smoother, no jumping around
Screen.Recording.2025-12-01.at.5.28.41.PM.mov
Changelog
No api changes
New
n/a
Changed
Improved drag to resize performance for PageLayout resizable
Removed
n/a
Rollout strategy
Testing & Reviewing
Merge checklist