June 18, 2026ยท8 min readยทBy Innovibe

7 Next.js App Router Mistakes That Are Killing Your Performance

The App Router is powerful but easy to misuse. These are the most common mistakes we see in production Next.js codebases.

Next.jsReactPerformanceTutorial

The Next.js App Router fundamentally changed how you write Next apps. Most teams have migrated โ€” and most teams have also spent at least one Friday afternoon staring at a mysterious hydration error, wondering if they should have just stuck with Create React App. They shouldn't have. But the App Router does require a mental model shift. Here are the mistakes we see most often in production codebases (and, uh, may have committed ourselves before we knew better).

Mistake 1: Adding 'use client' to everything

The #1 mistake. When you can't figure out why something isn't working, adding 'use client' at the top often fixes it โ€” but at the cost of losing server rendering entirely. It's the React equivalent of unplugging the wifi router to fix a software bug. Sure, it worked. But at what cost?

What it costs: Larger JS bundles, slower Time to Interactive, no server-side data access.

Fix: Only add 'use client' to components that need browser APIs, event handlers, or React state/effects. Keep data fetching, layout, and static content as Server Components.

// โŒ Don't do this just because it's easier
'use client'
export default function ProductPage({ id }: { id: string }) {
  const [product, setProduct] = useState(null)
  useEffect(() => {
    fetch(`/api/products/${id}`).then(r => r.json()).then(setProduct)
  }, [id])
  return <div>{product?.name}</div>
}

// โœ… Server Component โ€” no bundle cost, direct DB access
export default async function ProductPage({ params }: { params: { id: string } }) {
  const product = await db.products.findUnique({ where: { id: params.id } })
  return <div>{product?.name}</div>
}

Mistake 2: Waterfall fetches in Server Components

Sequential awaits = sequential round trips = slow pages. Your users didn't come here to watch a loading spinner do a solo performance.

// โŒ Sequential: 3 ร— 200ms = 600ms minimum
export default async function Dashboard() {
  const user = await getUser()
  const orders = await getOrders(user.id)
  const analytics = await getAnalytics(user.id)
  // ...
}

// โœ… Parallel: max(200ms, 200ms, 200ms) = 200ms
export default async function Dashboard() {
  const [user, orders, analytics] = await Promise.all([
    getUser(),
    getOrders(),
    getAnalytics(),
  ])
  // ...
}

Mistake 3: Not using loading.tsx and Suspense

Without Suspense boundaries, the entire page waits for the slowest data fetch before rendering anything.

// app/dashboard/loading.tsx
export default function Loading() {
  return <DashboardSkeleton />
}

// app/dashboard/page.tsx
import { Suspense } from 'react'

export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Suspense fallback={<StatsSkeleton />}>
        <Stats />  {/* This can load independently */}
      </Suspense>
      <Suspense fallback={<OrdersSkeleton />}>
        <RecentOrders />  {/* This too */}
      </Suspense>
    </div>
  )
}

Mistake 4: Over-fetching in layouts

Layout components run on every navigation within their scope. If your root layout fetches the user on every page, you're making an extra DB call on every page transition.

// โŒ Fetches on every navigation
export default async function Layout({ children }) {
  const user = await getUser()  // called on EVERY page in this layout
  return <div><Header user={user} />{children}</div>
}

// โœ… Cache the result
export default async function Layout({ children }) {
  const user = await getCachedUser()  // cached per request
  return <div><Header user={user} />{children}</div>
}

// Use React's cache() for deduplication across the request
import { cache } from 'react'

export const getCachedUser = cache(async () => {
  return db.users.findFirst({ where: { id: getSessionUserId() } })
})

Mistake 5: Putting secrets in client components

Environment variables without the NEXT_PUBLIC_ prefix are server-only. But if you import a server module from a client component, Next.js will throw an error โ€” or worse, silently expose the variable.

// โŒ This will error or leak your secret
'use client'
const API_KEY = process.env.MY_SECRET_KEY  // undefined or exposed

// โœ… Keep secrets server-side only
// Server Component or Route Handler:
const key = process.env.MY_SECRET_KEY  // only runs on server

Use a server-only package to enforce this:

// lib/db.ts
import 'server-only'
// This file can never be imported by a client component

Mistake 6: Skipping generateStaticParams for dynamic routes

If your dynamic routes ([id]) have a finite set of values, pre-render them at build time.

// app/blog/[slug]/page.tsx

// Without this, every blog post is rendered on-demand (slow first load)
export async function generateStaticParams() {
  const posts = await getAllPostSlugs()
  return posts.map(slug => ({ slug }))
}
// Now all known posts are pre-rendered โ€” instant loads

Mistake 7: Not setting cache headers on fetch calls

By default, fetch in Server Components is cached. But you need to understand when to revalidate.

// โŒ Stale forever (default in some Next versions)
const data = await fetch('https://api.example.com/data')

// โœ… Revalidate every 60 seconds
const data = await fetch('https://api.example.com/data', {
  next: { revalidate: 60 }
})

// โœ… Never cache (always fresh)
const data = await fetch('https://api.example.com/data', {
  cache: 'no-store'
})

// โœ… Cache until manually invalidated (use with revalidatePath/revalidateTag)
const data = await fetch('https://api.example.com/data', {
  next: { tags: ['products'] }
})

Need a Next.js performance audit? We review codebases and ship the fixes โ€” and we promise not to judge your 'use client' count. Much. Get in touch.

K
Innovibe
Founder & Technical Lead, Innovibe

Building software for 15+ years. Passionate about AI, system design, and shipping things that work.

Frequently asked questions

When should I use Server Components vs Client Components?+

Server Components are the default โ€” use them for data fetching, static content, and anything that doesn't need interactivity. Only switch to Client Components when you need browser APIs, state, effects, or event handlers. The less JS you ship to the browser, the faster your site.

Why is my Next.js build so slow?+

Usually it's TypeScript type-checking or a large dependency. Try `NEXT_TELEMETRY_DISABLED=1 next build` with `--profile` and look at what's taking time. Splitting large data files and enabling turborepo are the two biggest wins.

Should I use the App Router or Pages Router for a new project?+

App Router for new projects, full stop. Pages Router still works fine but receives fewer improvements. The learning curve for App Router is steep for one week, then you never want to go back.

How long will this take to implement?+

Most readers ship a working version in an afternoon. The first implementation is always the hardest โ€” once you understand the pattern, iterations get much faster.

Does Innovibe build this kind of thing for clients?+

Yes โ€” this is exactly what we do day-to-day for clients across BC and Canada. If you'd rather have us build and maintain it than implement it yourself, reach out.

Building something with AI?

We scope and ship AI features quickly. Let's talk.

Start a Conversation