Design Engineering
Build

Performance

Meet Core Web Vitals targets through code splitting, image optimisation, bundle analysis, and server-side rendering strategies.

Performance is a feature. A 1-second delay in page load time reduces conversions by 7%. Every 100ms improvement in LCP improves user engagement. Performance work is not premature optimisation — it is part of the definition of done for every page.

Core Web Vitals

Google's Core Web Vitals are the primary performance contract. They directly influence search ranking and reflect real user experience.

MetricMeasuresGoodNeeds improvementPoor
LCP (Largest Contentful Paint)Perceived load speed — when the main content is visible≤ 2.5s2.5–4.0s> 4.0s
INP (Interaction to Next Paint)Responsiveness — how fast UI responds to user input≤ 200ms200–500ms> 500ms
CLS (Cumulative Layout Shift)Visual stability — unexpected layout shifts during load≤ 0.10.1–0.25> 0.25
TTFB (Time to First Byte)Server response time≤ 800ms800–1800ms> 1800ms
FCP (First Contentful Paint)Time to first visible content≤ 1.8s1.8–3.0s> 3.0s

INP replaced FID (First Input Delay) as a Core Web Vital in March 2024. Ensure your performance monitoring tools are reporting INP, not FID.

Bundle Optimisation

Code splitting

Split large components and routes so that only the code needed for the current page is loaded. Next.js does route-level splitting automatically. Apply component-level splitting with dynamic() for large, non-critical components.

Dynamic imports for non-critical components
import dynamic from "next/dynamic"

// Heavy chart library — only loads when component is rendered
const RevenueChart = dynamic(() => import("@/components/charts/revenue-chart"), {
  loading: () => <ChartSkeleton />,
  ssr: false, // Client-only — avoid server rendering for canvas-based charts
})

// Below-fold content — defer loading
const ActivityFeed = dynamic(() => import("@/components/activity-feed"), {
  loading: () => <FeedSkeleton />,
})

export default function DashboardPage() {
  return (
    <>
      <DashboardMetrics />     {/* Always loads */}
      <RevenueChart />          {/* Lazy loaded */}
      <ActivityFeed />          {/* Lazy loaded */}
    </>
  )
}

Avoiding bundle bloat

  • Import only what you use: import { format } from 'date-fns' not import * as dateFns from 'date-fns'
  • Audit dependencies with @next/bundle-analyzer before adding libraries
  • Prefer native browser APIs over libraries when the functionality is simple (e.g., Intl.DateTimeFormat instead of moment.js)
  • Use server components for heavy libraries — they never contribute to the client bundle
Bundle analyzer setup
// next.config.mjs
import bundleAnalyzer from "@next/bundle-analyzer"

const withBundleAnalyzer = bundleAnalyzer({
  enabled: process.env.ANALYZE === "true",
})

export default withBundleAnalyzer({
  // your next config
})

// Run: ANALYZE=true pnpm build
// Opens a visual treemap of your bundle — identify and eliminate large packages

Rendering Strategies

StrategyWhen to useNext.js implementation
Static (SSG)Content that is the same for all users and changes infrequentlyNo fetch options, or fetch with cache: 'force-cache'
ISR (Incremental Static Regeneration)Content that changes but can tolerate being slightly stalefetch with next: { revalidate: 3600 }
SSR (Server-Side Rendering)Content personalised per user or request, must be currentfetch with cache: 'no-store'
Streaming SSRPages with slow data dependencies — render fast parts firstSuspense boundaries + async Server Components
CSR (Client-Side Rendering)Highly interactive UI that changes continuouslyDynamic import with ssr: false

Image Optimisation

Images are the most common cause of poor LCP. Always use Next.js Image component — it handles WebP conversion, responsive sizes, lazy loading, and prevents CLS automatically.

Correct Image usage
import Image from "next/image"

// Above-fold hero image — preload with priority
<Image
  src="/hero.jpg"
  alt="Dashboard overview showing project metrics"
  width={1200}
  height={630}
  priority           // Preloads — use only for LCP image
  sizes="100vw"
/>

// Below-fold content image — lazy by default
<Image
  src={project.thumbnail}
  alt={`${project.name} preview`}
  width={400}
  height={225}
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>

Font Loading

Unoptimised fonts cause both FCP delays and CLS (layout shifts when fonts swap). Use next/font to load fonts at build time with zero layout shift.

next/font setup — zero CLS
// app/layout.tsx
import { Inter, Geist_Mono } from "next/font/google"

const inter = Inter({
  subsets: ["latin"],
  variable: "--font-sans",
  display: "swap",
  preload: true,
})

const geistMono = Geist_Mono({
  subsets: ["latin"],
  variable: "--font-mono",
  display: "swap",
})

export default function RootLayout({ children }) {
  return (
    <html className={`${inter.variable} ${geistMono.variable}`}>
      <body>{children}</body>
    </html>
  )
}

Measuring Performance

ToolWhat it measuresWhen to use
Lighthouse (DevTools)LCP, INP, CLS, TTFB, FCP, accessibility, SEODevelopment and CI — run on every major feature
WebPageTestReal device + network simulation, waterfall analysisDeep diagnosis of specific pages
Chrome DevTools Performance tabJavaScript execution, long tasks, layout thrashingDebugging specific INP or janky scroll issues
Vercel Speed InsightsReal User Monitoring (RUM) in productionOngoing production monitoring
@next/bundle-analyzerJavaScript bundle composition and sizeBefore shipping any new large dependency

On this page