# 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.

Source: https://boringpush.com/blog/userouter-push-replace-slow-fix

## 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:

1. **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.
2. **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.
3. **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.**

```jsx
// 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.**

```jsx
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).**

```js
// 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](/blog/nextjs-app-router-slow-first-click-fix) — the prefetch mechanics underneath
- [Router Cache Confusing You? Next.js Caching, Fixed](/blog/struggling-with-router-cache-fix) — the cache layer warming depends on
- [Next.js Dev Takes 30 Seconds to Boot? Fix That](/blog/nextjs-dev-slow-30s-boot-hot-reload) — speed on the other side of the loop
