Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
df64c00
more page layout optimizing
mattcosta7 Dec 17, 2025
a1fafe2
organization
mattcosta7 Dec 17, 2025
e9f14e7
Add changeset
mattcosta7 Dec 17, 2025
7ff9f17
Update changeset
mattcosta7 Dec 17, 2025
38705c0
Update changeset with accurate changes
mattcosta7 Dec 17, 2025
164e57f
snapshots
mattcosta7 Dec 17, 2025
a28253a
update test
mattcosta7 Dec 17, 2025
fc284bf
resize
mattcosta7 Dec 17, 2025
b569c7a
Apply suggestion from @mattcosta7
mattcosta7 Dec 17, 2025
f8ecc3b
remove unused test
mattcosta7 Dec 17, 2025
f679bf2
perf: disable transition on drag handle during drag for instant feedback
mattcosta7 Dec 17, 2025
1cbbda0
Merge branch 'main' into second-optimization-pass-for-page-layout
mattcosta7 Dec 17, 2025
b30a9dd
update snapshots
mattcosta7 Dec 17, 2025
b232aa1
perf: add contain-intrinsic-size for content-visibility optimization
mattcosta7 Dec 17, 2025
ea160a9
gr snapshots
mattcosta7 Dec 17, 2025
19a05e4
Refine PageLayout performance optimizations: improve resize handling …
Copilot Dec 26, 2025
79430bb
Merge branch 'main' into second-optimization-pass-for-page-layout
mattcosta7 Dec 26, 2025
77daf97
Update packages/react/src/PageLayout/PageLayout.tsx
mattcosta7 Dec 26, 2025
a4f5c3e
Update packages/react/src/PageLayout/PageLayout.tsx
mattcosta7 Dec 26, 2025
56db8bd
Update packages/react/src/PageLayout/PageLayout.tsx
mattcosta7 Dec 26, 2025
d127f79
Update packages/react/src/PageLayout/PageLayout.tsx
mattcosta7 Dec 26, 2025
fd649b4
Update packages/react/src/PageLayout/PageLayout.tsx
mattcosta7 Dec 26, 2025
64dbf83
Update packages/react/src/PageLayout/PageLayout.tsx
mattcosta7 Dec 26, 2025
ea61584
Remove will-change: width and use actual element height in PageLayout…
Copilot Dec 29, 2025
4fb8c0b
Merge branch 'main' into second-optimization-pass-for-page-layout
mattcosta7 Dec 29, 2025
7fb56ef
Refactor PageLayout drag/resize optimizations to use CSS attribute se…
Copilot Dec 29, 2025
5c0533c
Debounce window resize containment cleanup in PageLayout (#7386)
Copilot Dec 29, 2025
b1068f2
Update packages/react/src/PageLayout/usePaneWidth.ts
mattcosta7 Dec 29, 2025
a8a43ae
Update .changeset/optimized-page-layout-resize.md
mattcosta7 Dec 29, 2025
f78cdb1
Apply suggestions from code review
mattcosta7 Dec 29, 2025
ad5b3c7
[WIP] WIP address feedback on drag/resize performance in PageLayout (…
Copilot Dec 29, 2025
5009206
[WIP] Update drag/resize performance in PageLayout based on feedback …
Copilot Dec 29, 2025
08207c4
Merge branch 'main' into second-optimization-pass-for-page-layout
mattcosta7 Dec 30, 2025
020eed7
Merge branch 'main' into second-optimization-pass-for-page-layout
mattcosta7 Dec 31, 2025
545af79
refactor(PageLayout): use contentWrapperRef for drag containment
mattcosta7 Dec 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/optimized-page-layout-resize.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@primer/react": patch
---

PageLayout: Optimize drag/resize performance with inline styles and new optimizations

**Refactored:**
- Use direct attribute selectors (`.Pane[data-dragging='true']`) instead of descendant selectors for CSS containment (O(1) vs O(n) selector matching)
- Extract optimization utilities to `paneUtils.ts`
- Apply drag handle visual feedback via inline styles and CSS variables

**Added:**
- `content-visibility: auto` during drag/resize to skip off-screen content rendering
- rAF throttle for drag updates (one update per frame, latest position wins)
- Containment during window resize (parity with drag)

These changes improve style recalculation performance on large DOMs (100k+ nodes) by eliminating descendant selector traversal.
87 changes: 23 additions & 64 deletions packages/react/src/PageLayout/PageLayout.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -381,26 +381,6 @@
}
}

