From a3326a029611ce91ffeca9187898d2acd7cb20d7 Mon Sep 17 00:00:00 2001 From: Jakob Wennberg Date: Wed, 26 Aug 2026 14:01:37 +0200 Subject: [PATCH] perf(nav): hover-intent prefetch for the dashboard nav + 30 s client router cache (#1943) * perf(nav): prefetch dashboard routes on hover intent, not on viewport DashboardNav renders ~45 links, all dynamic routes with a loading boundary, so Next prefetched every one of them as soon as the nav mounted. Each prefetch is a full request through the auth proxy (Supabase Auth round trip, active-company RPC, MFA check) whose only payload is the shared loading skeleton; prod logs showed 1,000 to 1,300 such hits per nav route per day. NavLink wraps next/link with prefetch={false} and an explicit router.prefetch on mouseenter/focus/touchstart (Link's own hover prefetch is disabled together with viewport prefetch, so the warm-up must be explicit). The link to the current route and non-routes are skipped (shouldWarmNavRoute, tested). A source-shape test pins that DashboardNav has no bare next/link left. Cost: an un-hovered click shows the route's loading skeleton ~50-100 ms later than before; the skeleton is all a dynamic prefetch ever carried. Co-Authored-By: Claude Fable 5 * perf(router): keep dynamic routes in the client router cache for 30 s experimental.staleTimes.dynamic was 0: every back/forward or repeated nav click re-requested the RSC payload through the auth proxy. 30 s covers the click-around pattern the customer described while the 16 router.refresh() sites after mutations keep the pages that must not go stale fresh. Separate commit so it can be dropped on its own if stale numbers are reported. Co-Authored-By: Claude Fable 5 --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 --- components/dashboard/DashboardNav.tsx | 42 +++++++------- components/dashboard/NavLink.tsx | 57 +++++++++++++++++++ .../dashboard/__tests__/nav-prefetch.test.ts | 33 +++++++++++ components/dashboard/nav-prefetch.ts | 12 ++++ next.config.ts | 8 +++ 5 files changed, 131 insertions(+), 21 deletions(-) create mode 100644 components/dashboard/NavLink.tsx create mode 100644 components/dashboard/__tests__/nav-prefetch.test.ts create mode 100644 components/dashboard/nav-prefetch.ts diff --git a/components/dashboard/DashboardNav.tsx b/components/dashboard/DashboardNav.tsx index b8bdcce7..5de1e539 100644 --- a/components/dashboard/DashboardNav.tsx +++ b/components/dashboard/DashboardNav.tsx @@ -1,7 +1,7 @@ 'use client' import { useEffect, useState, useRef } from 'react' -import Link from 'next/link' +import { NavLink } from './NavLink' import Image from 'next/image' import { usePathname, useRouter } from 'next/navigation' import { useTranslations } from 'next-intl' @@ -605,9 +605,9 @@ export default function DashboardNav({ companyName: _companyName, entityType, pa : 'text-muted-foreground/40 cursor-not-allowed', ) return enabled ? ( - + {content} - + ) : (
+ {inner} - + ) : (
{inner} @@ -712,7 +712,7 @@ export default function DashboardNav({ companyName: _companyName, entityType, pa : 'justify-between pl-5 pr-3', )} > - - +