App Router in Next.js, laggy routing to subpages, how to get out of this mess?


Introduction
We decide to go down the path of next,js because of fast development speeds in the case that routing is already set up with the app router and backend as well with route handlers.
When building apps; internal tools, SaaS products e.t.c. You may have a couple of subpages and heavy queries are loaded in that order. you may observe the routing is laggy when switching from one route to another, this transition may seem to be taking a long time. How can you fix this whilst staying in the next.js without starting afresh using another framework or just going with (CSR + React Router)?
Chances are you are using the app router the wrong way;
You should NOT write "use client" in page.tsx or layout.tsx files This defeats the entire purpose of the App Router and React Server Components. Here's the correct approach:
Your page.tsx should stay as a Server Component to handle data fetching on the server. Only mark specific components with "use client" when they need interactivity.
Wrong Approach:
// app/dashboard/page.tsx - DON'T DO THIS
'use client'
import { useState, useEffect } from 'react'
export default function DashboardPage() {
const [data, setData] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/dashboard-data')
.then(res => res.json())
.then(data => {
setData(data)
setLoading(false)
})
}, [])
if (loading) return <div>Loading...</div>
return <div>{/* Your dashboard UI */}</div>
}
useEffect could also be reactQuery or useSWR, it does not really matter it is still not the most optimal solution when using the app router.
Correct Approach:
// app/dashboard/page.tsx - Server Component (no 'use client')
import { Suspense } from 'react'
import DashboardContent from './dashboard-content'
import DashboardSkeleton from './dashboard-skeleton'
async function getDashboardData() {
// This runs on the server - faster, secure, closer to your database
const res = await fetch('https://your-api.com/dashboard', {
cache: 'force-cache', // Adjust caching strategy as needed
next: { revalidate: 60 } // Revalidate every 60 seconds
})
if (!res.ok) throw new Error('Failed to fetch data')
return res.json()
}
export default async function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<DashboardSkeleton />}>
<DashboardData />
</Suspense>
</div>
)
}
async function DashboardData() {
const data = await getDashboardData()
return <DashboardContent initialData={data} />
}
Create the interactive client component separately:
// app/dashboard/dashboard-content.tsx - Client Component
'use client'
import { useState } from 'react'
export default function DashboardContent({ initialData }) {
const [data, setData] = useState(initialData)
const [selectedItem, setSelectedItem] = useState(null)
// Your interactive logic here
const handleItemClick = (item) => {
setSelectedItem(item)
}
return (
<div>
{data.map(item => (
<div
key={item.id}
onClick={() => handleItemClick(item)}
className="cursor-pointer hover:bg-gray-100"
>
{item.name}
</div>
))}
{selectedItem && (
<div>Selected: {selectedItem.name}</div>
)}
</div>
)
}
To wrap it up;
You should try to keep as much as possible on the server and only move the parts of the UI that need client-side functionality like useState, useRef, and so on, out into separate components marked with 'use client'. You should also try to use searchParams and params as much as possible as your server state, since these can be passed as arguments to page.tsx: https://nextjs.org/docs/app/api-reference/file-conventions/page
Subscribe to my newsletter
Read articles from Hillary Nyakundi directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Hillary Nyakundi
Hillary Nyakundi
I am a software engineer who is fascinated with building exclusive technology.