1:"$Sreact.fragment"
2:I[22016,["/_next/static/chunks/0sqf3kwsxhw92.js","/_next/static/chunks/15vvi4du_kj4d.js","/_next/static/chunks/0t2xr05rlu96l.js","/_next/static/chunks/0j_00-43ohwi..js","/_next/static/chunks/074m5~1.spxnd.js","/_next/static/chunks/03pwh54kk_crp.js"],""]
8:I[6966,["/_next/static/chunks/0sqf3kwsxhw92.js","/_next/static/chunks/15vvi4du_kj4d.js","/_next/static/chunks/0t2xr05rlu96l.js","/_next/static/chunks/0j_00-43ohwi..js","/_next/static/chunks/074m5~1.spxnd.js","/_next/static/chunks/03pwh54kk_crp.js"],"BlogPostContent"]
a:I[97367,["/_next/static/chunks/0sqf3kwsxhw92.js","/_next/static/chunks/15vvi4du_kj4d.js","/_next/static/chunks/0t2xr05rlu96l.js","/_next/static/chunks/0j_00-43ohwi..js","/_next/static/chunks/074m5~1.spxnd.js"],"OutletBoundary"]
b:"$Sreact.suspense"
0:{"rsc":["$","$1","c",{"children":[["$","div",null,{"className":"min-h-screen bg-background text-foreground","children":[["$","section",null,{"className":"pt-28 pb-16 md:pt-36 md:pb-24 bg-gradient-to-b from-accent/30 to-background","children":["$","div",null,{"className":"container px-4 md:px-6","children":["$","div",null,{"className":"max-w-4xl mx-auto","children":[["$","$L2",null,{"href":"/blog","children":[["$","svg",null,{"xmlns":"http://www.w3.org/2000/svg","width":24,"height":24,"viewBox":"0 0 24 24","fill":"none","stroke":"currentColor","strokeWidth":2,"strokeLinecap":"round","strokeLinejoin":"round","className":"lucide lucide-arrow-left mr-2 h-4 w-4","children":[["$","path","1l729n",{"d":"m12 19-7-7 7-7"}],["$","path","x3x0zl",{"d":"M19 12H5"}],"$undefined"]}],"Back to Blog"],"className":"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2 mb-6","ref":null}],["$","div",null,{"className":"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 border-transparent bg-primary text-primary-foreground hover:bg-primary/80 mb-4","children":"Frontend"}],["$","h1",null,{"className":"text-3xl md:text-4xl lg:text-5xl font-bold tracking-tighter mb-6 animate-fade-in","children":"How I Finally Fixed the Slow Navigation in Next.js App Router"}],["$","div",null,{"className":"flex flex-wrap items-center gap-4 text-muted-foreground mb-8 animate-fade-in","children":[["$","div",null,{"className":"flex items-center gap-2","children":[["$","svg",null,{"xmlns":"http://www.w3.org/2000/svg","width":24,"height":24,"viewBox":"0 0 24 24","fill":"none","stroke":"currentColor","strokeWidth":2,"strokeLinecap":"round","strokeLinejoin":"round","className":"lucide lucide-calendar h-4 w-4","children":[["$","path","1cmpym",{"d":"M8 2v4"}],["$","path","4m81vk",{"d":"M16 2v4"}],["$","rect","1hopcy",{"width":"18","height":"18","x":"3","y":"4","rx":"2"}],["$","path","8toen8",{"d":"M3 10h18"}],"$undefined"]}],["$","span",null,{"children":"December 23, 2024"}]]}],["$","div",null,{"className":"flex items-center gap-2","children":[["$","svg",null,{"xmlns":"http://www.w3.org/2000/svg","width":24,"height":24,"viewBox":"0 0 24 24","fill":"none","stroke":"currentColor","strokeWidth":2,"strokeLinecap":"round","strokeLinejoin":"round","className":"lucide lucide-clock h-4 w-4","children":[["$","circle","1mglay",{"cx":"12","cy":"12","r":"10"}],["$","polyline","68esgv",{"points":"12 6 12 12 16 14"}],"$undefined"]}],["$","span",null,{"children":"10 min read"}]]}],["$","div",null,{"className":"flex items-center gap-2","children":["$","span",null,{"children":["By ","Muhammad Zaid"]}]}]]}],["$","div",null,{"className":"flex flex-wrap gap-2 mb-8 animate-fade-in","children":[["$","div","Next.js",{"className":"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 text-foreground","children":"Next.js"}],["$","div","React",{"className":"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 text-foreground","children":"React"}],["$","div","TypeScript",{"className":"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 text-foreground","children":"TypeScript"}],["$","div","Web Development",{"className":"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 text-foreground","children":"Web Development"}]]}]]}]}]}],"$L3","$L4","$L5"]}],["$L6"],"$L7"]}],"isPartial":false,"staleTime":300,"varyParams":null,"buildId":"QX83e4YaSJMU9KhrDXtKJ"}
3:["$","section",null,{"className":"pb-12","children":["$","div",null,{"className":"container px-4 md:px-6","children":["$","div",null,{"className":"max-w-4xl mx-auto","children":["$","img",null,{"src":"https://images.unsplash.com/photo-1555066931-4365d14bab8c?auto=format&fit=crop&w=800","alt":"How I Finally Fixed the Slow Navigation in Next.js App Router","className":"w-full h-auto rounded-lg shadow-xl","loading":"lazy"}]}]}]}]
9:T2289,# How I Finally Fixed the Slow Navigation in Next.js App Router (And You Can Too!)
If you're reading this, chances are you've experienced that annoying lag when clicking links in your Next.js App Router application. You know what I'm talking about - that frustrating moment when you click a navigation link and... nothing happens. You wait. Still nothing. Then suddenly, boom - the page changes.
Yeah, I've been there too. And honestly? It was driving me crazy.
## The Problem That Kept Me Up at Night
I recently migrated one of my projects to Next.js 15 with the App Router, and while I loved all the new features - server components, improved data fetching, better performance - there was this one thing that kept bugging me: **the navigation felt sluggish**.
Coming from the world of SPAs (Single Page Applications), I was used to instant feedback. Click a link, see it highlighted immediately, page transitions smooth as butter. But with the App Router, there was this weird limbo period between clicking a link and actually seeing any visual feedback.
I started Googling. "Next.js slow navigation", "App Router navigation lag", "Next.js navigation feels slow" - you name it, I searched it. And you know what? I wasn't alone. Tons of developers were complaining about the same issue.
## Why Does This Happen?
Here's the thing: the App Router relies heavily on server-side rendering (SSR) and static site generation (SSG). While this is great for performance and SEO, it means Next.js has to wait for the server to process the request before updating the UI.
During this waiting period:
- The link doesn't show an active state
- The current content just sits there, looking stale
- Users (like me) keep clicking, wondering if the app is broken
- The experience feels janky and unresponsive
Even worse, the navigation hooks like `usePathname` and `useSearchParams` only update **after** the navigation completes. So you can't even use them to show a loading state or highlight the active link immediately.
## The Search for a Solution
I tried different approaches:
- Added loading.js files (helped, but didn't solve the instant feedback issue)
- Experimented with Suspense boundaries (same story)
- Attempted client-side state management with onClick events (worked, but had edge cases like Cmd+Click to open in new tab)
Nothing felt quite right. Until I stumbled upon Next.js 15.3 release notes and saw two game-changing features:
1. **`onNavigate` event** - fires when navigation starts, only on client-side
2. **`useOptimistic` hook** - allows optimistic UI updates
And that's when it clicked. I could combine these to create instant, snappy navigation!
## The Solution That Actually Works
Here's what I built, and trust me, it's simpler than you might think.
### Step 1: Create a Navigation Context
First, I created a context to manage the optimistic navigation state across my entire app:
```tsx
// contexts/OptimisticNavigationContext.tsx
"use client";
import { usePathname } from "next/navigation";
import { createContext, ReactNode, useContext, useOptimistic } from "react";
type OptimisticNavigationContextType = {
isNavigating: boolean;
optimisticPathname: string;
setOptimisticPathname: (pathname: string) => void;
};
const OptimisticNavigationContext = createContext<
OptimisticNavigationContextType | undefined
>(undefined);
export const OptimisticNavigationContextProvider = ({
children,
}: {
children: ReactNode;
}) => {
const pathname = usePathname();
const [optimisticPathname, setOptimisticPathname] = useOptimistic(
pathname,
(_, action: string) => action
);
return (