Skip to main content
ARCHITECTVI

Software Engineer

Available for work

Open to opportunities
๐Ÿ“šFrontend

Building Scalable Next.js Applications

Deep dive into architectural patterns and best practices for creating production-grade Next.js applications.

Mar 15, 2024ยท8 min read
Next.jsArchitecturePerformance

After shipping MuslimPro's web platform to 3M+ daily users on Next.js, I've learned that scalability isn't a feature you bolt on at the end โ€” it's a set of deliberate decisions made from day one. This article walks through the patterns that made the difference in production.

Choosing the Right Rendering Strategy

Next.js gives you four rendering strategies per route: Static Site Generation (SSG), Incremental Static Regeneration (ISR), Server-Side Rendering (SSR), and full client-side rendering. The mistake most teams make is reaching for SSR by default.

  • SSG โ€” Use for pages whose data rarely changes (marketing pages, blog posts, docs). Zero server cost at runtime.
  • ISR โ€” Use when data changes periodically but not per-request (prayer times by city, product listings). Set revalidate to match your data freshness requirement.
  • SSR โ€” Reserve for pages that need per-request personalisation (authenticated dashboards, cart pages).
  • Client-side โ€” Use for non-critical UI that only appears after interaction (modals, tooltips, preference panels).
๐Ÿ’กAt MuslimPro we shifted prayer times pages from SSR to ISR with a 6-hour revalidation window. P95 Time-to-First-Byte dropped from 420 ms to 38 ms.

Caching at Every Layer

Next.js 14+ has four distinct caching layers: the Request Memoisation cache (per render), the Data Cache (persistent, cross-request), the Full Route Cache (HTML + RSC payload), and the Router Cache (client-side navigation). Understanding which layer applies to each fetch call is critical.

typescript
// ISR fetch โ€” revalidate every 6 hours
async function getPrayerTimes(city: string) {
  const res = await fetch(`/api/prayer-times?city=${city}`, {
    next: { revalidate: 21600 },
  });
  return res.json();
}

// Always-fresh fetch โ€” opt out of caching
async function getUserSubscription(userId: string) {
  const res = await fetch(`/api/subscription/${userId}`, {
    cache: 'no-store',
  });
  return res.json();
}

Structuring a Large Next.js Codebase

As the codebase grows, the default flat structure breaks down. I use a feature-first layout where each major domain owns its routes, components, and data-fetching logic together.

text
src/
  app/
    (marketing)/         # Route group โ€” no shared layout cost
      page.tsx
    prayer-times/
      page.tsx
      loading.tsx
      error.tsx
  features/
    prayer-times/
      components/
      hooks/
      api.ts             # Server-side data fetching
  components/
    ui/                  # Design system primitives
    layout/              # Navigation, Footer

Parallel Routes and Intercepting Routes

Two underused App Router features that dramatically reduce complexity: parallel routes let you render multiple independent page segments in one layout (ideal for dashboards), while intercepting routes let you show a modal version of a page without losing the current context.

โ„นWe used intercepting routes for the MuslimPro giving platform so clicking a donation item opened a modal detail view, while a direct URL visit rendered the full page โ€” with zero duplicate code.

Server Components Are Not Free

React Server Components eliminate client JS bundle size but introduce their own overhead. Each RSC with a database call adds a sequential waterfall unless you parallelise fetches with Promise.all or use Suspense boundaries strategically.

typescript
// โŒ Sequential โ€” 3ร— latency
const user = await getUser(id);
const posts = await getPosts(user.id);
const comments = await getComments(posts[0].id);

// โœ… Parallel where possible
const [user, globalSettings] = await Promise.all([
  getUser(id),
  getGlobalSettings(),
]);
const posts = await getPosts(user.id); // depends on user โ€” must stay sequential

Conclusion

The patterns above aren't theoretical โ€” they're extracted from running Next.js at scale. Start with ISR by default, cache aggressively, organise by feature, and parallelise your data fetching. The result is an application that stays fast as it grows.

Written by

Md. Saniuzzaman Robin

Full-Stack Software Engineer

More Articles โ†’