nextjs · performance · tutorial

Next.js App Router Slow First Click? Fix the 1-5s Delay

First navigation stalls 1-5s on App Router while later ones fly. Why prefetch misses and the loading.tsx fix. BoringPush notes.

Two wagons hauling wheat behind teams of oxen, Rhodesia, before 1890
Two wagons hauling wheat behind teams of oxen, Rhodesia, before 1890 · Photo: Frank and Frances Carpenter Collection, public domain, via Wikimedia Commons

First click crawls, second click flies

Delay on first navigation, instant after: 1–5s between clicking a navbar link and anything happening, on small pages, in dev AND production. Then instant forever. The useRouter is so slow thread reports the same for programmatic push()/replace(), and the slow-network thread nails the UX cost: no URL change, no spinner, three seconds of dead UI — users call it broken, and they're right.

Two mechanisms stack. First, App Router prefetches linked routes on hover/viewport — but useRouter.push() destinations and below-the-fold links often arrive cold, so the click pays a full server render.

Second, on serverless that render can include an 800ms–2.5s cold boot before your code even runs. First click eats both; later clicks eat neither.

The fix, in order of impact

1. Prefetch what matters. <Link> prefetches by default in the viewport — keep it, and add prefetch={true} to the 3–5 links users actually click (nav, pricing, dashboard). Measure in DevTools with Slow 3G throttling, exactly like the slow-network thread did.

2. Show something in 0ms. A route-level loading.tsx skeleton turns "dead for 3 seconds" into "skeleton for 3 seconds" — the difference between broken-feeling and fast-feeling with zero data changes:

// app/dashboard/loading.tsx
export default function Loading() {
  return (
    <div className="animate-pulse space-y-3 p-6" aria-hidden="true">
      <div className="h-8 w-1/3 rounded bg-stone-200" />
      <div className="h-4 w-2/3 rounded bg-stone-100" />
      <div className="h-4 w-1/2 rounded bg-stone-100" />
    </div>
  );
}

3. Stop navigating for state. If you're calling router.replace() just to update ?tab= or ?page=, switch to window.history.pushState + local state — several threads confirm it drops update latency from seconds to instant, because no navigation happens at all.

4. Kill the cold-start half. Prefetch + skeletons fix perception; a persistent server fixes the rest. This is the BoringPush shape — one Node process on a $5 VPS via PM2, SQLite at 0.1ms next to it, so first render never waits on a lambda boot or a 35ms cloud-database hop. Ship from anywhere on this Next.js stack and the first click costs a warm render, not a cold start plus a cold database.

Prove it with Slow 3G before and after

Don't ship the fix on faith — measure the exact complaint from the threads:

  1. Open DevTools → Network → throttle to Slow 3G, disable cache off (keep it on — you want real conditions).
  2. Hard-load the app, click the main nav link, count seconds until paint. Write it down: that's your "before."
  3. Add prefetch={true} to nav links + a loading.tsx on the two heaviest routes, re-run. Typical before/after from these threads: 3–5s of dead UI → instant skeleton + 1–2s content.
  4. Now compare warm vs cold server: hit the same route after 15 idle minutes on serverless vs PM2. The gap is your cold-start tax — usually 0.8–2.5s, and it never appears in localhost testing, which is why teams discover it from user complaints.
Step What you feel on Slow 3G
Before: no prefetch, no skeleton, cold lambda 3–5s dead click
After prefetch + skeleton Instant feedback, 1–2s content
Plus persistent server (PM2 + local SQLite) Skeleton flashes, content lands in ~1s

What prefetch doesn't cover (so you don't chase ghosts)

Three cases prefetch can't save, straight from the threads: routes behind authentication (prefetch skips authed content by design — your dashboard first click will always be colder than marketing pages), useRouter navigations with dynamic search params (prefetch keys on the route, not your param combinations), and anything after 15 idle minutes on serverless (the prefetch itself goes cold). For the first two, skeletons + loading.tsx are the entire fix — stop tuning what you should be masking.

For the third, only a warm process helps, which is exactly why the VPS shape exists. Measure each case separately and you'll stop filing them all under "router slow."

FAQ

Is App Router just slower than Pages Router?

Cold starts are slower; warm navigations are comparable or faster with prefetch. Most "App Router is slow" reports are missing prefetch + missing loading states, not a slower router.

Does prefetch={true} hurt bandwidth bills?

Marginally — prefetches are small RSC payloads. On Vercel it counts toward edge requests, but a handful of nav links is noise next to image/API traffic.

Link preloads on hover and viewport entry; push() starts cold at click time. Same destination, different head start.

loading.tsx shows but data still takes seconds — now what?

Skeleton bought you perception; now cut the fetch: cache the slow hop (unstable_cache or route-level revalidate), or move the data next to the server — local SQLite answers in 0.1ms.

Will a VPS really fix my first click?

The cold-boot half, yes: PM2 keeps Node warm, so first render skips 800ms–2.5s of lambda boot. The prefetch half still needs the three fixes above — do both.

+ all the boring stuff (analytics, auth, SQLite ↔ Supabase, full marketplace, webhooks)

The Agent-First Shipping Kit

Command from your phone.

Built so autonomous AI agents build what you want without breaking what's already paying you.

$199

Get BoringPush

One-time payment · Lifetime updates · Unlimited projects.

Keep reading