/**
* OPTIMIZATION: Aggressive containment during drag for ContentWrapper
* data-dragging is set on PageLayoutContent by JavaScript
* This avoids expensive :has() selectors
*/
.PageLayoutContent[data-dragging='true'] .ContentWrapper {
/* Add paint containment during drag - safe since user can't interact */
contain: layout style paint;

/* Disable interactions */
pointer-events: none;

/* Disable transitions to prevent expensive recalculations */
transition: none;

/* Force compositor layer for hardware acceleration */
will-change: width;
transform: translateZ(0);
}

.Content {
width: 100%;

Expand All @@ -427,16 +407,6 @@
}
}

/**
* OPTIMIZATION: Freeze content layout during resize drag
* This prevents expensive recalculations of large content areas
* while keeping content visible (just frozen in place)
*/
.PageLayoutContent[data-dragging='true'] .Content {
/* Full containment (without size) - isolate from layout recalculations */
contain: layout style paint;
}

.PaneWrapper {
display: flex;
width: 100%;
Expand Down Expand Up @@ -630,27 +600,6 @@
}
}

/**
* OPTIMIZATION: Performance enhancements for Pane during drag
* data-dragging is set on PageLayoutContent by JavaScript
* This avoids expensive :has() selectors
*/
.PageLayoutContent[data-dragging='true'] .Pane {
/* Full containment - isolate from layout recalculations */
contain: layout style paint;

/* Disable interactions during drag */
pointer-events: none;

/* Disable transitions during drag */
transition: none;

/* Force hardware acceleration */
will-change: width, transform;
transform: translateZ(0);
backface-visibility: hidden;
}

