Skip to main content

performance-optimization

Next.js 16 performance — next/image and next/font setup, dynamic imports, bundle analysis, the React Compiler (stable in 16, experimental Rust compiler in 16.3), prefetching and Partial Prefetching, third-party scripts and Core Web Vitals (LCP, INP, CLS). Use when a page is slow or heavy. Caching strategy belongs to data-fetching.

Source facts

Repository
Agents-Store/codex-plugins
Last source activity
November 1, 2026 at 02:46
Detected SKILL.md language
English
Stars
0
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
2 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
performance-optimization
description
Next.js 16 performance — next/image and next/font setup, dynamic imports, bundle analysis, the React Compiler (stable in 16, experimental Rust compiler in 16.3), prefetching and Partial Prefetching, third-party scripts and Core Web Vitals (LCP, INP, CLS). Use when a page is slow or heavy. Caching strategy belongs to data-fetching.
# Performance Optimization Next.js includes built-in optimizations for images, fonts, scripts, and rendering. Apply these patterns to achieve excellent Core Web Vitals scores. ## Image Optimization with `next/image` The `<Image>` component automatically optimizes images with lazy loading, responsive sizing, and modern formats (WebP/AVIF). ### Basic Usage ```tsx import Image from 'next/image' // Local image (auto width/height detection) import heroImage from '@/public/hero.jpg' export default function Hero() { return ( <Image src={heroImage} alt="Hero banner" placeholder="blur" // Auto blur placeholder for local images priority // Preload — use for above-the-fold images /> ) } ``` ### Remote Images ```tsx <Image src="https://images.example.com/photo.jpg" alt="Photo" width={800} height={600} sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw" /> ``` Configure allowed remote sources in `next.config.ts`: ```ts const nextConfig = { images: { remotePatterns: [ { protocol: 'https', hostname: 'images.example.com' }, ], }, } ``` ### Image Best Practices | Practice | Why | |----------|-----| | Always set `priority` on the LCP image | Eliminates lazy-load delay for the largest visible image | | Use `sizes` prop for responsive images | Tells the browser which size to download at each breakpoint | | Use `placeholder="blur"` for local images | Shows a low-quality preview while loading | | Use `fill` prop for unknown dimensions | Image fills its parent container (set `position: relative` on parent) | | Avoid layout shift | Always provide `width`/`height` or use `fill` | ## Font Optimization with `next/font` `next/font` self-hosts fonts with zero layout shift. Fonts are loaded at build time and served from the same domain. ### Google Fonts ```tsx import { Inter, JetBrains_Mono } from 'next/font/google' const inter = Inter({ subsets: ['latin'], display: 'swap', variable: '--font-sans', }) const jetbrainsMono = JetBrains_Mono({ subsets: ['latin'], display: 'swap', variable: '--font-mono', }) export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en" className={`${inter.variable} ${jetbrainsMono.variable}`}> <body className="font-sans">{children}</body> </html> ) } ``` ### Tailwind CSS Integration Tailwind v4 (the create-next-app default) uses CSS-first configuration — no `tailwind.config.ts` needed for font variables: ```css /* globals.css */ @import "tailwindcss"; @theme inline { --font-sans: var(--font-sans); --font-mono: var(--font-mono); } ``` ## Dynamic Imports & Code Splitting ### Component-level Code Splitting ```tsx import dynamic from 'next/dynamic' // Lazy load heavy components const Chart = dynamic(() => import('@/components/chart'), { loading: () => <p>Loading chart...</p>, }) // Skip SSR for browser-only components const Map = dynamic(() => import('@/components/map'), { ssr: false, loading: () => <div className="h-96 bg-gray-100 animate-pulse" />, }) ``` ### When to Use Dynamic Imports | Scenario | Approach | |----------|----------| | Heavy library (chart, editor, map) | `dynamic(() => import(...))` | | Browser-only library (uses `window`) | `dynamic(() => import(...), { ssr: false })` | | Modal or dialog content | `dynamic(() => import(...))` — loads on demand | | Above-the-fold content | Do NOT dynamic import — use regular import | ### Library-level Code Splitting ```tsx // Only import what you need import { format } from 'date-fns' // Tree-shakeable import { debounce } from 'lodash-es' // ES module version ``` Avoid importing entire libraries (`import _ from 'lodash'`). Use tree-shakeable ES module versions. ## Bundle Size Optimization ### Analyze the Bundle ```bash # Turbopack bundle analyzer (16.1+): interactive treemap on port 4000 next experimental-analyze # Write static analysis files instead next experimental-analyze --output ``` The analyzer filters by route, shows import chains, and provides separate client/server views. The older `@next/bundle-analyzer` (`ANALYZE=true`) is webpack-based and only works with `next build --webpack`. ### Common Bundle Bloaters | Issue | Fix | |-------|-----| | Large icon library | Import individual icons: `import { Search } from 'lucide-react'` | | Full lodash import | Use `lodash-es` or individual imports: `import debounce from 'lodash/debounce'` | | Moment.js | Replace with `date-fns` or `dayjs` | | CSS-in-JS runtime | Use Tailwind CSS or CSS Modules (zero runtime) | | Heavy markdown parser | Use `next-mdx-remote` or dynamic import | ### Keep Client Components Small Every component marked `'use client'` adds to the client JavaScript bundle. Strategies: - Extract only the interactive part into a Client Component - Keep data fetching and static rendering in Server Components - Use composition (pass Server Components as `children` to Client Components) ## React Compiler Automatic memoization without manual `useMemo`/`useCallback`. Stable since Next.js 16, but not enabled by default: ```ts // next.config.ts const nextConfig = { reactCompiler: true, // requires: npm install babel-plugin-react-compiler@latest } ``` The Babel-based compiler increases build time. Next.js 16.3 adds an **experimental** Rust-based compiler that runs inside Turbopack (34-46% faster dev-to-ready-page vs Babel). It is opt-in and released to gather feedback — do not roll it out to production without testing: ```ts const nextConfig = { reactCompiler: true, // still required — the flag below only picks the implementation experimental: { turbopackRustReactCompiler: true }, // 16.3+, Turbopack only } ``` Constraints: it requires `reactCompiler: true`, works only with Turbopack (using it with `--webpack` throws an error), and `babel-plugin-react-compiler` is not needed. `create-next-app` offers a `--react-compiler` flag for new projects. ## Core Web Vitals ### LCP (Largest Contentful Paint) - Set `priority` on the hero/banner image - Preload critical fonts with `next/font` - Avoid layout shifts that delay the largest element - Use `<Suspense>` to stream non-critical content ### FID/INP (First Input Delay / Interaction to Next Paint) - Minimize client-side JavaScript - Use Server Components for non-interactive UI - Defer heavy computations with `requestIdleCallback` or Web Workers - Avoid blocking the main thread during hydration ### CLS (Cumulative Layout Shift) - Always specify image dimensions (`width`/`height` or `fill`) - Use `next/font` to prevent font-swap layout shifts - Reserve space for dynamic content with CSS `min-height` - Avoid injecting content above existing content ## Rendering Strategy Selection | Content Type | Strategy | Implementation | |-------------|----------|----------------| | Marketing pages | Static (SSG) | Default (no dynamic data) | | Blog posts | ISR | `export const revalidate = 3600` — only without `cacheComponents`; with it: `'use cache'` + `cacheLife('hours')` | | Product pages | ISR | `export const revalidate = 60` + `revalidateTag` on update — only without `cacheComponents`; with it: `'use cache'` + `cacheLife('minutes')` + `cacheTag()` | | User dashboard | Dynamic + Streaming | `<Suspense>` boundaries | | Search results | Dynamic | Access `searchParams` | | Authenticated pages | Dynamic | Access `cookies()` | The two ISR rows use the previous model (route segment config). With `cacheComponents: true` the `revalidate`, `dynamic`, `dynamicParams` and `fetchCache` exports are removed and the lifetime moves into the cached function — see the migration table in the `data-fetching` skill's `references/cache-components.md`. ## Script Optimization ```tsx import Script from 'next/script' // Analytics — load after page is interactive <Script src="https://analytics.example.com/script.js" strategy="afterInteractive" /> // Non-critical widget — load when browser is idle <Script src="https://widget.example.com/embed.js" strategy="lazyOnload" /> // Critical third-party — load before page hydration (rare) <Script src="https://critical.example.com/script.js" strategy="beforeInteractive" /> ``` ## Prefetching Next.js automatically prefetches `<Link>` destinations when they appear in the viewport. Control this behavior: ```tsx import Link from 'next/link' // Prefetch enabled (default) <Link href="/dashboard">Dashboard</Link> // Disable prefetch for rarely visited pages <Link href="/terms" prefetch={false}>Terms</Link> ``` Next.js 16 rewrote prefetching: layout deduplication, incremental prefetching, cancellation when links leave the viewport, and hover prioritization all happen automatically. 16.3 can inline small prefetch payloads into the page (`prefetchInlining`), and `<Link prefetch={true}>` participates in Partial Prefetching when `cacheComponents` + `partialPrefetching` are enabled (see the `data-fetching` skill's Cache Components reference). With Partial Prefetching, per-link prefetching is the knob for a few high-intent links: `<Link href="/search?q=next" prefetch={true}>` resolves the cached content that depends on that URL before the click, while a plain `<Link>` prefetches only the shared App Shell. Do not put `prefetch={true}` on every card in a large grid — each visible link can trigger a server request; prefer hover-triggered prefetch there. A segment can cap the cost with `export const prefetch = 'partial'` or `'force-disabled'` (requires `cacheComponents`). ## View Transitions React's `<ViewTransition>` is stable as of React 19.3 (2026-09-09) and works in the App Router with no configuration (the App Router already ships the React features it needs). Route navigations are transitions, so wrapping the same-named element on both pages morphs it across the navigation: ```tsx import { ViewTransition } from 'react' import Image from 'next/image' // On both the list page (thumbnail) and the detail page (hero), with the same name: <ViewTransition name={`photo-${photo.id}`}> <Image src={photo.src} alt={photo.alt} width={400} height={300} /> </ViewTransition> ``` Animations are triggered by Transitions, `<Suspense>` reveals and `useDeferredValue` — a plain `setState` does not start one. Browsers without the View Transitions API simply skip the animation. Guide: [Designing view transitions](https://nextjs.org/docs/app/guides/view-transitions). ## Production Checklist - [ ] Hero image has `priority` prop - [ ] All images have `width`/`height` or `fill` - [ ] Fonts use `next/font` (no external stylesheet links) - [ ] Heavy components use `dynamic()` import - [ ] `'use client'` only on components that need interactivity - [ ] Bundle analyzed with `next experimental-analyze` - [ ] No unused dependencies in `package.json` - [ ] `output: 'standalone'` for Docker deployments - [ ] Static pages use ISR where appropriate - [ ] Third-party scripts use appropriate `strategy`
View on GitHub