Skip to content

perf: lazy-load mobile menu content to reduce RSC payload#17661

Merged
wackerow merged 6 commits into
devfrom
perf/lazy-load-mobile-menu
Mar 10, 2026
Merged

perf: lazy-load mobile menu content to reduce RSC payload#17661
wackerow merged 6 commits into
devfrom
perf/lazy-load-mobile-menu

Conversation

@pettinarip
Copy link
Copy Markdown
Member

@pettinarip pettinarip commented Feb 25, 2026

Summary

  • Move mobile menu content from server component to client-side lazy-loaded component
  • This avoids shipping all navigation translations and language display info in the initial RSC payload since the mobile menu is hidden behind a click
  • Saves ~82KB from the initial HTML payload by deferring navigation data until the user actually opens the mobile menu

Changes

  • MobileMenuContent.tsx: New client component with the actual menu content
  • MobileMenuClient.tsx: Uses React.lazy() to load content on first open
  • MobileMenu/index.tsx: Simplified to just render MobileMenuClient
  • LvlAccordion.tsx: Remove async keyword that caused repeated Suspense
  • useLanguagesDisplayInfo.ts: New client hook to generate language info

Test plan

  • Verify mobile menu still opens and functions correctly
  • Verify language selection works properly
  • Verify navigation links work in mobile menu
  • Check that initial page load doesn't include mobile menu data in RSC payload
  • Test accordion navigation levels open/close correctly

Move mobile menu content from server component to client-side lazy-loaded
component. This avoids shipping all navigation translations and language
display info in the initial RSC payload since the mobile menu is hidden
behind a click.

Changes:
- Add MobileMenuContent.tsx: Client component with the actual menu content
- MobileMenuClient.tsx: Uses React.lazy() to load content on first open
- MobileMenu/index.tsx: Simplified to just render MobileMenuClient
- LvlAccordion.tsx: Remove async keyword that caused repeated Suspense
- useLanguagesDisplayInfo.ts: Client hook to generate language info

This saves ~82KB from the initial HTML payload by deferring navigation
data until the user actually opens the mobile menu.
@netlify
Copy link
Copy Markdown

netlify Bot commented Feb 25, 2026

Deploy Preview for ethereumorg ready!

Name Link
🔨 Latest commit e1337a4
🔍 Latest deploy log https://app.netlify.com/projects/ethereumorg/deploys/69a5d67dc7186f000866e695
😎 Deploy Preview https://deploy-preview-17661.ethereum.it
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
7 paths audited
Performance: 57 (🔴 down 3 from production)
Accessibility: 94 (🟢 up 1 from production)
Best Practices: 100 (no change from production)
SEO: 99 (no change from production)
PWA: 59 (no change from production)
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions github-actions Bot added the tooling 🔧 Changes related to tooling of the project label Feb 25, 2026
Copy link
Copy Markdown
Collaborator

@myelinated-wackerow myelinated-wackerow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: perf: lazy-load mobile menu content to reduce RSC payload

The core optimization is sound -- deferring mobile menu content from the RSC payload to a client-side lazy load is a real win for mobile users. The menu is behind a hamburger click, so there's no reason to SSR it. A few suggestions below.

1. Missing error boundary around lazy load (medium)

MobileMenuClient.tsx:68-72 -- If the dynamic import fails (flaky mobile network, CDN hiccup), React.lazy throws and propagates upward with no recovery path. This is a realistic failure mode on mobile.

Suggestion: wrap the Suspense in an error boundary with a retry mechanism so the menu isn't permanently broken on a transient network failure.

2. useLanguagesDisplayInfo should memoize its result (low-medium)

src/hooks/useLanguagesDisplayInfo.ts:20-31 -- This creates ~100 Intl.DisplayNames / Intl.NumberFormat objects (4 per locale x 25 locales) on every render. Intl constructors are among the most expensive synchronous JS operations. While re-renders should be infrequent thanks to PersistentPanel staying mounted, any parent context change would recompute everything.

Suggestion:

return useMemo(
  () => (FILTERED_LOCALES as Lang[]).map((localeOption) =>
    localeToDisplayInfo(localeOption, locale as Lang, t)
  ),
  [locale, t]
)

3. hasBeenOpened state is redundant with PersistentPanel (low)

MobileMenuClient.tsx:39-43 -- PersistentPanel already returns null until first open (its internal isMounted state), which means children never render until then. React.lazy only triggers the import when the component actually renders in the tree, not when JSX elements are created. So the hasBeenOpened guard is defense-in-depth rather than strictly necessary. Not harmful, but worth a comment noting the intent if kept.

4. Consider prefetching the chunk during idle time (low)

On slow 3G, the first menu open now has to download + parse the JS chunk before anything renders. The skeleton helps with perceived latency, but a requestIdleCallback-based prefetch (or <link rel="prefetch">) could eliminate the download penalty while keeping the RSC payload savings.

5. Skeleton doesn't match actual menu layout (nitpick)

MobileMenuClient.tsx:21-28 -- The skeleton shows 5 uniform rectangles, but the real menu has a header, tabbed nav sections, and a 3-column footer bar. This causes a visual jump on first open. Matching the skeleton to the real structure would smooth the transition.


Overall this is a well-executed optimization -- the tradeoff (slightly slower first open for significantly smaller initial payload) is the right call for a mobile menu. The error boundary is the main thing I'd want addressed before merge.


Reviewed by Claude (claude-opus-4-6)

Copy link
Copy Markdown
Member

@wackerow wackerow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pettinarip Thanks for this! Looks great overall, left the claude review above.. Curious your take on the Error boundary. The skeleton looks fine imo. Approving as-is, but ofc feel free to update if you think it's worth it.

@pettinarip
Copy link
Copy Markdown
Member Author

@wackerow thanks for the review. I've applied most of them. Also, improved a bit the skeleton.

Copy link
Copy Markdown
Member

@wackerow wackerow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pettinarip Looks good.. left a couple notes, but nothing that needs to block. Will leave it up to you but think this is good to go for now

const lazyImport = () => import("./MobileMenuContent")
const MobileMenuContent = React.lazy(lazyImport)

function MobileMenuContentSkeleton() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, looks great now 🔥

Comment on lines +75 to +78
if (typeof window.requestIdleCallback === "function") {
const id = window.requestIdleCallback(() => lazyImport())
return () => window.cancelIdleCallback(id)
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid

Comment thread src/components/Nav/MobileMenu/MobileMenuClient.tsx Outdated
Comment thread src/components/Nav/MobileMenu/MobileMenuClient.tsx Outdated
@github-actions github-actions Bot added content 🖋️ This involves copy additions or edits translation 🌍 This is related to our Translation Program labels Mar 2, 2026
@wackerow wackerow merged commit a291e35 into dev Mar 10, 2026
6 checks passed
@wackerow wackerow deleted the perf/lazy-load-mobile-menu branch March 10, 2026 22:47
@wackerow wackerow mentioned this pull request Mar 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

content 🖋️ This involves copy additions or edits tooling 🔧 Changes related to tooling of the project translation 🌍 This is related to our Translation Program

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants