#Tech#Web Development#Programming#Next.js#React

Next.js 15 Features

A comprehensive deep dive into Next.js 15 features, including Turbopack stability, Server Actions improvements, Partial Prerendering, and performance optimizations.

Next.js 15: The New Era of React Framework Performance

Next.js 15 represents a major leap forward in the React ecosystem, bringing production-ready features that developers have been eagerly anticipating. With Turbopack reaching stable status, enhanced Server Actions, and the revolutionary Partial Prerendering (PPR), this release transforms how we build modern web applications.

In this comprehensive guide, we'll explore every major feature, provide practical code examples, and show you how to migrate your existing Next.js applications to take full advantage of these improvements.

What's New in Next.js 15

Stability Milestones

Next.js 15 marks several critical stability milestones:

FeatureStatusImpact
TurbopackStableUp to 700x faster HMR, 10x faster builds
Server ActionsStableSimplified data mutations without API routes
Partial PrerenderingStableHybrid static + dynamic rendering
App RouterStableFull feature parity with Pages Router

Turbopack: The Future of Next.js Build Tool

Turbopack, written in Rust, replaces Webpack for development and production builds. After years of refinement, it's now stable and production-ready.

Performance Improvements

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // Turbopack is now default in Next.js 15
  // No configuration needed - it just works!
}

module.exports = nextConfig

Benchmarks:

  • Local Development HMR: Up to 700x faster than Webpack
  • Production Builds: Up to 10x faster
  • Startup Time: 4x faster
  • Memory Usage: 30% lower

Turbopack Advanced Configuration

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // Turbopack-specific optimizations
  experimental: {
    turbo: {
      // Memory limit for Turbopack (default: 8192 MB)
      memoryLimit: 16384,

      // Resolve aliases
      resolveAlias: {
        '@': './src',
        '@components': './src/components',
      },

      // Turbopack rules
      rules: {
        '*.svg': {
          loaders: ['@svgr/webpack'],
          as: '*.js',
        },
      },
    },
  },

  // Optimize CSS handling
  optimizeCss: true,
}

module.exports = nextConfig

Turbopack + SWC: The Perfect Pair

Next.js 15 uses SWC (Speedy Web Compiler) for minification and transformation, complementing Turbopack's bundling capabilities.

// next.config.js - SWC optimizations
/** @type {import('next').NextConfig} */
const nextConfig = {
  swcMinify: true, // Enabled by default

  compiler: {
    // Remove console.log in production
    removeConsole: process.env.NODE_ENV === 'production',

    // React Fast Refresh for HMR
    reactFastRefresh: true,

    // Remove PropTypes in production
    removePropTypes: true,
  },
}

module.exports = nextConfig

Server Actions: Server-Side Mutations Simplified

Server Actions, introduced in Next.js 13.4, are now fully stable and provide an elegant way to handle data mutations without creating API routes.

Basic Server Action

// app/actions.ts
'use server'

import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'

interface FormData {
  title: string
  content: string
  author: string
}

export async function createPost(formData: FormData) {
  // 1. Validate input
  if (!formData.title || !formData.content) {
    throw new Error('Title and content are required')
  }

  // 2. Perform server-side operation
  const post = await db.post.create({
    data: {
      title: formData.title,
      content: formData.content,
      author: formData.author,
    },
  })

  // 3. Revalidate cached data
  revalidatePath('/posts')
  revalidatePath(`/posts/${post.id}`)

  // 4. Optional: Redirect or return data
  redirect(`/posts/${post.id}`)

  // Or return data to client:
  // return { success: true, postId: post.id }
}

Using Server Actions in Forms

// app/posts/new/page.tsx
import { createPost } from '@/app/actions'

export default function NewPostPage() {
  return (
    <form action={createPost} className="space-y-4">
      <div>
        <label htmlFor="title" className="block text-sm font-medium">
          Title
        </label>
        <input
          id="title"
          name="title"
          type="text"
          required
          className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2"
        />
      </div>

      <div>
        <label htmlFor="content" className="block text-sm font-medium">
          Content
        </label>
        <textarea
          id="content"
          name="content"
          required
          rows={5}
          className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2"
        />
      </div>

      <button
        type="submit"
        className="rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
      >
        Create Post
      </button>
    </form>
  )
}

