Description
👋🏼 Related to https://github.com/github/pull-requests/issues/24571 and #7801, I've been investigating more performance optimizations. I think there are a few more things we could do with UnderlineNav to help.
Diagnosis
After a window resize, UnderlineNav measures its own width to determine which tabs fit vs. overflow into a "more" menu. This triggers a synchronous setState() that re-renders the component and all its children — 385 child component renders (ExtendedLink ×133, ForwardRef ×128, Link ×124) producing a 20.5ms React render on each resize event.
Because UnderlineNav mutates the DOM during this render, four downstream components detect layout changes and each call setState() synchronously mid-commit, cascading one after another:
StackState.setState() → 2. PullRequestHeader.setState() → 3. Overlay.setState() → 4. ActionMenu.setState()
This cascade produces 14 separate React renders totaling ~138ms of JS, followed by two full-page layouts (~117ms, touching all 1,285 DOM nodes), for a total post-resize cost of ~750ms.
Suggested Fixes
-
Debounce the resize handler — wrap UnderlineNav's measurement logic in requestAnimationFrame (or a short debounce) so it fires once per frame, not once per resize event (7 dispatches observed in this trace).
-
Memoize child components — the Link/ExtendedLink/ForwardRef children should be wrapped in React.memo(). Their props almost certainly don't change on resize, yet they account for 385 unnecessary re-renders.
-
Batch the layout-dependent state updates — StackState, PullRequestHeader, Overlay, and ActionMenu all independently read layout and call setState(), creating the cascade. Consolidating their measurements into a single shared ResizeObserver callback (or a useLayoutEffect that batches all reads before any writes) would collapse 4 cascading renders into 1. -- This one I'm less sure about and is a suggestion from Copilot. the other ones sound reasonable to me.
Steps to reproduce
- Go to a Conversation page on a PR
- Throttle CPU
- Resize the window
- Observe lag
Version
v38.21.0
Browser
No response
Description
👋🏼 Related to https://github.com/github/pull-requests/issues/24571 and #7801, I've been investigating more performance optimizations. I think there are a few more things we could do with
UnderlineNavto help.Diagnosis
After a window resize,
UnderlineNavmeasures its own width to determine which tabs fit vs. overflow into a "more" menu. This triggers a synchronoussetState()that re-renders the component and all its children — 385 child component renders (ExtendedLink×133,ForwardRef×128,Link×124) producing a 20.5ms React render on each resize event.Because
UnderlineNavmutates the DOM during this render, four downstream components detect layout changes and each callsetState()synchronously mid-commit, cascading one after another:StackState.setState()→ 2.PullRequestHeader.setState()→ 3.Overlay.setState()→ 4.ActionMenu.setState()This cascade produces 14 separate React renders totaling ~138ms of JS, followed by two full-page layouts (~117ms, touching all 1,285 DOM nodes), for a total post-resize cost of ~750ms.
Suggested Fixes
Debounce the resize handler — wrap
UnderlineNav's measurement logic inrequestAnimationFrame(or a short debounce) so it fires once per frame, not once per resize event (7 dispatches observed in this trace).Memoize child components — the
Link/ExtendedLink/ForwardRefchildren should be wrapped inReact.memo(). Their props almost certainly don't change on resize, yet they account for 385 unnecessary re-renders.Batch the layout-dependent state updates —
StackState,PullRequestHeader,Overlay, andActionMenuall independently read layout and callsetState(), creating the cascade. Consolidating their measurements into a single sharedResizeObservercallback (or auseLayoutEffectthat batches all reads before any writes) would collapse 4 cascading renders into 1. -- This one I'm less sure about and is a suggestion from Copilot. the other ones sound reasonable to me.Steps to reproduce
Version
v38.21.0
Browser
No response