Next.js Advanced Patterns: Server Actions, Performance Optimization & Styling
Elevate your Next.js application beyond basic routing. This deep dive covers handling data mutations with Server Actions, achieving perfect Core Web Vitals via Image and Font optimization, and architecting scalable styling solutions using Tailwind and CSS Modules.
Server Actions
Server Actions overview
Server Actions are asynchronous functions that execute on the server, enabling you to handle form submissions and data mutations without creating separate API endpoints—they're Next.js's answer to the traditional REST API pattern for mutations, allowing you to call server-side code directly from components.
┌─────────────────┐ ┌─────────────────┐
│ Client Component│ ──────▶│ Server Action │
│ (Form Submit) │ │ (runs on server)│
└─────────────────┘ └────────┬────────┘
│
▼
┌─────────────────┐
│ Database │
└─────────────────┘
'use server' directive
The 'use server' directive marks a function or file as server-only code, telling Next.js to create a secure endpoint that clients can call—it must be at the top of an async function or at the top of a file to mark all exports as Server Actions.
// In a file or at function level 'use server' export async function createUser(formData) { const name = formData.get('name') await db.user.create({ data: { name } }) }
Actions in Server Components
In Server Components, you can define Server Actions inline within the component body since the entire component already runs on the server—just add 'use server' at the top of the async function.
// app/page.js (Server Component) export default function Page() { async function submitForm(formData) { 'use server' await db.posts.create({ title: formData.get('title') }) } return <form action={submitForm}>...</form> }
Actions in Client Components
Client Components cannot define Server Actions inline; instead, they must import actions from a separate file marked with 'use server' at the top, maintaining the security boundary between client and server code.
// actions.js 'use server' export async function saveData(data) { /* ... */ } // ClientComponent.jsx 'use client' import { saveData } from './actions' export default function Form() { return <form action={saveData}>...</form> }
Form actions
The action prop on HTML <form> elements can directly accept a Server Action, enabling native form submissions that work without JavaScript and automatically serialize FormData to be passed to your server function.
<form action={async (formData) => { 'use server' const email = formData.get('email') await subscribe(email) }}> <input name="email" type="email" /> <button type="submit">Subscribe</button> </form>
useFormState hook
useFormState manages form state by wrapping a Server Action, providing access to the action's return value (like validation errors or success messages) and enabling you to show feedback based on the server response.
'use client' import { useFormState } from 'react-dom' import { createUser } from './actions' const initialState = { message: null } export default function Form() { const [state, formAction] = useFormState(createUser, initialState) return ( <form action={formAction}> <input name="name" /> {state?.message && <p>{state.message}</p>} </form> ) }
useFormStatus hook
useFormStatus provides pending state information for the parent form, allowing you to show loading indicators or disable buttons during form submission—it must be used in a component that's a child of the form element.
'use client' import { useFormStatus } from 'react-dom' function SubmitButton() { const { pending } = useFormStatus() return ( <button disabled={pending}> {pending ? 'Saving...' : 'Save'} </button> ) }
Progressive enhancement
Server Actions work without JavaScript enabled because they use native HTML form behavior, meaning your forms will function even before hydration completes or if JS fails to load—the experience simply enhances when JavaScript is available.
Without JS: Form Submit → Full Page Reload → Server Processes → New Page
With JS: Form Submit → AJAX Request → Server Processes → UI Updates
Revalidation with actions
After mutating data with a Server Action, you can refresh cached data using revalidatePath() to invalidate a specific route's cache or revalidateTag() to invalidate data by cache tag.
'use server' import { revalidatePath, revalidateTag } from 'next/cache' export async function createPost(formData) { await db.post.create({ /* ... */ }) revalidatePath('/posts') // Invalidate specific path revalidateTag('posts') // Or invalidate by tag }
Error handling
Errors in Server Actions can be caught using try/catch blocks and returned as part of the state, or you can let them propagate to the nearest error.js boundary for automatic error UI handling.
'use server' export async function createUser(prevState, formData) { try { await db.user.create({ name: formData.get('name') }) return { success: true } } catch (error) { return { error: 'Failed to create user' } } }
Optimistic updates
Use the useOptimistic hook to immediately update the UI before the Server Action completes, providing instant feedback while the actual mutation happens in the background—if the action fails, the UI automatically reverts.
'use client' import { useOptimistic } from 'react' function TodoList({ todos, addTodo }) { const [optimisticTodos, addOptimisticTodo] = useOptimistic( todos, (state, newTodo) => [...state, { ...newTodo, pending: true }] ) // Use optimisticTodos for rendering }
Server Actions with forms
The complete pattern combines form elements with Server Actions, useFormState for response handling, and useFormStatus for loading states—creating a full-featured, progressively enhanced form experience.
// Complete pattern <form action={formAction}> <input name="email" required /> <SubmitButton /> {/* Uses useFormStatus */} {state?.error && <Error />} {/* From useFormState */} </form>
Image Optimization
next/image component
The next/image component automatically optimizes images by lazy-loading, resizing, and serving modern formats—it prevents layout shift by requiring dimensions and generates multiple sizes for responsive delivery.
import Image from 'next/image' export default function Hero() { return ( <Image src="/hero.jpg" alt="Hero image" width={1200} height={600} /> ) }
Image component props
The Image component accepts essential props like src, alt, width, height, plus optimization props like quality, priority, placeholder, and layout props like fill and sizes—each controlling different aspects of image delivery and display.
┌─────────────────────────────────────────────┐
│ Image Props │
├──────────────┬──────────────────────────────┤
│ Required │ src, alt, width/height OR fill│
│ Optimization │ quality, priority, loading │
│ Layout │ fill, sizes, style │
│ Placeholder │ placeholder, blurDataURL │
└──────────────┴──────────────────────────────┘
width and height
Width and height props define the rendered size in pixels and are required for static images to prevent Cumulative Layout Shift (CLS)—they represent the intrinsic dimensions or desired display size, not the actual file dimensions.
<Image src="/photo.jpg" alt="Photo" width={800} // Rendered width in pixels height={600} // Rendered height in pixels />
fill prop
The fill prop makes the image fill its parent container (which must have position: relative), eliminating the need for explicit width/height—useful when you don't know dimensions ahead of time or want fluid responsive images.
<div style={{ position: 'relative', width: '100%', height: '400px' }}> <Image src="/banner.jpg" alt="Banner" fill style={{ objectFit: 'cover' }} /> </div>
sizes prop
The sizes prop tells the browser how wide the image will be at different viewport sizes, enabling it to download the appropriately sized image—critical for performance when using fill or responsive layouts.
<Image src="/hero.jpg" alt="Hero" fill sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" /> // Mobile: 100% viewport, Tablet: 50%, Desktop: 33%
priority prop
Setting priority={true} preloads the image and disables lazy loading, making it load immediately—use this for above-the-fold images like hero banners or LCP (Largest Contentful Paint) elements.
<Image src="/hero.jpg" alt="Hero banner" width={1920} height={1080} priority // Preloads, no lazy loading />
quality prop
The quality prop controls the optimization quality from 1-100 (default 75), letting you balance file size against visual quality—lower values mean smaller files but more compression artifacts.
<Image src="/photo.jpg" alt="High quality photo" width={800} height={600} quality={90} // Higher quality, larger file />
placeholder prop
The placeholder prop specifies what to show while the image loads—"empty" (default) shows nothing, "blur" shows a blurred preview which requires blurDataURL for remote images or is auto-generated for static imports.
import heroImage from './hero.jpg' // Static import <Image src={heroImage} alt="Hero" placeholder="blur" // Auto-generated blur for static imports />
blurDataURL
blurDataURL is a base64-encoded tiny image shown as the blur placeholder while the full image loads—required when using placeholder="blur" with remote images, typically a 10x10 pixel or smaller data URI.
<Image src="https://example.com/photo.jpg" alt="Remote photo" width={800} height={600} placeholder="blur" blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..." />
Image formats (WebP, AVIF)
Next.js automatically serves modern formats like WebP and AVIF to browsers that support them, falling back to JPEG/PNG for older browsers—AVIF offers ~50% smaller files than JPEG, WebP ~30% smaller.
┌─────────────┬─────────────┬──────────────────┐
│ Format │ Size vs JPEG│ Browser Support │
├─────────────┼─────────────┼──────────────────┤
│ AVIF │ ~50% smaller│ Chrome, Firefox │
│ WebP │ ~30% smaller│ All modern │
│ JPEG │ baseline │ Universal │
└─────────────┴─────────────┴──────────────────┘
Responsive images
Next.js generates multiple image sizes and uses srcset automatically, serving appropriately sized images based on device width—combine with the sizes prop to optimize delivery for different viewport widths.
// Generates srcset with multiple sizes automatically <Image src="/photo.jpg" alt="Responsive photo" fill sizes="(max-width: 640px) 100vw, 640px" /> // Output includes: 640w, 750w, 828w, 1080w, etc.
Image loader
The loader is a function that generates image URLs, allowing you to use external image services—Next.js provides a default loader for its built-in optimization, but you can customize it for CDNs like Cloudinary or Imgix.
const cloudinaryLoader = ({ src, width, quality }) => { return `https://res.cloudinary.com/demo/image/upload/w_${width},q_${quality || 75}/${src}` } <Image loader={cloudinaryLoader} src="sample.jpg" alt="Cloudinary image" width={500} height={300} />
Custom image loaders
Configure a custom default loader in next.config.js using the loader option, or create a per-image loader function—useful when all images come from a specific CDN or optimization service.
// next.config.js module.exports = { images: { loader: 'custom', loaderFile: './lib/image-loader.js', }, } // lib/image-loader.js export default function myLoader({ src, width, quality }) { return `https://my-cdn.com/${src}?w=${width}&q=${quality || 75}` }
Remote images
Remote images require explicit width/height (or fill) since Next.js can't analyze them at build time, and their domains must be configured in next.config.js for security—this prevents arbitrary external images from being optimized.
// next.config.js module.exports = { images: { domains: ['example.com', 'cdn.example.com'], }, } // Usage <Image src="https://example.com/photo.jpg" alt="Remote" width={800} height={600} />
Image domains configuration
The domains array in next.config.js whitelists external hostnames for image optimization—this is the simpler but less secure option compared to remotePatterns, accepting any path from allowed domains.
// next.config.js module.exports = { images: { domains: [ 'images.unsplash.com', 'cdn.shopify.com', 's3.amazonaws.com', ], }, }
remotePatterns
remotePatterns provides fine-grained control over remote images by specifying protocol, hostname, port, and pathname patterns—more secure than domains because you can restrict to specific paths.
// next.config.js module.exports = { images: { remotePatterns: [ { protocol: 'https', hostname: 'cdn.example.com', port: '', pathname: '/images/**', // Only allow /images/ path }, ], }, }
Image optimization API
Next.js provides a built-in image optimization API at /_next/image that resizes, optimizes, and caches images on-demand—it accepts url, w (width), and q (quality) parameters and can be customized or replaced with external services.
GET /_next/image?url=/hero.jpg&w=640&q=75
┌──────────────┐ ┌─────────────────┐ ┌──────────┐
│ Browser │───▶│ /_next/image │───▶│ Optimized│
│ requests 640w│ │ API optimizes │ │ WebP/AVIF│
└──────────────┘ └─────────────────┘ └──────────┘
Font Optimization
next/font
next/font is Next.js's built-in font optimization system that automatically self-hosts fonts, eliminates external network requests, removes layout shift with size-adjust, and includes only the characters you use through automatic subsetting.
import { Inter } from 'next/font/google' const inter = Inter({ subsets: ['latin'] }) export default function RootLayout({ children }) { return ( <html className={inter.className}> <body>{children}</body> </html> ) }
next/font/google
Import and configure Google Fonts with zero layout shift and no external requests—fonts are downloaded at build time and self-hosted with your static assets, improving privacy and performance.
import { Roboto, Open_Sans } from 'next/font/google' const roboto = Roboto({ weight: ['400', '700'], subsets: ['latin'], display: 'swap', }) // Use: <div className={roboto.className}>
next/font/local
Load custom font files from your project using next/font/local—useful for proprietary fonts, self-hosted fonts, or when you need complete control over font files.
import localFont from 'next/font/local' const myFont = localFont({ src: [ { path: './fonts/Custom-Regular.woff2', weight: '400' }, { path: './fonts/Custom-Bold.woff2', weight: '700' }, ], display: 'swap', })
Font loading strategies
Next.js fonts support different loading strategies through the display option—controlling how text appears while fonts load: swap shows fallback immediately, block hides text briefly, optional may skip the font on slow connections.
┌─────────────┬────────────────────────────────────┐
│ display │ Behavior │
├─────────────┼────────────────────────────────────┤
│ 'swap' │ Fallback → Custom font (flash) │
│ 'block' │ Invisible → Custom font │
│ 'fallback' │ Brief invisible → swap/keep fallback│
│ 'optional' │ May use fallback on slow networks │
└─────────────┴────────────────────────────────────┘
Font display
The display option maps to CSS font-display and controls the font loading behavior—'swap' (recommended) ensures text is always visible, showing the fallback font until the custom font loads.
const inter = Inter({ subsets: ['latin'], display: 'swap', // Show fallback, then swap when loaded })
Font subsetting
Subsetting includes only the character sets you specify, dramatically reducing font file size—for example, latin is much smaller than including latin-extended, cyrillic, and greek.
const roboto = Roboto({ weight: '400', subsets: ['latin'], // Only Latin characters // subsets: ['latin', 'greek'] // Multiple subsets })
Preloading fonts
Next.js automatically preloads fonts that are used on the current page by adding <link rel="preload"> to the document head—this ensures fonts start downloading immediately without waiting for CSS parsing.
<!-- Automatically added by next/font --> <link rel="preload" href="/_next/static/media/inter.woff2" as="font" type="font/woff2" crossorigin />
Variable fonts
Variable fonts contain multiple weights/styles in a single file, and Next.js supports them with weight ranges—this reduces file count while providing design flexibility.
import { Inter } from 'next/font/google' const inter = Inter({ subsets: ['latin'], // Variable font: specify range instead of discrete weights weight: '100 900', // Or omit for full range })
Font fallbacks
The fallback option specifies backup fonts to use while the custom font loads or if it fails—combined with adjustFontFallback, Next.js can adjust fallback metrics to minimize layout shift.
const myFont = localFont({ src: './CustomFont.woff2', fallback: ['Helvetica', 'Arial', 'sans-serif'], adjustFontFallback: 'Arial', // Adjust Arial metrics to match })
CSS variables for fonts
Use the variable option to generate a CSS custom property for your font, enabling flexible usage in CSS files or with Tailwind CSS—useful when you can't apply className directly.
const inter = Inter({ subsets: ['latin'], variable: '--font-inter', // Creates CSS variable }) // Usage in layout <html className={inter.variable}> // In CSS body { font-family: var(--font-inter), sans-serif; }
Script Optimization
next/script component
The next/script component optimizes third-party script loading with built-in strategies for timing and performance—it handles deduplication, proper ordering, and works with both pages and app routers.
import Script from 'next/script' export default function Page() { return ( <> <Script src="https://example.com/analytics.js" /> <h1>My Page</h1> </> ) }
Script loading strategies (beforeInteractive, afterInteractive, lazyOnload)
Three strategies control when scripts load: beforeInteractive loads before hydration (critical scripts), afterInteractive (default) loads after hydration, and lazyOnload loads during idle time (low-priority scripts).
┌───────────────────┬─────────────────────────────────────┐
│ Strategy │ When it loads │
├───────────────────┼─────────────────────────────────────┤
│ beforeInteractive │ Before page hydrates (blocking) │
│ afterInteractive │ After page becomes interactive │
│ lazyOnload │ During browser idle time │
│ worker │ In a web worker (experimental) │
└───────────────────┴─────────────────────────────────────┘
<Script src="/critical.js" strategy="beforeInteractive" /> <Script src="/analytics.js" strategy="afterInteractive" /> <Script src="/widget.js" strategy="lazyOnload" />
Inline scripts
Next/script supports inline JavaScript using the dangerouslySetInnerHTML prop or children—use id prop for proper deduplication and tracking of inline scripts.
<Script id="show-banner" strategy="afterInteractive"> {`document.getElementById('banner').classList.remove('hidden')`} </Script> // Or with dangerouslySetInnerHTML <Script id="analytics-init" dangerouslySetInnerHTML={{ __html: `window.dataLayer = window.dataLayer || [];`, }} />
Third-party scripts
Optimize common third-party scripts like analytics, chat widgets, and social media embeds by placing them in layout files for site-wide inclusion or in specific pages, using appropriate loading strategies.
// app/layout.js - Site-wide analytics import Script from 'next/script' export default function RootLayout({ children }) { return ( <html> <body>{children}</body> <Script src="https://www.googletagmanager.com/gtag/js?id=GA_ID" strategy="afterInteractive" /> </html> ) }
Script events (onLoad, onError, onReady)
Event handlers let you execute code when scripts load, fail, or are ready—onLoad fires once when loaded, onReady fires after load and on every subsequent navigation, onError catches failures.
<Script src="https://maps.googleapis.com/maps/api/js" onLoad={() => { console.log('Maps loaded, initializing...') initMap() }} onReady={() => { // Fires on every route change too console.log('Script ready') }} onError={(e) => { console.error('Script failed:', e) }} />
Web Workers
The experimental worker strategy offloads third-party scripts to a web worker using Partytown, keeping the main thread free for your app—ideal for heavy analytics or advertising scripts.
// next.config.js module.exports = { experimental: { nextScriptWorkers: true }, } // Component <Script src="https://heavy-analytics.com/script.js" strategy="worker" />
Styling in Next.js
CSS Modules
CSS Modules provide locally-scoped CSS by automatically generating unique class names—create files with .module.css extension and import them as objects to prevent style conflicts.
/* Button.module.css */ .button { background: blue; color: white; } .primary { background: green; }
import styles from './Button.module.css' export default function Button() { return <button className={styles.button}>Click</button> // Renders: class="Button_button__abc123" }
Global styles
Import global CSS files in your root layout (app router) or _app.js (pages router)—these styles apply to all pages and are useful for resets, base styles, and utility classes.
// app/layout.js import './globals.css' export default function RootLayout({ children }) { return ( <html> <body>{children}</body> </html> ) }
Sass/SCSS support
Next.js supports Sass out of the box after installing the sass package—use .scss or .sass extensions for global styles or .module.scss for scoped module styles with full Sass features.
npm install sass
/* styles/variables.scss */ $primary-color: #0070f3; /* Button.module.scss */ @import 'variables'; .button { background: $primary-color; }
CSS-in-JS (styled-components, Emotion)
CSS-in-JS libraries require configuration for SSR to prevent hydration mismatches—styled-components needs a registry setup, Emotion works with the @emotion/react package and optional compiler plugin.
// styled-components setup (app/lib/registry.js) 'use client' import { useState } from 'react' import { StyleSheetManager } from 'styled-components' export default function StyledRegistry({ children }) { const [sheet] = useState(() => new ServerStyleSheet()) // ... registry logic }
Tailwind CSS with Next.js
Tailwind integrates seamlessly with Next.js—install it, configure content paths in tailwind.config.js, import the base CSS in your layout, and use utility classes throughout your components.
// tailwind.config.js module.exports = { content: [ './app/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}', ], theme: { extend: {} }, } // Usage <button className="bg-blue-500 hover:bg-blue-700 text-white px-4 py-2 rounded"> Click </button>
PostCSS configuration
Next.js uses PostCSS by default for CSS processing—customize it with a postcss.config.js file to add plugins like autoprefixer, nesting, or custom transforms.
// postcss.config.js module.exports = { plugins: { 'tailwindcss': {}, 'autoprefixer': {}, 'postcss-nested': {}, }, }
Styled JSX
Styled JSX is Next.js's built-in CSS-in-JS solution providing scoped styles without configuration—write CSS in template literals inside <style jsx> tags, and it's automatically scoped to the component.
export default function Button() { return ( <> <button>Click me</button> <style jsx>{` button { background: blue; color: white; padding: 10px 20px; } `}</style> </> ) }
CSS imports
Next.js supports importing CSS files directly in JavaScript files—global CSS in layouts, CSS Modules anywhere, and third-party CSS from node_modules for component libraries.
// Import global CSS (only in layout or _app) import 'normalize.css' // Import CSS Module (any component) import styles from './Component.module.css' // Import third-party component CSS import 'react-datepicker/dist/react-datepicker.css'
Static Assets
public directory
The public directory at your project root serves static files at the base URL path—files are cached by browsers and served efficiently without processing, ideal for images, fonts, and other static resources.
your-project/
├── public/
│ ├── images/
│ │ └── logo.png → /images/logo.png
│ ├── fonts/
│ │ └── custom.woff2 → /fonts/custom.woff2
│ └── favicon.ico → /favicon.ico
└── app/
Serving static files
Reference files in public using absolute paths starting with /—Next.js serves them directly without modification, and you should use the Image component for images to get optimization benefits.
// Direct reference (no optimization) <img src="/logo.png" alt="Logo" /> // Recommended: Use Image component for optimization import Image from 'next/image' <Image src="/logo.png" alt="Logo" width={200} height={100} /> // Linking to files <a href="/documents/guide.pdf">Download Guide</a>
Image assets
Store static images in public for direct access, but prefer next/image for optimization—static imports from outside public (like app/ or components/) are analyzed at build time for automatic dimension detection.
// From public (no build-time analysis) <Image src="/hero.jpg" alt="Hero" width={1200} height={600} /> // Static import (auto width/height, blur placeholder) import heroImg from './hero.jpg' <Image src={heroImg} alt="Hero" placeholder="blur" />
Font assets
Store custom font files in public/fonts for direct access, or better yet use next/font/local which optimizes loading—public fonts need manual @font-face declarations while next/font handles everything automatically.
// Recommended: next/font/local import localFont from 'next/font/local' const myFont = localFont({ src: '../public/fonts/MyFont.woff2' }) // Manual (public directory) /* globals.css */ @font-face { font-family: 'MyFont'; src: url('/fonts/MyFont.woff2') format('woff2'); }
robots.txt
Place robots.txt in the public directory for static serving, or generate it dynamically using a robots.ts file in the app directory (app router)—it controls search engine crawler access to your site.
# public/robots.txt (static)
User-agent: *
Allow: /
Disallow: /admin/
Sitemap: https://example.com/sitemap.xml
// app/robots.ts (dynamic) export default function robots() { return { rules: { userAgent: '*', allow: '/', disallow: '/admin/' }, sitemap: 'https://example.com/sitemap.xml', } }
sitemap.xml
Generate sitemaps statically in public or dynamically with app/sitemap.ts—the dynamic approach lets you fetch URLs from your database and automatically updates with content changes.
// app/sitemap.ts export default async function sitemap() { const posts = await getPosts() return [ { url: 'https://example.com', lastModified: new Date() }, ...posts.map(post => ({ url: `https://example.com/posts/${post.slug}`, lastModified: post.updatedAt, })), ] }
favicon.ico
Place favicon.ico in public for automatic serving, or in the app directory for automatic metadata handling—Next.js (app router) also supports icon.png, apple-icon.png, and dynamic generation with icon.tsx.
app/
├── favicon.ico # Automatic <link rel="icon">
├── icon.png # Modern icon
├── apple-icon.png # Apple touch icon
└── icon.tsx # Dynamic generation
// app/icon.tsx (dynamic favicon)
import { ImageResponse } from 'next/og'
export default function Icon() {
return new ImageResponse(<div style={{...}}>🚀</div>)
}