Server Actions with Client Components

// components/PostForm.tsx
'use client'

import { useState } from 'react'
import { createPost } from '@/app/actions'

export default function PostForm() {
  const [isSubmitting, setIsSubmitting] = useState(false)
  const [error, setError] = useState<string | null>(null)

  async function handleSubmit(formData: FormData) {
    setIsSubmitting(true)
    setError(null)

    try {
      await createPost(formData as any)
      // Server action handles redirect
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Failed to create post')
      setIsSubmitting(false)
    }
  }

  return (
    <form action={handleSubmit} className="space-y-4">
      {error && (
        <div className="rounded-md bg-red-50 p-4 text-red-700">
          {error}
        </div>
      )}

      {/* Form fields here */}

      <button
        type="submit"
        disabled={isSubmitting}
        className="rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50"
      >
        {isSubmitting ? 'Creating...' : 'Create Post'}
      </button>
    </form>
  )
}

Advanced Server Actions Patterns

// app/actions/advanced.ts
'use server'

import { revalidateTag } from 'next/cache'
import { headers } from 'next/headers'

// Action with error handling
export async function updateUser(formData: {
  id: string
  name: string
  email: string
}) {
  try {
    const user = await db.user.update({
      where: { id: formData.id },
      data: {
        name: formData.name,
        email: formData.email,
      },
    })

    revalidateTag('users')
    return { success: true, user }
  } catch (error) {
    if (error instanceof Error) {
      return { success: false, error: error.message }
    }
    return { success: false, error: 'Unknown error' }
  }
}

// Action with authentication
export async function deletePost(postId: string) {
  // Get session from headers
  const headersList = await headers()
  const sessionToken = headersList.get('x-session-token')

  if (!sessionToken) {
    throw new Error('Unauthorized')
  }

  // Validate session
  const session = await validateSession(sessionToken)
  if (!session) {
    throw new Error('Invalid session')
  }

  // Check permissions
  const post = await db.post.findUnique({ where: { id: postId } })
  if (!post || post.authorId !== session.userId) {
    throw new Error('Forbidden')
  }

  // Delete post
  await db.post.delete({ where: { id: postId } })

  // Revalidate
  revalidatePath('/posts')
  revalidatePath(`/posts/${postId}`)
  revalidateTag('posts')

  return { success: true }
}

// Streaming action
export async function processLargeDataset(datasetId: string) {
  const dataset = await db.dataset.findUnique({
    where: { id: datasetId },
  })

  if (!dataset) {
    throw new Error('Dataset not found')
  }

  // Process in chunks
  const CHUNK_SIZE = 1000
  let processed = 0

  while (processed < dataset.totalItems) {
    const chunk = await db.item.findMany({
      where: { datasetId },
      skip: processed,
      take: CHUNK_SIZE,
    })

    await Promise.all(
      chunk.map(async (item) => {
        await processItem(item)
      })
    )

    processed += chunk.length

    // Emit progress (if using streaming)
    emitProgress({
      datasetId,
      processed,
      total: dataset.totalItems,
    })
  }

  revalidateTag(`dataset-${datasetId}`)

  return { success: true, processed }
}

Server Action Validation with Zod

// lib/validations.ts
import { z } from 'zod'

export const createPostSchema = z.object({
  title: z.string().min(1, 'Title is required').max(200, 'Title too long'),
  content: z.string().min(1, 'Content is required').max(10000, 'Content too long'),
  authorId: z.string().uuid('Invalid author ID'),
  tags: z.array(z.string()).default([]),
  published: z.boolean().default(false),
})

export type CreatePostInput = z.infer<typeof createPostSchema>
// app/actions/validated.ts
'use server'

import { createPostSchema } from '@/lib/validations'
import { revalidatePath } from 'next/cache'

export async function createPostWithValidation(data: unknown) {
  // Validate input
  const result = createPostSchema.safeParse(data)

  if (!result.success) {
    // Return validation errors
    return {
      success: false,
      errors: result.error.flatten().fieldErrors,
    }
  }

  // Create post with validated data
  const post = await db.post.create({
    data: result.data,
  })

  revalidatePath('/posts')

  return {
    success: true,
    post,
  }
}

Partial Prerendering (PPR): The Best of Both Worlds

Partial Prerendering is a groundbreaking feature that allows you to combine static and dynamic rendering in the same page. This means you can serve a fast static shell while hydrating dynamic content in the background.

Understanding PPR

PPR works by:

  1. Rendering the static shell at build time (instant delivery)
  2. Streaming dynamic content on demand (fresh data)
  3. Progressive enhancement (user can interact immediately)

Enabling Partial Prerendering

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // Enable Partial Prerendering
  experimental: {
    ppr: 'incremental', // or 'true' for PPR everywhere
  },
}

module.exports = nextConfig

PPR in Action

// app/dashboard/page.tsx
import { Suspense } from 'react'
import { PostList } from '@/components/PostList'
import { UserProfile } from '@/components/UserProfile'
import { TrendingPosts } from '@/components/TrendingPosts'

// Static shell - rendered at build time
export const dynamic = 'force-static' // This overrides PPR for specific pages

// OR use PPR for this page
export const dynamic = 'auto' // PPR will apply

export default function DashboardPage() {
  return (
    <div className="container mx-auto p-6">
      {/* Static content - instant load */}
      <header>
        <h1 className="text-3xl font-bold">Dashboard</h1>
        <p className="text-gray-600">Welcome back!</p>
      </header>

      <div className="mt-6 grid grid-cols-1 gap-6 lg:grid-cols-3">
        {/* Left column: Static sidebar */}
        <aside className="lg:col-span-1">
          <nav className="space-y-2">
            <a href="/dashboard" className="block rounded-md bg-blue-600 px-4 py-2 text-white">
              Overview
            </a>
            <a href="/dashboard/posts" className="block rounded-md px-4 py-2 hover:bg-gray-100">
              Posts
            </a>
            <a href="/dashboard/analytics" className="block rounded-md px-4 py-2 hover:bg-gray-100">
              Analytics
            </a>
          </nav>
        </aside>

        {/* Right column: Dynamic content */}
        <main className="lg:col-span-2 space-y-6">
          {/* Streaming content with Suspense boundaries */}
          <Suspense fallback={<div className="animate-pulse h-64 bg-gray-200 rounded" />}>
            <UserProfile />
          </Suspense>

          <Suspense fallback={<div className="animate-pulse h-64 bg-gray-200 rounded" />}>
            <TrendingPosts />
          </Suspense>

          <Suspense fallback={<div className="animate-pulse h-64 bg-gray-200 rounded" />}>
            <PostList />
          </Suspense>
        </main>
      </div>
    </div>
  )
}

PPR with Route Segments

// app/products/[slug]/page.tsx
import { ProductDetails } from '@/components/ProductDetails'
import { ProductReviews } from '@/components/ProductReviews'
import { RelatedProducts } from '@/components/RelatedProducts'

export default function ProductPage({ params }: { params: { slug: string } }) {
  return (
    <div>
      {/* Static shell: product basics cached */}
      <ProductDetails slug={params.slug} />

      {/* Dynamic content: streamed on demand */}
      <Suspense fallback={<div>Loading reviews...</div>}>
        <ProductReviews slug={params.slug} />
      </Suspense>

      <Suspense fallback={<div>Loading related products...</div>}>
        <RelatedProducts slug={params.slug} />
      </Suspense>
    </div>
  )
}

PPR Performance Comparison

// Without PPR (dynamic)
// - Initial HTML: 2.3s
// - First Contentful Paint: 2.5s
// - Time to Interactive: 3.1s

// With PPR (static + dynamic streaming)
// - Initial HTML: 0.1s (static shell)
// - First Contentful Paint: 0.2s
// - Time to Interactive: 0.3s
// - Dynamic content: Streams in over 0.5s

Enhanced App Router Features

