©Code byMilanParmar

Four App Router mistakes I keep finding in production

None of these are exotic. They are the four things I find almost every time I open someone else's App Router codebase, and each one is an afternoon of work to fix.

4 min readUpdated

A thin cable knotted once on a desk

Every one of these came out of a real audit, and every one of them was shipped by someone who had read the docs.

None of the four need a rewrite and none of them show up as an error. They're what I look for first when someone hands me a codebase and asks why it feels slow, and they turn up often enough that they're most of what fills Next.js notes.

Client boundary too high

Someone needs a useState for a dropdown, so use client goes at the top of the page. Everything below it — the whole page, its cards, its formatting helpers, the date library it imported — now ships to the browser as JavaScript.

The boundary belongs at the leaf, not the root. Keep the page a server component and make the dropdown its own client component. The rest of the tree stays server-rendered and ships nothing.

At the root

The whole page ships as JavaScript

At the leaf

Only the dropdown ships

Fetching in layouts

Layouts don't re-render on navigation between the routes they wrap. That's usually what you want, but it means data fetched in a layout can be stale in a way that's genuinely hard to debug — the page shows fresh content next to a header that hasn't moved.

Fetch in the page. If two pages need the same data, fetch it in both and let the request be deduplicated, rather than hoisting it into a layout to feel efficient.

In the layout

Stale after the first navigation

In the page

Fresh on every navigation

Hand-written metadata

Titles typed twice, canonical URLs missing on half the routes, an OpenGraph description that no longer matches the page. This is what happens when metadata is written per route by hand instead of derived from the same object that renders the page.

What to derive instead of type

  1. 01

    Title and description

    From the same content object that renders the page. Typed twice is already a bug.

  2. 02

    Canonical path

    From the route params. Missing on half the routes is the usual failure.

  3. 03

    OG image

    From the file convention rather than a static asset sitting in a folder nobody updates.

  4. 04

    Sitemap and JSON-LD

    From the same source as the page. If adding a route means editing more than one file, that's the bug.

If adding a page means editing more than one file, that's the bug.

It's the same principle as how I scope a project — decide once, write it down once, derive everything else from it.

Images without dimensions

next/image without width and height — or fillwith a sized parent — gives you layout shift, and layout shift is the easiest Core Web Vitals score to lose by accident. It's also the easiest to fix, because the compiler will tell you where every one of them is.

Wrap it. One component that requires dimensions and sets a sensible sizes means the mistake becomes impossible rather than merely discouraged. You can see the pattern across the builds in my portfolio, and the details are in the Next.js docs.

Without dimensions

The page jumps as images load

Wrapped once

The mistake becomes impossible

What these four cost a business

None of this is about elegance. A client boundary in the wrong place means a phone on a train takes three extra seconds to show anything, and three seconds is where people leave. Data fetched in a layout means someone on your team eventually opens a support ticket about a number that looks wrong, spends a day chasing it, and finds nothing — because the number is only stale on the second page a visitor lands on, which is the one nobody thinks to test.

Hand-written metadata is the expensive one, because it fails quietly. Nobody notices a missing canonical tag the week it ships. You notice it six months later as a flat line in a traffic graph, and by then the fix is still an hour of work but the six months are gone. Images without dimensions are the same shape of problem: the page looks fine, so nobody reports it, and the score costs you rankings in the background.

01

Three extra seconds

A client boundary in the wrong place. A phone on a train. That's where people leave.

02

A stale number

A support ticket, a day chasing it, nothing to find — it's only wrong on the second page.

03

A missing canonical

Nobody notices the week it ships. You notice six months later as a flat line in traffic.

04

Layout shift

The page looks fine, so nobody reports it. The score costs you rankings in the background.

That's the whole reason these four are worth writing down. They are afternoon fixes with month-long payoffs, they don't need a migration, and they don't need anyone's permission to start. If you're deciding whether an existing codebase needs a rebuild or a repair, this is where I look first — and most of the time the honest answer is repair, which is a fraction of the budget.

If that sounds like your app, the fastest version of this is an hour on a call with the repo open. Most of the work I take on starts there rather than with a proposal.

FAQs

Is the App Router worth migrating to?

For a new build, yes. For a working Pages Router app with no performance problem, a migration is a cost with no user-visible benefit — do it when you have a reason beyond wanting to be current.

How do I check my own app for these?

Search the repo for the use client directive and look at where the matches sit in the tree, then run a production build and read the route sizes it prints. Both take about ten minutes and usually find at least one of these four.

How long does it take to fix these?

An afternoon on a small app, a couple of days if the client boundary has spread through a large tree. None of them need a rewrite, which is why they're the first things I look at on a call.