Building Scalable Next.js Applications
Patterns and practices for production-grade Next.js
Next.js has become the go-to framework for React applications, and for good reason. The combination of server-side rendering, static generation, and the new App Router makes it incredibly powerful for production applications.
The App Router Mental Model
The shift from Pages Router to App Router isn't just a technical change — it's a mental model change. Server Components change how you think about data fetching, state management, and component organization. The key insight: treat the server/client boundary as explicit, not implicit.
Data Fetching at the Server
In the App Router, data fetching happens on the server by default. You can fetch data directly in your components — no useEffect, no loading states for initial data. Use React's cache() function to deduplicate requests across components in the same request.
// This runs on the server, cached per-request
import { cache } from 'react';
const getUser = cache(async (id: string) => {
const user = await db.user.findUnique({ where: { id } });
return user;
});
Client State Should Be Minimal
With Server Components handling data, client state should be minimal. Only interactive UI state — dropdowns, modals, form inputs — needs client-side React. Everything else can live on the server and be streamed to the client.
Performance Considerations
Bundle size matters significantly. With Server Components, heavy dependencies (parsing libraries, DB clients, etc.) stay on the server and never ship to the browser. Use dynamic imports for client-heavy features not immediately needed:
const Chart = dynamic(() => import('./Chart'), { ssr: false })
The key question to ask about every component: "Does this need to be interactive?" If not, keep it as a Server Component. Your users' browsers will thank you.