Next.js 15 brings several enhancements to the App Router, improving developer experience and performance.

Improved Caching Controls

// app/posts/page.tsx
import { Suspense } from 'react'

// Revalidate after 60 seconds
export const revalidate = 60

// OR revalidate on-demand using tags
export async function generateMetadata() {
  return {
    title: 'Blog Posts',
  }
}

async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    // Cache options
    next: {
      tags: ['posts'], // Tag for revalidation
      revalidate: 3600, // Revalidate every hour
    },
  })

  if (!res.ok) throw new Error('Failed to fetch posts')
  return res.json()
}

export default async function PostsPage() {
  const posts = await getPosts()

  return (
    <div>
      <h1>Blog Posts</h1>
      <ul>
        {posts.map((post: any) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  )
}

Optimistic Updates with useOptimistic

// app/todos/page.tsx
'use client'

import { useOptimistic, useTransition } from 'react'
import { addTodo, deleteTodo } from '@/app/actions'

interface Todo {
  id: string
  text: string
  completed: boolean
}

export default function TodosPage({ initialTodos }: { initialTodos: Todo[] }) {
  const [isPending, startTransition] = useTransition()
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    initialTodos,
    (state, action: { type: 'ADD' | 'DELETE'; todo?: Todo; id?: string }) => {
      if (action.type === 'ADD' && action.todo) {
        return [...state, { ...action.todo, id: 'temp-id' }]
      }
      if (action.type === 'DELETE' && action.id) {
        return state.filter((todo) => todo.id !== action.id)
      }
      return state
    }
  )

  function handleAddTodo(formData: FormData) {
    const text = formData.get('text') as string

    startTransition(() => {
      addOptimisticTodo({
        type: 'ADD',
        todo: { id: 'temp', text, completed: false },
      })
      addTodo(text)
    })
  }

  function handleDeleteTodo(id: string) {
    startTransition(() => {
      addOptimisticTodo({ type: 'DELETE', id })
      deleteTodo(id)
    })
  }

  return (
    <div>
      <form action={handleAddTodo} className="space-y-2">
        <input
          name="text"
          type="text"
          placeholder="Add a todo..."
          className="rounded-md border px-3 py-2"
        />
        <button
          type="submit"
          disabled={isPending}
          className="rounded-md bg-blue-600 px-4 py-2 text-white disabled:opacity-50"
        >
          Add Todo
        </button>
      </form>

      <ul className="mt-4 space-y-2">
        {optimisticTodos.map((todo) => (
          <li key={todo.id} className="flex items-center justify-between">
            <span>{todo.text}</span>
            <button
              onClick={() => handleDeleteTodo(todo.id)}
              className="text-red-600 hover:text-red-700"
            >
              Delete
            </button>
          </li>
        ))}
      </ul>
    </div>
  )
}

Improved Error Handling

// app/error.tsx
'use client'

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <div className="flex h-screen flex-col items-center justify-center">
      <h2 className="text-2xl font-bold">Something went wrong!</h2>
      <p className="text-gray-600">{error.message}</p>
      <button
        onClick={reset}
        className="mt-4 rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
      >
        Try again
      </button>
    </div>
  )
}
// app/not-found.tsx
import Link from 'next/link'

