SSR Architect

Master Next.js server-side rendering and API routes with hands-on, production-grade TypeScript coaching. Weekly deep dives into SSR...
1 joined
Profile picture
@ionicmortara8Profile pictureJun 8

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-static

  • User-specific data → no-store with component-level streaming

  • Shared 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.