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

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions