Next.js 15 in Production: Performance Tips and Gotchas

Ship Next.js 15 to production with confidence. Server components, caching pitfalls, bundle optimization, ISR, and real-world performance techniques.

Y
Yash Pritwani
13 min read

Next.js 15: What Changed for Production

Next.js 15 introduced significant changes to caching, server components, and the App Router. If you are upgrading from 14 or deploying a new project, there are critical differences that affect production performance. At TechSaaS, our company website runs on Next.js 15 with static export — here is what we learned.

docker-compose.yml123456789version: "3.8"services: web: image: nginx:alpine ports: - "80:80" volumes: - ./html:/usr/share/nginx

A well-structured configuration file is the foundation of reproducible infrastructure.

Gotcha 1: Caching Is No Longer Aggressive by Default

In Next.js 14, fetch requests were cached by default. In Next.js 15, they are not. This is the biggest change and catches many teams off guard.

// Next.js 14: cached by default
const data = await fetch('https://api.example.com/products');

// Next.js 15: NOT cached by default
const data = await fetch('https://api.example.com/products');

// Next.js 15: explicitly cache
const data = await fetch('https://api.example.com/products', {
  next: { revalidate: 3600 }  // Cache for 1 hour
});

// Or cache indefinitely
const data = await fetch('https://api.example.com/products', {
  cache: 'force-cache'
});

Action: Audit every fetch call and add explicit caching where appropriate. Without this, every page load hits your API.

Gotcha 2: Dynamic vs Static Rendering

Next.js decides at build time whether a page is static or dynamic. Anything that reads headers, cookies, or search params makes the entire route dynamic.

// This page is DYNAMIC (reads cookies)
export default async function Dashboard() {
  const session = await cookies();  // Forces dynamic rendering
  // ... rest of page
}

// This page is STATIC (no dynamic functions)
export default async function Blog() {
  const posts = await fetch('https://cms.example.com/posts', {
    next: { revalidate: 3600 }
  });
  // ... render posts
}

Get more insights on Tutorials

Join 2,000+ engineers who get our weekly deep-dives. No spam, unsubscribe anytime.

Check which pages are static vs dynamic after building:

next build
# Look for the output:
# ○  (Static)   /about
# ●  (SSG)      /blog
# λ  (Dynamic)  /dashboard
# ƒ  (Dynamic)  /api/webhook

Tip 1: Use Server Components by Default

Server components render on the server, send zero JavaScript to the client, and can directly access databases and APIs. Use them for everything that does not need interactivity.

// app/products/page.tsx (Server Component — default)
import { db } from '@/lib/db';

export default async function ProductsPage() {
  // Direct database access — no API layer needed
  const products = await db.query('SELECT * FROM products WHERE active = true');

  return (
    <div className="grid grid-cols-3 gap-4">
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

// components/ProductCard.tsx (Server Component)
function ProductCard({ product }: { product: Product }) {
  return (
    <div className="border rounded-lg p-4">
      <h3>{product.name}</h3>
      <p className="text-gray-600">{product.description}</p>
      <span className="font-bold">${product.price}</span>
      <AddToCartButton productId={product.id} />
    </div>
  );
}
// components/AddToCartButton.tsx (Client Component — needs onClick)
'use client';

export function AddToCartButton({ productId }: { productId }) {
  return (
    <button  => addToCart(productId)} className="btn-primary">
      Add to Cart
    </button>
  );
}

Rule: Only add 'use client' when you need useState, useEffect, onClick, or browser APIs.

Unoptimized Code — 2000ms+ Caching — 800ms+ CDN — 200msOptimized — 50msBaseline-60%-90%-97.5%

Performance optimization funnel: each layer of optimization compounds to dramatically reduce response times.

Tip 2: Optimize Images Aggressively

import Image from 'next/image';

// Good: Responsive with proper sizing
<Image
  src="/hero.jpg"
  alt="Hero"
  width={1200}
  height={630}
  priority              // LCP image — preload it
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px"
  quality={80}          // Default is 75, 80 is a good balance
/>

// For static export (output: 'export'), use unoptimized or a loader
// next.config.js
module.exports = {
  output: 'export',
  images: {
    unoptimized: true,  // For static hosting (nginx, S3)
    // Or use a CDN image loader
    // loader: 'custom',
    // loaderFile: './image-loader.ts',
  },
};

Tip 3: Bundle Size Analysis

# Install analyzer
npm install @next/bundle-analyzer

# next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({ /* your config */ });

# Run analysis
ANALYZE=true next build

Common bundle size wins:

  • Replace moment.js (300KB) with date-fns (tree-shakeable) or native Intl
  • Replace lodash with lodash-es or native methods
  • Dynamic import heavy components: const Chart = dynamic(() => import('./Chart'))
  • Move heavy client libs behind dynamic(..., { ssr: false })

Tip 4: ISR (Incremental Static Regeneration)

For pages that change periodically but do not need real-time data:

// app/blog/[slug]/page.tsx
export const revalidate = 3600;  // Revalidate every hour

export async function generateStaticParams() {
  const posts = await fetch('https://cms.example.com/posts')
    .then(r => r.json());

  return posts.map((post: any) => ({
    slug: post.slug,
  }));
}

export default async function BlogPost({ params }: { params: { slug } }) {
  const post = await fetch(`https://cms.example.com/posts/${params.slug}`, {
    next: { revalidate: 3600 }
  }).then(r => r.json());

  return <article>{/* render post */}</article>;
}

Tip 5: Streaming and Suspense

Stream long-running content instead of blocking the entire page:

Free Resource

Free Cloud Architecture Checklist

A 47-point checklist covering security, scalability, cost optimization, and disaster recovery for production cloud environments.

Download the Checklist
import { Suspense } from 'react';

export default function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>

      {/* This loads instantly */}
      <UserGreeting />

      {/* This streams in when ready */}
      <Suspense fallback={<ChartSkeleton />}>
        <AnalyticsChart />
      </Suspense>

      <Suspense fallback={<TableSkeleton />}>
        <RecentOrders />
      </Suspense>
    </div>
  );
}

async function AnalyticsChart() {
  // This takes 2-3 seconds
  const data = await fetchAnalytics();
  return <Chart data={data} />;
}

The page shell loads immediately while heavy components stream in. Users see content faster.

Tip 6: Metadata and SEO

import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: {
    template: '%s | TechSaaS',
    default: 'TechSaaS - Infrastructure for Modern Teams',
  },
  description: 'Self-hosted infrastructure...',
  openGraph: {
    type: 'website',
    siteName: 'TechSaaS',
    images: [{ url: '/og-image.png', width: 1200, height: 630 }],
  },
  robots: { index: true, follow: true },
};

// Per-page metadata
export async function generateMetadata({ params }): Promise<Metadata> {
  const post = await getPost(params.slug);
  return {
    title: post.title,
    description: post.description,
    openGraph: {
      title: post.title,
      description: post.description,
      type: 'article',
      publishedTime: post.date,
    },
  };
}
Terminal$docker compose up -d[+] Running 5/5Network app_default CreatedContainer web StartedContainer api StartedContainer db Started$

Docker Compose brings up your entire stack with a single command.

Production Deployment Checklist

  1. Run next build and check for static/dynamic page classification
  2. Verify no unnecessary dynamic rendering
  3. Add explicit caching to all fetch calls
  4. Analyze bundle size, eliminate large dependencies
  5. Set priority on above-the-fold images
  6. Use Suspense for slow data fetches
  7. Add proper metadata for SEO
  8. Enable compression in your reverse proxy (gzip/brotli)
  9. Set proper Cache-Control headers for static assets

At TechSaaS, our Next.js 15 site achieves a Lighthouse score of 95+ on all metrics running on a simple nginx:alpine container behind Traefik.

#nextjs#react#performance#production#web-development#tutorial

Related Service

Cloud Solutions

Let our experts help you build the right technology strategy for your business.

Need help with tutorials?

TechSaaS provides expert consulting and managed services for cloud infrastructure, DevOps, and AI/ML operations.

We Will Build You a Demo Site — For Free

Like it? Pay us. Do not like it? Walk away, zero complaints. You will spend way less than hiring developers or any agency.

47+ companies trusted us
99.99% uptime
< 48hr response

No spam. No contracts. Just a free demo.