.PaneHorizontalDivider {
&:where([data-position='start']) {
/* stylelint-disable-next-line primer/spacing */
Expand Down Expand Up @@ -756,26 +705,36 @@
position: absolute;
inset: 0 -2px;
cursor: col-resize;
background-color: transparent;
transition-delay: 0.1s;

/**
* OPTIMIZATION: Prevent touch scrolling and text selection during drag
* This is done in CSS because it needs to be set before any pointer events
*/
/* Prevent touch scrolling and text selection during drag */
touch-action: none;
user-select: none;
}

.DraggableHandle:hover {
background-color: var(--bgColor-neutral-muted);
.DraggableHandle::before {
content: '';
position: absolute;
inset: 0;
/* stylelint-disable-next-line primer/colors */
background-color: var(--draggable-handle--bg-color, var(--bgColor-neutral-muted));
opacity: var(--draggable-handle--drag-opacity, 0);
transition: var(--draggable-handle--transition, opacity 150ms ease); /* compositor-friendly, disabled during drag */
border-radius: inherit; /* optional if you need rounded corners */
}

.DraggableHandle[data-dragging='true'] {
background-color: var(--bgColor-accent-emphasis);
cursor: col-resize;
/* Hover effect */
.DraggableHandle:hover::before {
opacity: 1;
}

.DraggableHandle[data-dragging='true']:hover {
background-color: var(--bgColor-accent-emphasis);
/**
* OPTIMIZATION: CSS containment during drag/resize
* Direct attribute selectors are O(1) - only the attributed element is invalidated
* (Unlike descendant selectors which require O(n) traversal)
*/
.Pane[data-dragging='true'],
.ContentWrapper[data-dragging='true'] {
contain: layout style paint;
pointer-events: none;
content-visibility: auto;
}
59 changes: 42 additions & 17 deletions packages/react/src/PageLayout/PageLayout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,8 @@ describe('PageLayout', async () => {
)

const placeholder = await screen.findByText('Pane')
const pane = placeholder.parentNode
const initialWidth = (pane as HTMLElement).style.getPropertyValue('--pane-width')
const pane = placeholder.parentNode as HTMLElement | null
const initialWidth = pane?.style.getPropertyValue('--pane-width')
const divider = await screen.findByRole('slider')

// Moving divider should resize pane.
Expand All @@ -176,11 +176,11 @@ describe('PageLayout', async () => {
fireEvent.keyDown(divider, {key: 'ArrowRight'})
fireEvent.keyDown(divider, {key: 'ArrowRight'})

const finalWidth = (pane as HTMLElement).style.getPropertyValue('--pane-width')
const finalWidth = pane?.style.getPropertyValue('--pane-width')
expect(finalWidth).not.toEqual(initialWidth)
})

it('should set data-dragging attribute during pointer drag', async () => {
it('should set optimization styles during pointer drag', async () => {
const {container} = render(
<PageLayout>
<PageLayout.Pane resizable>
Expand All @@ -192,22 +192,21 @@ describe('PageLayout', async () => {
</PageLayout>,
)

const content = container.querySelector('[class*="PageLayoutContent"]')
const contentWrapper = container.querySelector<HTMLElement>('[class*="ContentWrapper"]')
const divider = await screen.findByRole('slider')

// Before drag - no data-dragging attribute
expect(content).not.toHaveAttribute('data-dragging')
expect(contentWrapper).not.toHaveAttribute('data-dragging')

// Start drag
// Start drag - optimization attribute is set
fireEvent.pointerDown(divider, {clientX: 300, clientY: 200, pointerId: 1})
expect(content).toHaveAttribute('data-dragging', 'true')

// End drag - pointer capture lost ends the drag and removes attribute
expect(contentWrapper).toHaveAttribute('data-dragging', 'true')
// End drag - pointer capture lost ends the drag and removes optimization attribute
fireEvent.lostPointerCapture(divider, {pointerId: 1})
expect(content).not.toHaveAttribute('data-dragging')
expect(contentWrapper).not.toHaveAttribute('data-dragging')
})

it('should set data-dragging attribute during keyboard resize', async () => {
it('should set optimization styles during keyboard resize', async () => {
const {container} = render(
<PageLayout>
<PageLayout.Pane resizable>
Expand All @@ -219,20 +218,46 @@ describe('PageLayout', async () => {
</PageLayout>,
)

const content = container.querySelector('[class*="PageLayoutContent"]')
const contentWrapper = container.querySelector<HTMLElement>('[class*="ContentWrapper"]')
const divider = await screen.findByRole('slider')

// Before interaction - no data-dragging attribute
expect(content).not.toHaveAttribute('data-dragging')
expect(contentWrapper).not.toHaveAttribute('data-dragging')

// Start keyboard resize (focus first)
fireEvent.focus(divider)
fireEvent.keyDown(divider, {key: 'ArrowRight'})
expect(content).toHaveAttribute('data-dragging', 'true')
expect(contentWrapper).toHaveAttribute('data-dragging', 'true')

// End keyboard resize - removes attribute
// End keyboard resize - removes optimization attribute
fireEvent.keyUp(divider, {key: 'ArrowRight'})
expect(content).not.toHaveAttribute('data-dragging')
expect(contentWrapper).not.toHaveAttribute('data-dragging')
})

it('should not add will-change during drag', async () => {
const {container} = render(
<PageLayout>
<PageLayout.Pane resizable>
<Placeholder height={320} label="Pane" />
</PageLayout.Pane>
<PageLayout.Content>
<Placeholder height={640} label="Content" />
</PageLayout.Content>
</PageLayout>,
)

const pane = container.querySelector<HTMLElement>('[class*="Pane"][data-resizable]')
const divider = await screen.findByRole('slider')

// Before drag - no will-change
expect(pane!.style.willChange).toBe('')

// Start drag - will-change should still not be set (removed optimization)
fireEvent.pointerDown(divider, {clientX: 300, clientY: 200, pointerId: 1})
expect(pane!.style.willChange).toBe('')
// End drag - will-change remains unset
fireEvent.lostPointerCapture(divider, {pointerId: 1})
expect(pane!.style.willChange).toBe('')
})
})

Expand Down
Loading
Loading