Skip to content

Conversation

@mattcosta7
Copy link
Contributor

@mattcosta7 mattcosta7 commented Nov 28, 2025

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 PageLayout component 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:

  • Introduced advanced CSS containment (contain, content-visibility, will-change, etc.) and optimized styles for .PageLayoutContent, .ContentWrapper, .PaneWrapper, .Pane, and .DraggableHandle to 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]
  • Removed global body[data-page-layout-dragging='true'] styles and replaced them with local, component-scoped optimizations, further reducing unnecessary style recalculations.

Drag Handling and Accessibility:

  • Refactored drag logic to use pointer events and local drag state per handle, eliminating global listeners and attributes. Drag operations are now handled via onPointerDown, onPointerMove, and related events, with ARIA attributes updated directly on the DOM for better accessibility and performance. [1] [2] [3]
  • Improved keyboard accessibility by handling arrow key events for resizing and updating ARIA live attributes without triggering re-renders. [1] [2]

Responsive Constraints and State Management:

  • Added a shared ResizeObserver and custom hook to efficiently track viewport width changes and update pane constraints responsively, ensuring correct min/max pane widths across breakpoints. [1] [2]
  • Updated pane width calculation and storage logic for accuracy and performance, including direct manipulation of CSS variables via useIsomorphicLayoutEffect and robust localStorage handling. [1] [2] [3] [4]

Changelog:

  • Added a patch changelog entry for @primer/react describing the improved drag performance for PageLayout.

Closes #

PageLayout Performance + Accessibility Upgrade

Improve PageLayout pane drag performance with pointer capture, GPU-accelerated transforms, and resilience against visual regressions.


Summary

  • Reworked pane resizing to avoid React re-renders, keep ARIA attributes synchronized, and support both pointer and keyboard interactions.
  • Added Storybook performance diagnostics that measure FPS under progressively heavier DOM loads.
  • Tuned CSS containment to maintain 60 FPS drag while preventing the content region from shrinking during resize.

Key Changes

  1. Viewport subscription & constraint caching

    • Added useViewportWidth() backed by a shared ResizeObserver and useSyncExternalStore.
    • Cached --pane-max-width-diff via maxPaneWidthRef, reducing getComputedStyle() calls to a single check per viewport change.
  2. Pane width management

    • Pointer drags update --pane-width through refs and useIsomorphicLayoutEffect, skipping React state for instant feedback.
    • Keyboard interactions reuse the same clamping logic, persist the resulting width to localStorage, and update slider ARIA attributes without re-rendering.
  3. Accessibility improvements

    • updateAriaValues() writes aria-valuemin, aria-valuemax, aria-valuenow, and aria-valuetext directly to the slider handle.
    • New Storybook Keyboard & ARIA Test exposes live ARIA values to simplify screen-reader QA.
  4. Storybook performance coverage

    • Added PageLayout.performance.stories.tsx with scenarios ranging from ~100 to ~5,000 DOM nodes, plus an Empty Baseline diagnostic.
    • FPS monitoring is performed via direct DOM updates (no React overhead) to give accurate live feedback during drags.
  5. CSS containment & compositor tuning (PageLayout.module.css)

    • Relaxed .Content drag containment to contain: layout style paint;, avoiding width collapse while isolating layout and paint work.
    • .PageLayoutContent:has(.DraggableHandle[data-dragging='true']) temporarily disables transitions, pointer hits, and applies will-change/translateZ on .ContentWrapper & .Pane, ensuring hardware acceleration only when dragging.
    • Added persistent contain: layout style and content-visibility: auto to .Content/.Pane, letting browsers skip off-screen work without breaking tooltips/modals.
    • The handle now enforces touch-action: none, user-select: none, and keeps cursor: col-resize without depending on the old body[data-page-layout-dragging] override.

Performance Impact (Storybook measurements)

Scenario main This Branch
Baseline drag (~100 elements) ~60 FPS ~60 +FPS (no regressions)
Medium Content (~3k nodes) ~35–45 FPS 55–60+ FPS
Heavy Content (~5k nodes) Drops below 25 FPS 50–60+ FPS with throttling
Keyboard resize Occasional lag, no persistence Smooth, width persisted on keyup
Responsive max width Recomputed every drag frame Recomputed only on viewport change

Trade-off: We intentionally rely on direct DOM calls (CSS variables, ARIA attributes) in hot paths to guarantee 60 FPS behavior under load.


Accessibility Notes

  • Slider ARIA values stay accurate for both pointer and keyboard interactions.
  • Drag state toggles data-dragging only when needed, preventing CSS flicker while preserving focus cues.
  • Storybook’s diagnostics make it easy to verify announcements with VoiceOver/NVDA.

--

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

  • Patch release
  • Minor release
  • Major release; if selected, include a written rollout or migration plan
  • None; if selected, include a brief description as to why

Testing & Reviewing

Merge checklist

@changeset-bot
Copy link

changeset-bot bot commented Nov 28, 2025

🦋 Changeset detected

Latest commit: e193e7d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@primer/react Patch

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

@mattcosta7 mattcosta7 changed the title Mc/copilot/sub pr 7248 Improve more PageLayout pane drag performance with pointer capture and GPU-accelerated transforms Nov 28, 2025
@github-actions github-actions bot added the integration-tests: recommended This change needs to be tested for breaking changes. See https://arc.net/l/quote/tdmpakpm label Nov 28, 2025
@github-actions
Copy link
Contributor

👋 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 integration-tests: skipped manually label to skip these checks.

@github-actions github-actions bot requested a deployment to storybook-preview-7251 November 28, 2025 22:18 Abandoned
@github-actions github-actions bot temporarily deployed to storybook-preview-7251 November 28, 2025 22:28 Inactive
@mattcosta7 mattcosta7 requested a review from Copilot November 29, 2025 04:21
@mattcosta7 mattcosta7 assigned mattcosta7 and unassigned Copilot Nov 29, 2025
Copilot finished reviewing on behalf of mattcosta7 November 29, 2025 04:25

This comment was marked as outdated.

Copy link
Contributor

Copilot AI left a 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.

@github-actions github-actions bot temporarily deployed to storybook-preview-7251 December 1, 2025 22:53 Inactive
@github-actions github-actions bot requested a deployment to storybook-preview-7251 December 1, 2025 22:59 Abandoned
@github-actions github-actions bot temporarily deployed to storybook-preview-7251 December 1, 2025 23:10 Inactive
Copy link
Contributor

Copilot AI left a 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.

Comment on lines +876 to +878
// 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.
Copy link

Copilot AI Dec 2, 2025

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:

  1. paneWidth (React state) may differ from currentWidthRef.current (actual width)
  2. 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
  3. 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.

Suggested change
// 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)

Copilot uses AI. Check for mistakes.
// 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`)
Copy link

Copilot AI Dec 2, 2025

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)
  }
})
Suggested change
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)
}

Copilot uses AI. Check for mistakes.
@mattcosta7 mattcosta7 added this pull request to the merge queue Dec 3, 2025
Merged via the queue into main with commit 12e12f5 Dec 3, 2025
49 checks passed
@mattcosta7 mattcosta7 deleted the mc/copilot/sub-pr-7248 branch December 3, 2025 02:43
@primer primer bot mentioned this pull request Dec 3, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integration-tests: failing Changes in this PR cause breaking changes in gh/gh

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants