Last Minute Notes

Performance and Optimization

Next.js performance: Core Web Vitals and how each is hurt or helped, measuring with Lighthouse, Web Vitals, and the bundle analyzer, next/image, next/font, next/script, bundle size and dynamic imports, Server Component wins, streaming and Partial Prerendering, caching layers, React Compiler, Turbopack, and a debugging playbook.

📘 Performance and Optimization

Web performance interviews follow a pattern: name the metric, find the bottleneck with a tool, apply the smallest fix, and verify with data. Next.js bundles many of the fixes (image, font, script optimization, code splitting, streaming), so the skill is knowing which lever moves which metric.

Table of Contents

  1. Core Web Vitals
  2. Measuring
  3. Images
  4. Fonts
  5. Third-party Scripts
  6. JavaScript Bundle Size
  7. Server-side Levers
  8. Client-side Rendering Cost
  9. Build and Dev Performance
  10. Backend and Network
  11. Debugging Playbook
  12. Questions

1. Core Web Vitals

MetricMeasuresGood (75th percentile)Main Next.js levers
LCP (Largest Contentful Paint)When the main content appears2.5 s or lessStatic/cached HTML, priority on hero image, streaming, fast TTFB, font preloading
INP (Interaction to Next Paint)Responsiveness to input across the visit200 ms or lessLess client JS, fewer hydrated components, useTransition, break long tasks, avoid heavy handlers
CLS (Cumulative Layout Shift)Visual stability0.1 or lessnext/image dimensions, next/font fallbacks, reserved space for ads and embeds, skeletons of final size

Supporting metrics: TTFB (server and network speed), FCP (first paint), TBT (lab proxy for INP), bundle size (first-load JS).

Field data (real users, CrUX / RUM) is what Google ranks on; lab data (Lighthouse) helps you debug.


2. Measuring

ToolUse
Lighthouse / PageSpeed InsightsLab audit, plus CrUX field data for public URLs
Chrome DevTools Performance panelLong tasks, main-thread flame chart, layout shifts, INP breakdown
@next/bundle-analyzer (or next experimental-analyze with Turbopack)What is in each route's bundle
next build outputFirst-load JS per route and static/dynamic status
useReportWebVitalsSend real-user metrics to analytics
React DevTools ProfilerWhy a Client Component re-renders
web-vitals library, Vercel Speed Insights, Sentry, Datadog RUMProduction RUM with segmentation
WebPageTestRealistic network/device throttling, filmstrip
// app/web-vitals.tsx
'use client';
import { useReportWebVitals } from 'next/web-vitals';

export function WebVitals() {
  useReportWebVitals((metric) => {
    navigator.sendBeacon('/api/vitals', JSON.stringify(metric));   // name, value, rating, id
  });
  return null;
}

Always test a production build (next build && next start) on a throttled mobile profile; dev mode is much slower and unrepresentative.


3. Images

Images are usually the biggest bytes and the LCP element.

import Image from 'next/image';

// Above the fold hero: preload, high priority
<Image src="/hero.jpg" alt="..." width={1600} height={900} priority sizes="100vw" />

// Responsive card grid
<Image src={p.image} alt={p.name} width={400} height={300}
  sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw" />

// Container-filling background
<div className="relative h-64"><Image src={cover} alt="" fill sizes="100vw" className="object-cover" /></div>
FeatureEffect
Automatic resizingServes widths matching sizes and device pixel ratio via srcset
Modern formatsWebP / AVIF where supported (images.formats)
Lazy loadingBelow-the-fold images load on approach (default)
priority (or loading="eager" / fetchPriority="high")Preloads the LCP image, never lazy loaded
width/height or fillReserves space, prevents CLS
placeholder="blur"Low-res preview, smoother perceived load
CachingOptimized images cached on disk/CDN; minimumCacheTTL controls freshness
remotePatternsAllowlist of external hosts (security and abuse protection)

Pitfalls:

  • Missing sizes on responsive images makes the browser assume 100vw and download an oversized file.
  • Marking many images priority defeats the purpose; one or two per page.
  • Serving huge originals: resize at upload; the optimizer costs CPU on first request per size.
  • On self-hosted setups install sharp (included by default in recent versions) and put a CDN in front.
  • Use unoptimized or a custom loader (Cloudinary, imgix) if you already have an image CDN.
  • SVGs and animated GIFs are not optimized by default.

4. Fonts

import { Geist } from 'next/font/google';
const geist = Geist({ subsets: ['latin'], display: 'swap' });
  • Self-hosted at build time: no layout-blocking third-party font request, no extra DNS/TLS handshake.
  • Automatic fallback font with size-adjust metrics removes the layout shift when the web font swaps in.
  • Subset fonts (subsets, weight) and use variable fonts to cut bytes.
  • Limit families and weights; each one is a request.
  • display: 'swap' shows text immediately; optional avoids shift entirely on slow connections.

5. Third-party Scripts

Analytics, chat widgets, tag managers, and ads are common causes of poor INP and LCP.

import Script from 'next/script';

<Script src="https://example.com/analytics.js" strategy="afterInteractive" />
<Script src="https://example.com/chat.js" strategy="lazyOnload" />
<Script id="init" strategy="beforeInteractive">{`window.__flag = true`}</Script>
StrategyWhen it loads
beforeInteractiveBefore hydration (critical, root layout only)
afterInteractive (default)Soon after hydration
lazyOnloadDuring browser idle time
worker (experimental)Off the main thread via Partytown
  • Prefer @next/third-parties components (Google Tag Manager, Analytics, YouTube/Maps embeds) which load efficiently.
  • Replace heavy embeds with a facade (static thumbnail that loads the iframe on click).
  • Audit the tag manager regularly; every tag is a main-thread cost.

6. JavaScript Bundle Size

Every Client Component and everything it imports ships to the browser, is parsed, and must hydrate.

TechniqueHow
Server Components by defaultLibraries used only in Server Components never reach the client
Push 'use client' downA small interactive leaf instead of a whole page
Dynamic importLoad heavy, below-the-fold, or interaction-triggered UI lazily
AnalyzeFind big dependencies (moment, lodash, chart libs) and replace or lazy-load them
Tree-shakeable importsimport { debounce } from 'lodash-es', or optimizePackageImports for icon/UI libraries
Avoid barrel files in hot pathsBarrel re-exports can pull in whole packages
Replace heavy librariesdate-fns or Intl instead of moment; native fetch instead of axios
Check use client leaksA provider at the root that imports big modules makes everything under it heavier
import dynamic from 'next/dynamic';

const Chart = dynamic(() => import('@/components/Chart'), {
  loading: () => <ChartSkeleton />,
  ssr: false,            // only inside a Client Component; skips server render for browser-only libs
});

// Load on interaction: declare at module scope, render only when needed
const Editor = dynamic(() => import('./Editor'));

function Post() {
  const [open, setOpen] = useState(false);
  return open ? <Editor /> : <button onClick={() => setOpen(true)}>Edit</button>;
}
  • React.lazy + Suspense also works in Client Components.
  • ssr: false with dynamic is only allowed inside Client Components; in Server Components just import normally or wrap in a Client Component.
  • Set a bundle budget in CI (fail the build if first-load JS for a route exceeds a threshold).
  • Compare next build first-load JS before and after each change; shared chunks count across routes.

Server Components win by example:

// Server Component: markdown + syntax highlighting libraries stay on the server (0 KB client)
import { marked } from 'marked';
import hljs from 'highlight.js';

export default async function Post({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const html = renderMarkdown((await getPost(slug)).body);
  return <article dangerouslySetInnerHTML={{ __html: html }} />;   // sanitize untrusted content
}

7. Server-side Levers

LeverEffect
Static rendering + CDNBest TTFB and LCP; no server work per request
ISR / use cache + tagsStatic speed with fresh content
Streaming + SuspenseEarly shell, slow data does not block first paint
Partial Prerendering (Cache Components)Static shell instantly, dynamic holes stream in
Parallel data fetchingEliminates waterfalls; see Data Fetching and Caching
Colocate with the dataRun the server in the same region as the database (round trips add up)
Connection poolingServerless functions need a pooler (PgBouncer, Prisma Accelerate, Neon pooling)
CompressionBrotli/gzip at the CDN or server (compress: true)
HTTP caching headersCache-Control with s-maxage and stale-while-revalidate for CDN caching
PrefetchingLink prefetches in viewport; tune with prefetch prop

Dynamic rendering should be a deliberate choice:

  • Move cookies() and searchParams reads into small components behind Suspense, so the rest of the page remains static.
  • Check that a page you thought was static is not accidentally dynamic in the build table.

8. Client-side Rendering Cost

Even with Server Components, Client Components must hydrate and re-render efficiently.

ProblemFix
Large hydrated treeFewer/smaller Client Components, lazy-load below the fold
Slow input handlersuseTransition/useDeferredValue, debounce, move work off the main thread (Web Worker)
Long listsVirtualize (@tanstack/react-virtual), paginate, content-visibility: auto
Re-render stormsColocate state, split contexts, memoize expensive children; see React
Layout thrashBatch reads/writes, avoid animating layout properties; use transform and opacity
Large JSON in propsSend only needed fields; paginate; fetch on demand
Long tasks (over 50 ms)Break up work with scheduler.yield() or requestIdleCallback

React Compiler (stable support in current Next.js, opt in with reactCompiler: true) automatically memoizes components and hooks at build time, reducing manual useMemo/useCallback and many wasted renders. It does not fix algorithmic problems, large trees, or oversized bundles.


9. Build and Dev Performance

  • Turbopack is the default for dev and build: faster cold starts, incremental rebuilds, and optional file-system caching for restarts.
  • Keep dependency graphs lean; large barrel files and giant JSON imports slow compilation.
  • CI caching: persist .next/cache between builds so image optimization and compilation results are reused.
  • Monorepo builds: Turborepo/Nx remote cache and outputFileTracingRoot for correct tracing.
  • Static export or too many generateStaticParams can make builds slow; prerender the top N and let the rest render on demand.
  • Use output: 'standalone' to produce a small deployable folder (see Testing, Deployment, and Operations).

10. Backend and Network

AreaPractice
DatabaseIndexes for your query patterns, select only needed columns, avoid N+1 (batch with IN, DataLoader), pagination
CacheRedis/KV for hot reads; use cache for rendered results; CDN for static and public data
RegionDeploy compute near the DB, assets near users
HTTPHTTP/2 or 3, Brotli, long-lived immutable caching for hashed /_next/static assets
Preconnect<link rel="preconnect"> for critical third-party origins
PayloadCompress JSON, drop unused fields, return DTOs
Timeouts and retriesBound upstream calls; fail fast, show fallback via Suspense/error boundaries

Static assets under /_next/static are fingerprinted and served with immutable caching; do not change that. Files in public/ are not fingerprinted, so set cache headers deliberately when they change.


11. Debugging Playbook

SymptomInvestigateTypical fix
Slow LCPLCP element in Lighthouse; is it an image or text? TTFB?priority, correct sizes, static/cached rendering, preload font, reduce render-blocking CSS
High TTFBServer timing, DB queries, cold starts, regionCache, parallelize, add indexes, move compute closer to data, pooling
Poor INPPerformance panel, long tasks on interactionLess hydration, transitions, split work, remove heavy third-party scripts
High CLS"Layout shift regions" in DevToolsImage dimensions, font fallbacks, skeletons sized correctly, reserve ad slots
Big first-load JSBundle analyzerConvert to Server Components, dynamic import, replace libraries
Slow navigationNetwork tab for _rsc, waterfall in componentsPrefetch, parallel fetch, loading.tsx, avoid dynamic layouts
Slow only in productionCold starts, cache misses, region mismatchWarm up, bigger memory, cache, connection pooling
Memory growth on serverHeap snapshots, unbounded in-memory cachesBound caches, avoid module-level state keyed by request, check large closures
Hydration is slowReact Profiler, hydration timeSmaller client tree, Suspense boundaries, defer non-critical widgets

Method:

  1. Measure with field data first, then reproduce with throttling in a production build.
  2. Locate the bottleneck (network, server, main thread, bundle).
  3. Change one thing and re-measure.
  4. Guard with a performance budget or CI check so it does not regress.

12. Questions

Q: How do you improve LCP in Next.js? Make the HTML fast (static or cached rendering, low TTFB, streaming), mark the LCP image with priority and correct sizes, serve modern formats, preload fonts via next/font, and keep render-blocking CSS and JS small.

Q: How do Server Components help performance? They send no component JavaScript, keep heavy libraries on the server, fetch data close to the source, and reduce hydration work, which improves load time and INP.

Q: What causes CLS and how does Next.js help? Images, fonts, and embeds that change size after load. next/image requires dimensions, next/font provides size-adjusted fallbacks, and you should reserve space for dynamic content with correctly sized skeletons.

Q: How would you reduce the bundle size of a route? Run the bundle analyzer, move non-interactive code into Server Components, push 'use client' to leaves, lazy-load heavy components with dynamic, swap large libraries, and enforce a budget in CI.

Q: What is Partial Prerendering for performance? A static shell served immediately from the CDN with dynamic sections streamed in, so personalized pages get static-level TTFB and LCP.

Q: How do you load third-party scripts without hurting performance? next/script with afterInteractive or lazyOnload, @next/third-parties, facades for heavy embeds, and regular tag audits.

Q: Lighthouse says 95 but users complain it is slow. Why? Lab data uses a fast simulated device on one URL. Check field data (CrUX/RUM) by device, geography, and route; authenticated pages and slow phones expose INP and TTFB issues that Lighthouse does not.

Q: What does the React Compiler change? It automatically memoizes components and values at build time, cutting wasted re-renders without manual useMemo/useCallback. It is not a substitute for reducing bundle size or fixing slow data fetching.

On this page