Why Your Next.js App Is Slow — 5 SSR Mistakes Senior Devs Still Make
Most Next.js apps ship with broken rendering strategies. I've reviewed hundreds of production codebases, and the same five mistakes keep showing up — even from senior engineers.
1. Defaulting to Client Components
// ❌ This is what most teams do
"use client"
export default function Dashboard({ userId }: { userId: string }) {
const [data, setData] = useState(null);
useEffect(() => { fetch(`/api/dashboard/${userId}`).then(...) }, []);
return <DashboardView data={data} />;
}// ✅ Server Component — zero client JS, faster TTFB
export default async function Dashboard({ userId }: { userId: string }) {
const data = await db.dashboard.findUnique({ where: { userId } });
return <DashboardView data={data} />;
}The fix: Start every component as a Server Component. Only add "use client" when you need interactivity (event handlers, hooks, browser APIs).
2. Ignoring the Data Fetching Waterfall
// ❌ Sequential — each await blocks the next
const user = await getUser(id);
const posts = await getPosts(user.id);
const analytics = await getAnalytics(user.id);// ✅ Parallel — fires all three simultaneously
const [user, posts, analytics] = await Promise.all([
getUser(id),
getPosts(id),
getAnalytics(id),
]);This alone can cut page load times by 60%+ on data-heavy pages.
3. Caching Nothing (or Everything)
Next.js gives you granular caching with unstable_cache and route segment config — but most teams either cache nothing (slow) or cache everything (stale data).
The right approach: cache by data volatility.
Static content →
force-staticUser-specific data →
no-storewith component-level streamingShared but changing data →
revalidate: 60(or whatever fits your SLA)
4. Fat API Routes With No Type Safety
// ❌ Untyped, no validation, error-prone
export async function POST(req: Request) {
const body = await req.json();
const result = await db.users.create({ data: body }); // 💀
return Response.json(result);
}// ✅ Zod-validated, typed end-to-end
import { z } from "zod";
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
role: z.enum(["admin", "member"]),
});
export async function POST(req: Request) {
const parsed = CreateUserSchema.safeParse(await req.json());
if (!parsed.success) {
return Response.json({ error: parsed.error.flatten() }, { status: 400 });
}
const user = await db.users.create({ data: parsed.data });
return Response.json(user, { status: 201 });
}5. Not Streaming
If your page has a slow data source, the entire page waits. Streaming with <Suspense> lets you send the shell instantly and fill in slow sections as they resolve:
export default function Page() {
return (
<main>
<Header /> {/* Sent immediately */}
<Suspense fallback={<Skeleton />}>
<SlowDataSection /> {/* Streams in when ready */}
</Suspense>
</main>
);
}---
These five patterns separate production-grade Next.js from tutorial-grade Next.js. I teach all of them (and much more) in depth inside SSR Architect — a complete course + coaching program on server-side rendering and API architecture.
If you're a TypeScript developer ready to build apps that actually perform in production, check it out.