export default function NotFound() {
  return (
    <div className="flex h-screen flex-col items-center justify-center">
      <h1 className="text-4xl font-bold">404 - Page Not Found</h1>
      <p className="mt-2 text-gray-600">
        The page you're looking for doesn't exist.
      </p>
      <Link
        href="/"
        className="mt-4 rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700"
      >
        Go Home
      </Link>
    </div>
  </div>
)

TypeScript Improvements

Next.js 15 includes enhanced TypeScript support with better type inference and developer experience.

Improved Route Parameter Types

// app/posts/[slug]/page.tsx
import { notFound } from 'next/navigation'

// Better type inference for params
export default async function PostPage({
  params,
  searchParams,
}: {
  params: { slug: string }
  searchParams: { [key: string]: string | string[] | undefined }
}) {
  // params.slug is properly typed as string
  const post = await getPost(params.slug)

  if (!post) {
    notFound()
  }

  return <div>{post.title}</div>
}

Typed Server Actions

// app/actions.ts
'use server'

import { z } from 'zod'

const createPostSchema = z.object({
  title: z.string(),
  content: z.string(),
})

export async function createPost(input: z.infer<typeof createPostSchema>) {
  // input is properly typed!
  const { title, content } = input

  await db.post.create({ data: { title, content } })
}

Performance Optimizations

Built-in Image Optimization

import Image from 'next/image'

export default function GalleryPage() {
  return (
    <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
      {images.map((image) => (
        <div key={image.id}>
          <Image
            src={image.url}
            alt={image.alt}
            width={400}
            height={300}
            loading="lazy"
            className="rounded-md"
            // New in Next.js 15: priority loading for above-the-fold images
            priority={false}
            // New: automatic blur placeholder
            placeholder="blur"
            blurDataURL={image.blurDataURL}
          />
        </div>
      ))}
    </div>
  )
}

Font Optimization

// app/layout.tsx
import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
  display: 'swap', // Better performance
})

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className={`${inter.variable} font-sans`}>{children}</body>
    </html>
  )
}

Script Optimization

import Script from 'next/script'

export default function Analytics() {
  return (
    <>
      {/* Load Google Analytics */}
      <Script
        src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"
        strategy="afterInteractive"
      />

      <Script id="google-analytics" strategy="afterInteractive">
        {`
          window.dataLayer = window.dataLayer || [];
          function gtag(){dataLayer.push(arguments);}
          gtag('js', new Date());
          gtag('config', 'GA_MEASUREMENT_ID');
        `}
      </Script>

      {/* Load analytics after page load */}
      <Script src="/analytics.js" strategy="lazyOnload" />
    </>
  )
}

Migration Guide: Upgrading from Next.js 14

Step 1: Update Dependencies

npm install next@15 react@18 react-dom@18
npm install -D @types/react@18 @types/react-dom@18

Step 2: Update next.config.js

// next.config.js (Next.js 14)
/** @type {import('next').NextConfig} */
const nextConfig = {
  // Old config
  webpack: (config) => {
    // Custom webpack config
    return config
  },
}

module.exports = nextConfig
// next.config.js (Next.js 15)
/** @type {import('next').NextConfig} */
const nextConfig = {
  // Turbopack is now default for dev
  // No webpack config needed for most cases

  // If you still need webpack, wrap it in a conditional
  webpack: (config, { isServer }) => {
    // Custom webpack config (only runs when not using Turbopack)
    return config
  },

  // Enable experimental features
  experimental: {
    ppr: 'incremental', // Enable Partial Prerendering
  },
}

module.exports = nextConfig

Step 3: Update Server Actions

// Before (Next.js 13/14)
export async function createPost(formData: FormData) {
  // Implementation
}

// After (Next.js 15 - recommended)
import { revalidatePath } from 'next/cache'

export async function createPost(formData: FormData) {
  // Add revalidation
  const post = await db.post.create({ data: { title: '...' } })
  revalidatePath('/posts')
  return post
}

Step 4: Update API Routes to Server Actions

// pages/api/posts.ts (Old API route)
import type { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method === 'POST') {
    const { title, content } = req.body
    const post = await db.post.create({ data: { title, content } })
    res.status(201).json(post)
  }
}
// app/actions/posts.ts (New Server Action)
'use server'

export async function createPost(formData: { title: string; content: string }) {
  const post = await db.post.create({ data: formData })
  revalidatePath('/posts')
  return post
}

Real-World Examples

E-Commerce Product Page with PPR

// app/products/[id]/page.tsx
import { ProductHeader } from '@/components/products/ProductHeader'
import { ProductGallery } from '@/components/products/ProductGallery'
import { ProductReviews } from '@/components/products/ProductReviews'
import { RelatedProducts } from '@/components/products/RelatedProducts'
import { AddToCartButton } from '@/components/products/AddToCartButton'

