nextjs · tutorial · agents
useRouter Feels Slow? Skip It on First Paint
Router push delays on first interaction: prefetch the target, render instantly, navigate underneath. The perceived-speed pattern inside.

The button that thinks before moving
Click "View order." Nothing — one second, two seconds — then the page appears, fully formed, as if it had been ready all along. It had been: the delay was the router fetching, compiling, and hydrating the target while the user stared at a button that seemed broken.
My analytics showed the corpse: 12% of first-clicks followed by rage-clicks (same button, three times), 4% followed by exits. Users don't wait gracefully; they punish uncertainty. A two-second navigation that acknowledges instantly converts; a two-second navigation in silence bleeds.
Userouter slow fix isn't about router speed — it's about the silence between click and paint. Fill the silence and the slowness stops existing.
Silent vs acknowledged navigation
| First click | Silent (default) | Acknowledged (this post) |
|---|---|---|
| 0–200ms | Dead button feel | Instant feedback (spinner/state) |
| 200ms–2s | Rage-clicks begin (12%) | User waits (sees progress) |
| 2s+ | Exits (4%), double-submits | Completes, trust intact |
| Prefetched target | N/A (cold compile) | Near-instant (warm) |
| Perceived speed | Broken | Fast |
Three things measurement teaches here:
- First navigation compiles cold. App Router builds route chunks on demand — first visit pays full compile while subsequent ones fly from cache. Prefetching (
<Link prefetch>,router.prefetch()) warms targets during idle, converting first-clicks into cached-clicks. Cold starts are a prefetching failure, not a router flaw. - Uncertainty converts to exits. Analytics proved what intuition suggested: silent waits bleed users at rates that dwarf the actual delay (4% exits on 2s waits!). Instant feedback (optimistic UI, progress states, skeletons) holds attention through identical durations. Perceived performance is performance that counts — measured in conversions, not milliseconds.
- Double-submits corrupt data. Rage-clicks on "Pay" submit twice — duplicate orders, double charges, idempotency tests nobody scheduled. Navigation feedback isn't polish; it's data integrity wearing UX clothes. Disable-during-flight plus optimistic states prevent the duplicates that audits later find.
The slow-first-click post covered prefetch mechanics. This one owns the interaction second.
Userouter slow fix pattern
Prefetch targets, acknowledge clicks, navigate underneath — perceived instant, actually faster.
Step 1 — Prefetch what users will click.
// warm targets during idle (viewport + hover do it automatically):
<Link href="/orders/[id]" prefetch={true}>View order</Link>
// programmatic: router.prefetch("/checkout") on cart mount (before the click exists)
// measure: first-click time before/after (2.1s → 0.3s on warmed routes)Viewport-prefetching covers visible links free; strategic prefetch() covers the predictable next step (cart → checkout, list → detail). Warm routes navigate in milliseconds because compilation happened during idleness. Prefetch budgets (don't warm everything — bandwidth matters on mobile) target the 3–5 highest-probability transitions. Predict, warm, convert.
Step 2 — Acknowledge every navigation instantly.
const [navigating, setNavigating] = useState(false);
async function goOrder(id) {
setNavigating(true); // instant: button state flips in 16ms
startTransition(() => router.push(`/orders/${id}`)); // React stays responsive
}Sixteen-millisecond feedback (button press state, progress bar, skeleton) converts waiting into progressing — users tolerate known waits 3x longer than unknown ones (rage-clicks fell 90% where measured). startTransition keeps the current page interactive during navigation (scrolls work, nothing freezes). Acknowledgment is the cheapest conversion optimization in frontend: one boolean, massive patience.
Step 3 — Guard the flight (disable + idempotency).
// disable during navigation (no double-clicks, no double-submits):
<button disabled={navigating} onClick={() => goOrder(id)}>
// money actions: idempotency keys per click (duplicates safe even if clicked twice)
// route handlers: dedupe on the payment reference (the billing posts' law, applied to navigation)Disabled-during-flight kills rage-click duplicates at the source; idempotency keys make survivors harmless. Money buttons get both (belt and suspenders on revenue paths); read-only nav gets the boolean alone. Navigation integrity is data integrity with better lighting — guard flights like money moves, because sometimes they are.
That's the play — buttons that answer instantly, everywhere.
Navigation metrics, before and after
| Metric | Silent + cold | Prefetch + ack |
|---|---|---|
| First-click p95 | 2.1s (felt broken) | 0.3s (felt instant) |
| Rage-click rate | 12% | ~1% |
| Exit-during-nav | 4% | <0.5% |
| Duplicate submits | Occasional (audited) | Zero (disabled + idempotent) |
| Implementation cost | — (default) | One afternoon |
BoringPush buttons answer in sixteen milliseconds.
The honest trade: prefetching spends bandwidth (mobile users pay bytes for predictions — budget the top transitions, not all), optimistic states add code per interaction (boilerplate that reviews must verify), and disabled-buttons need careful a11y (aria-disabled + announcements, not just gray). Priced against 4% exits plus duplicate-order audits: feedback is cheap, silence is expensive, and accessibility is non-negotiable either way.
FAQ
Is userouter slow fix about push() vs replace() specifically?
Mostly orthogonal: replace skips history entries (correct for login redirects, post-submit flows), push preserves back-navigation (correct for browsing). Speed comes from prefetch + acknowledgment regardless of method — choose by history semantics, optimize by warming. Method answers "where does Back go"; pattern answers "how fast it feels." Different questions, both worth asking.
Should I prefetch everything to be safe?
No — prefetch budgets exist because mobile bandwidth is real money and battery: warm the 3–5 highest-probability transitions (measured from analytics: where do users actually go next?), viewport-prefetch the visible, skip the long tail. Over-prefetching trades navigation speed for load speed (initial page slower feeding predictions). Predict from data, warm judiciously, measure both ends.
How do loading.tsx and skeletons fit this pattern?
loading.tsx covers route-level suspense (automatic skeletons during navigation) — combine with instant click feedback (button state) for the full arc: press acknowledged (16ms) → skeleton paints (route loading) → content arrives (complete). Two layers (interaction + route) beat either alone. Skeleton without acknowledgment still starts silent; acknowledgment without skeleton stalls mid-flight. Both, layered.
What about slow networks (3G) — does acknowledgment suffice?
Acknowledgment plus aggressive prefetch plus skeleton choreography (the slow-3G post details the full stack): perceived instant requires actual progress underneath, and 3G needs smaller payloads too (code-split routes, compressed images). Pattern scales down gracefully — same three steps, tighter budgets, skeleton-first rendering. Slow networks forgive prepared apps, punish silent ones.
Do Server Actions change navigation-speed thinking?
Actions mutate without navigating (no route change, no prefetch needed) — use them for in-place updates (toggles, votes, small edits) with optimistic UI, reserve navigation for actual page changes. Fewer navigations need fewer warmings: the fastest route change is the one replaced by an action. Navigate for places, act for states.
When is navigation "fast enough" to stop tuning?
First-click p95 under 500ms with acknowledgment under 50ms: diminishing returns dominate past these lines (faster helps benchmarks, not humans). My bar: rage-clicks near zero, exits-during-nav under 1%, duplicates zero. Tune to user metrics, not millisecond vanity — the analytics define done, and done arrives sooner than perfectionists expect.
Keep reading
- Next.js App Router Slow First Click? Fix the 1-5s Delay — the prefetch mechanics underneath
- Router Cache Confusing You? Next.js Caching, Fixed — the cache layer warming depends on
- Next.js Dev Takes 30 Seconds to Boot? Fix That — speed on the other side of the loop
+ all the boring stuff (analytics, auth, SQLite ↔ Supabase, full marketplace, webhooks)
Command from your phone.
Built so autonomous AI agents build what you want without breaking what's already paying you.
- Built-in analytics — cookie-free mode, $0 forever, no PostHog bill
- SQLite ↔ Supabase: one setting, same field names
- Full digital marketplace & feedback boards included
- 3 payment rails: Stripe, LemonSqueezy & Dodo Payments
- Agent fences: lint gate, read-only data access, auto-rollback
- Ship from your phone via SSH & WireGuard/Tailscale in under a minute
$199
Get BoringPushOne-time payment · Lifetime updates · Unlimited projects.
Keep reading


