Skip to content

UnderlineNav performance optimizations #7823

@talum

Description

@talum

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:

  1. 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

  1. 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).

  2. 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.

  3. Batch the layout-dependent state updatesStackState, 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

  1. Go to a Conversation page on a PR
  2. Throttle CPU
  3. Resize the window
  4. Observe lag

Version

v38.21.0

Browser

No response

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions