How to Fix: Im facing slow navigation on staging
Slow router.push() navigation on staging usually means your route transition is waiting on server work, not that the router itself is broken.
If clicking to a new page feels stuck for 1–2 seconds before loading.js appears, the issue is typically caused by slow server components, uncached data fetching, blocking layouts, or staging-only infrastructure latency. In Next.js App Router, navigation is often gated by the server preparing the next route segment. That is why the UI can appear frozen before the loading state renders.
Table of Contents
Understanding the Root Cause
In a Next.js application using the App Router, router.push() does not always behave like an instant client-side view swap. When you navigate, Next.js may need to fetch the next React Server Component payload, resolve async layouts, run server-side data requests, and stream the result back to the browser.
If any of these steps are slow, navigation appears delayed:
- Slow server-side fetches: APIs, databases, or CMS calls in the target route can block rendering.
- Blocking layouts: If a parent layout.tsx performs async work, every child route transition may wait on it.
- No effective caching: Using dynamic fetches or disabling caching forces fresh server work on every navigation.
- Staging infrastructure latency: Staging often runs on smaller instances, cold containers, slower databases, or regions far from users.
- Late loading boundary activation: loading.js only shows when the relevant route segment starts suspending in the expected boundary. If the delay happens before that boundary becomes active, the old page can remain visible briefly.
- Client-side heavy work: Large bundles, expensive effects, or synchronous rendering on the destination page can make transitions feel stuck.
This is why the issue appears most clearly on staging: production builds may still be optimized, but staging environments often expose latency from underpowered services and uncached requests.
Step-by-Step Solution
Use the following process to identify and fix the bottleneck.
1. Verify whether the delay is server-side or client-side
Start by measuring the target page. Add simple timing logs to the server component or data layer.
export default async function Page() {
console.time('page-render');
const data = await getData();
console.timeEnd('page-render');
return <div>{data.title}</div>;
}
If getData() takes 1–2 seconds in staging, the delay is expected during navigation.
2. Move expensive work out of shared layouts
A common problem is fetching data in layout.tsx. Because layouts wrap route segments, a slow layout can delay every page under it.
// Bad: expensive async work in a shared layout
export default async function DashboardLayout({ children }) {
const profile = await fetchUserProfile();
return (
<section>
<aside>{profile.name}</aside>
{children}
</section>
);
}
Instead, fetch only what is globally required, and move route-specific data into the page or a nested component.
// Better: keep layout lean
export default function DashboardLayout({ children }) {
return (
<section>
<aside>Sidebar</aside>
{children}
</section>
);
}
3. Use caching correctly for fetch requests
If your page data does not need to be fully dynamic on every request, enable caching or revalidation.
async function getData() {
const res = await fetch('https://example.com/api/products', {
next: { revalidate: 60 }
});
if (!res.ok) {
throw new Error('Failed to fetch data');
}
return res.json();
}
If you are using:
fetch(url, { cache: 'no-store' })
or dynamic server functions everywhere, you are forcing the route to wait on fresh data every time. That can be correct for real-time dashboards, but it is often unnecessary for standard pages.
4. Add or verify the correct loading boundary
Make sure the route segment actually has a loading.js file in the right location.
app/
dashboard/
loading.js
page.js
Example loading UI:
export default function Loading() {
return <p>Loading dashboard...</p>;
}
If the delay happens in a parent layout or before the target segment starts suspending, the loading state may appear late. In that case, add a loading boundary closer to the slow async component or reduce blocking work above it.
5. Prefetch routes before navigation
For links users are likely to click, prefetching can reduce perceived delay.
import Link from 'next/link';
export default function Nav() {
return (
<Link href="/dashboard" prefetch>
Open Dashboard
</Link>
);
}
If you are navigating programmatically, consider warming the route earlier using user interaction patterns like hover-triggered prefetch where appropriate.
6. Wrap navigation in a transition for better UI responsiveness
This does not fix the backend delay, but it helps the UI stay responsive and lets you show pending state immediately.
'use client';
import { useRouter } from 'next/navigation';
import { useTransition } from 'react';
export default function ExampleButton() {
const router = useRouter();
const [isPending, startTransition] = useTransition();
const handleClick = () => {
startTransition(() => {
router.push('/dashboard');
});
};
return (
<button onClick={handleClick}>
{isPending ? 'Navigating...' : 'Go to dashboard'}
</button>
);
}
7. Check staging infrastructure, not just application code
If local and production feel fast but staging is slow, inspect:
- Server region mismatch
- Cold starts on serverless functions
- Slow database connection setup
- Rate-limited third-party APIs
- Missing environment variables causing fallback behavior
- Disabled caches in staging
Log API timings directly in staging and compare them with local runs. That usually exposes the real bottleneck quickly.
8. Avoid expensive client-side hydration on the destination page
If the route contains large client components, charts, editors, or heavy state initialization, navigation can feel delayed even after server data arrives.
// Example: lazy-load a heavy client component
import dynamic from 'next/dynamic';
const HeavyChart = dynamic(() => import('./HeavyChart'), {
loading: () => <p>Loading chart...</p>
});
export default function Page() {
return <HeavyChart />;
}
9. Audit route data dependencies
If one page requests several APIs sequentially, combine or parallelize them.
// Slower
const user = await getUser();
const orders = await getOrders();
const settings = await getSettings();
// Better
const [user, orders, settings] = await Promise.all([
getUser(),
getOrders(),
getSettings()
]);
Parallelizing independent data fetches often removes the exact 1–2 second delay described in this issue.
10. Recommended practical fix pattern
If you want a safe baseline implementation, use this structure:
// app/dashboard/loading.js
export default function Loading() {
return <p>Loading dashboard...</p>;
}
// app/dashboard/page.js
async function getDashboardData() {
const res = await fetch('https://example.com/api/dashboard', {
next: { revalidate: 30 }
});
if (!res.ok) throw new Error('Failed to load dashboard');
return res.json();
}
export default async function DashboardPage() {
const data = await getDashboardData();
return <div>Welcome, {data.name}</div>;
}
// app/components/DashboardButton.js
'use client';
import { useRouter } from 'next/navigation';
import { useTransition } from 'react';
export default function DashboardButton() {
const router = useRouter();
const [isPending, startTransition] = useTransition();
return (
<button
onClick={() =>
startTransition(() => {
router.push('/dashboard');
})
}
>
{isPending ? 'Opening...' : 'Open dashboard'}
</button>
);
}
This combination addresses the most common causes: visible pending state, proper route loading fallback, and reduced server delay through caching.
Common Edge Cases
- Middleware delays: If you use authentication or geo-based middleware, it can slow every navigation before the route even renders.
- Redirect chains: A route that redirects through auth checks or locale handlers may feel like it hangs.
- Suspense in the wrong place: If your async component is outside the intended loading boundary, the fallback will not appear when expected.
- Dynamic rendering forced accidentally: Using cookies, headers, or no-store in a shared path can disable caching for the whole route.
- Third-party SDK initialization: Analytics, A/B testing tools, or auth libraries can block rendering in staging if misconfigured.
- Large client bundles: Even if the server responds quickly, too much JavaScript can delay interactivity after navigation.
- Database connection cold starts: Common on staging environments using serverless databases or paused instances.
FAQ
Why does loading.js show up late instead of immediately?
Because the delay may be happening before the route segment reaches the suspension point that triggers the loading boundary. Slow parent layouts, middleware, or blocking server work can keep the current page visible first.
Is router.push() the reason navigation is slow?
Usually no. router.push() only starts the transition. The real delay is commonly caused by server data fetching, uncached rendering, redirects, or staging infrastructure latency.
Why is this worse on staging than local?
Staging often has slower databases, cold starts, smaller compute resources, disabled caches, or slower third-party integrations. Local development may use faster mock data or lower network latency, so the issue stays hidden there.
The fastest path to a fix is to profile the destination route, remove heavy work from shared layouts, cache what can be cached, and verify that your loading.js boundary sits at the correct segment level. Once those are cleaned up, router.push() transitions usually become smooth again.