export default function ProductPage({ params }: { params: { id: string } }) {
  return (
    <div className="container mx-auto px-4 py-8">
      {/* Static shell - fast initial load */}
      <div className="grid grid-cols-1 gap-8 lg:grid-cols-2">
        <div>
          <ProductHeader id={params.id} />
          <ProductGallery id={params.id} />
        </div>

        <div className="space-y-6">
          <AddToCartButton productId={params.id} />

          {/* Dynamic content - streamed */}
          <Suspense fallback={<div className="animate-pulse h-64 bg-gray-200 rounded" />}>
            <ProductReviews id={params.id} />
          </Suspense>

          <Suspense fallback={<div className="animate-pulse h-64 bg-gray-200 rounded" />}>
            <RelatedProducts id={params.id} />
          </Suspense>
        </div>
      </div>
    </div>
  )
}

Real-time Dashboard with Server Actions

// app/dashboard/analytics/page.tsx
import { Suspense } from 'react'
import { AnalyticsChart } from '@/components/analytics/AnalyticsChart'
import { MetricsCards } from '@/components/analytics/MetricsCards'
import { RecentActivity } from '@/components/analytics/RecentActivity'

export default function AnalyticsPage() {
  return (
    <div className="space-y-6">
      {/* Static header */}
      <header>
        <h1 className="text-3xl font-bold">Analytics Dashboard</h1>
        <p className="text-gray-600">Real-time performance metrics</p>
      </header>

      {/* Streaming analytics data */}
      <Suspense fallback={<div className="animate-pulse h-32 bg-gray-200 rounded" />}>
        <MetricsCards />
      </Suspense>

      <Suspense fallback={<div className="animate-pulse h-64 bg-gray-200 rounded" />}>
        <AnalyticsChart />
      </Suspense>

      <Suspense fallback={<div className="animate-pulse h-48 bg-gray-200 rounded" />}>
        <RecentActivity />
      </Suspense>
    </div>
  )
}
// app/actions/analytics.ts
'use server'

import { revalidateTag } from 'next/cache'

// Update analytics in real-time
export async function updateAnalytics(event: {
  type: 'view' | 'click' | 'purchase'
  userId: string
  page?: string
}) {
  // Track event
  await db.analytics.create({
    data: {
      type: event.type,
      userId: event.userId,
      page: event.page,
      timestamp: new Date(),
    },
  })

  // Revalidate analytics dashboard
  revalidateTag('analytics')

  return { success: true }
}

Blog with Incremental Static Regeneration

// app/blog/page.tsx
export const revalidate = 3600 // Revalidate every hour

export async function generateMetadata() {
  const posts = await getPosts()
  return {
    title: `Blog - ${posts.length} Articles`,
    description: 'Latest articles and tutorials',
  }
}

async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: {
      tags: ['posts'],
      revalidate: 3600,
    },
  })

  if (!res.ok) throw new Error('Failed to fetch posts')
  return res.json()
}

export default async function BlogPage() {
  const posts = await getPosts()

  return (
    <div>
      <h1 className="text-3xl font-bold">Blog</h1>
      <ul className="mt-6 space-y-4">
        {posts.map((post: any) => (
          <li key={post.id}>
            <h2 className="text-xl font-semibold">{post.title}</h2>
            <p className="text-gray-600">{post.excerpt}</p>
          </li>
        ))}
      </ul>
    </div>
  )
}
// app/actions/blog.ts
'use server'

import { revalidateTag } from 'next/cache'

// Trigger revalidation when a new post is published
export async function publishPost(postId: string) {
  await db.post.update({
    where: { id: postId },
    data: { published: true },
  })

  revalidateTag('posts')

  return { success: true }
}

Best Practices for Next.js 15

1. Use Server Actions for Data Mutations

// ✅ Good: Server Action
export async function createUser(formData: FormData) {
  const user = await db.user.create({ data: { ...formData } })
  revalidatePath('/users')
  return user
}

// ❌ Bad: API route (unnecessary with Server Actions)
// app/api/users/route.ts
export async function POST(req: NextRequest) {
  const data = await req.json()
  const user = await db.user.create({ data })
  return NextResponse.json(user, { status: 201 })
}

2. Leverage Partial Prerendering

// ✅ Good: Use Suspense for dynamic content
export default function Page() {
  return (
    <div>
      <StaticHeader />
      <Suspense fallback={<Loading />}>
        <DynamicContent />
      </Suspense>
    </div>
  )
}

// ❌ Bad: Entire page dynamic
export const dynamic = 'force-dynamic'
export default async function Page() {
  const data = await fetch('https://api.example.com/data')
  return <div>{/* ... */}</div>
}

3. Optimize Images Properly

// ✅ Good: Next.js Image with proper sizing
<Image
  src="/hero.jpg"
  alt="Hero section"
  width={1200}
  height={600}
  priority // Above the fold
  placeholder="blur"
/>

// ❌ Bad: Regular img tag
<img src="/hero.jpg" alt="Hero section" />

4. Use Appropriate Caching Strategies

// ✅ Good: Appropriate revalidation
const staticData = await fetch('/api/static', {
  next: { revalidate: 3600 }, // 1 hour
})

const realtimeData = await fetch('/api/realtime', {
  next: { revalidate: 0 }, // No caching
})

const tagBasedData = await fetch('/api/data', {
  next: { tags: ['data'] }, // Revalidate on demand
})

// ❌ Bad: Always no cache
const data = await fetch('/api/data', { cache: 'no-store' })

5. Use Turbopack for Development

# ✅ Good: Use Turbopack (default in Next.js 15)
next dev

# ❌ Bad: Force Webpack
next dev --turbo=false

Performance Tips

1. Minimize Client-Side JavaScript

// ✅ Good: Server Components by default
export default async function Page() {
  const data = await fetchData()
  return <div>{/* Server-rendered content */}</div>
}

// ❌ Bad: Unnecessary Client Component
'use client'
export default function Page() {
  const [data, setData] = useState(null)
  useEffect(() => {
    fetchData().then(setData)
  }, [])
  return <div>{/* Client-side rendering */}</div>
}

2. Use Streaming for Large Data

export default function Page() {
  return (
    <Suspense fallback={<Loading />}>
      <LargeDataList />
    </Suspense>
  )
}

3. Optimize Font Loading

// ✅ Good: Use next/font
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })

// ❌ Bad: External font loading
<link href="https://fonts.googleapis.com/css2?family=Inter" rel="stylesheet" />

Troubleshooting Common Issues

Issue 1: Turbopack Build Failures

# Clear Turbopack cache
rm -rf .next

# Force rebuild
next build --turbo

Issue 2: Server Actions Not Working

// Make sure file has 'use server' at the top
'use server'

export async function myAction() {
  // ...
}

Issue 3: PPR Not Working

// Ensure PPR is enabled in next.config.js
experimental: {
  ppr: 'incremental',
}

// Use Suspense boundaries
<Suspense fallback={<Loading />}>
  <DynamicContent />
</Suspense>

Conclusion

Next.js 15 represents a significant evolution in web development, combining the performance of static generation with the flexibility of dynamic rendering. With Turbopack's stability, Server Actions maturity, and Partial Prerendering's revolutionary approach, developers now have the tools to build incredibly fast, scalable applications.

Key Takeaways

  1. Turbopack is production-ready - Experience dramatically faster development and builds
  2. Server Actions simplify data mutations - Eliminate the need for API routes
  3. Partial Prerendering offers the best of both worlds - Static shell with dynamic content streaming
  4. Enhanced TypeScript support - Better type inference and developer experience
  5. Performance optimizations built-in - From image optimization to intelligent caching

Next Steps

  1. Upgrade your Next.js application to version 15
  2. Enable Turbopack for development
  3. Migrate API routes to Server Actions
  4. Implement Partial Prerendering for suitable pages
  5. Optimize images and fonts
  6. Set up proper caching strategies
  7. Monitor performance and iterate

The future of web development is here, and Next.js 15 is leading the charge. Embrace these features, optimize your applications, and deliver exceptional user experiences at scale.


Ready to supercharge your Next.js applications? Start upgrading today and experience the performance improvements firsthand.