
Performance checklist: 20 ways to speed up a Next.js app (Lighthouse-focused)
This long-form guide is designed as a hands-on Next.js performance optimization playbook. It focuses on measurable Lighthouse improvements (Performance score, LCP, INP/FID, TBT, CLS) and practical code changes you can apply immediately.
1. Upgrade Next.js, React, Node and use Turbopack (if available)
Why: unlock newer optimizer, smaller bundles, better tree-shaking. Action:
Upgrade packages; enable Turbopack for dev to surface bundle issues. Before: package.json: "next": "13.x" After: package.json: "next": "16.x", "react": "19.x" Command:
npm i next@latest react@latest react-dom@latestMeasure: build time ↓, dev HMR speed ↑, Lighthouse gain often +2–10 points for modern bundler and tree-shaking.
2. Analyze bundles and remove heavy imports
Why: large client bundles = slower FCP/LCP and higher TBT. Action:
Use next build output, source-map-explorer, or @next/bundle-analyzer. Install and run:
npm i -D @next/bundle-analyzer
next.config.js snippet:
js
const withBundleAnalyzer = require('@next/bundle-analyzer')({ enabled: process.env.ANALYZE === 'true' })module.exports = withBundleAnalyzer({})
Then:
ANALYZE=true npm run build
Measure: First Load JS kB ↓. Expect 10–40% reductions after removing big libs.
3. Replace heavy libraries with lighter alternatives or dynamic imports
Why: tree-shaking may not remove whole libraries. Before:
js
import _ from 'lodash'
After:
js
import debounce from 'lodash/debounce'// orconst HeavyComp = dynamic(() => import('../components/HeavyComp'), { ssr: false })
Measure: bundle size ↓, Lighthouse perf +1–8 points.
4. Use dynamic imports with suspense or no SSR for non-critical components
Why: defer JS execution until needed. Code:
js
import dynamic from 'next/dynamic'const Chart = dynamic(() => import('../components/Chart'), { ssr: false, loading: () => <Skeleton/> })
Measure: First CPU Idle & TBT improve; LCP may improve if above-the-fold content reduces JS.
5. Enable React server components / use App Router when suitable
Why: move rendering to server reduces client JS. Action: adopt server components for data-heavy UI, keep interactive bits client components. Example:
jsx
// server component (default in app/)// uses fetch() server-side, returns HTML, minimal client JSexport default function Page() { const data = await fetch(...); return <div>{data.title}</div> }
Measure: First Load JS ↓ significantly; Lighthouse +5–15 points on pages converted.
6. Optimize images with next/image and AVIF/WebP
Why: large images increase payload and LCP. Before:
html
<img src="/hero.jpg" alt="hero" />
After:
jsx
import Image from 'next/image'<Image src="/hero.jpg" width={1200} height={600} priority />
Also use formats: set device sizes and quality. Use next.config to allow AVIF/WebP. Measure: Network payload ↓, LCP improves (often 100–800ms).
7. Use priority and preload for critical images and fonts
Why: ensures important resources load early. Example:
jsx
<Image src="/hero.jpg" priority />// preload a font in app/layout.tsx<link rel="preload" href="/fonts/my.woff2" as="font" type="font/woff2" crossOrigin="anonymous" />
Measure: LCP and FCP improvements.
8. Use next/font (or local fonts) to avoid render-blocking CSS
Why: reduces layout shifts and render-blocking CSS for fonts. Before:
html
<link href="https://fonts.googleapis.com/..." rel="stylesheet">
After:
js
import { Inter } from 'next/font/google'const inter = Inter({ subsets: ['latin'], variable: '--font-inter' })export default function Layout({ children }) { return <html className={inter.variable}>{children}</html> }
Measure: CLS ↓, LCP ±, Lighthouse accessibility/perf gains.
9. Defer or lazily load third-party scripts with next/script
Why: third-party scripts block main thread and increase TBT. Before:
html
<script src="https://analytics.com/script.js"></script>
After:
jsx
import Script from 'next/script'<Script src="https://analytics.com/script.js" strategy="afterInteractive" />
Use strategy="lazyOnload" for truly non-essential scripts. Measure: TBT and Performance score increase; often large gains if many scripts removed from initial load.
10. Remove unused CSS and avoid large CSS frameworks on client
Why: CSS payload increases render time. Action:
Use CSS modules, Tailwind with purge, or modular CSS.
Avoid importing big styles in _app that aren't used on page. Measure: CSS payload ↓, FCP/LCP improvements; Lighthouse CSS savings audit clears.
11. Minimize and split critical JS — use server-side rendering / static generation
Why: SSR/SSG moves work off client; ISR for infrequent updates. Action:
Use getStaticProps/getStaticPaths or server components for pages that don't need client interactivity. Measure: First Load JS ↓, LCP and TBT improve. SSG pages often see highest Lighthouse scores.
12. Implement HTTP caching and proper cache headers (CDN)
Why: reduces repeat load times and improves perceived performance. Action (Vercel example via headers in next.config.js):
js
module.exports = {Â async headers() {Â Â return [Â Â Â { source: '/:all*(svg|jpg|png|css|js)', headers: [{ key: 'Cache-Control', value: 'public, max-age=31536000, immutable' }] }Â Â ]Â }}
Measure: Repeat view Lighthouse & real UX speed-ups; lower TTFB for cached assets.
13. Use Edge CDN, middleware, and streaming where appropriate
Why: reduce latency and enable faster TTFB/LCP. Action: deploy on edge platform (Vercel Edge, Cloudflare) and use streaming SSR. Measure: TTFB ↓, Lighthouse perf gains dependent on geographic tests.
14. Optimize server response sizes: gzip/ Brotli and compress JSON
Why: smaller transfer = faster parse and render. Action: ensure hosting enables Brotli for text assets and compress API responses. Measure: bytes transferred ↓; LCP/FCP improvements.
15. Avoid large JSON in initial HTML and lazy-load data
Why: embedding big JSON inflates HTML payload and blocks parsing. Before:
html
<script id="__NEXT_DATA__">...huge json...</script>
After:
Fetch large data on client after initial render or stream it. Measure: initial HTML size ↓; faster FCP/LCP.
16. Reduce main-thread work and expensive renders (profiling)
Why: long JS tasks increase TBT and INP. Action:
Use React Profiler and Chrome Performance to find heavy tasks.
Break big tasks with requestIdleCallback, web workers, or microtasks. Code example to defer:
js
requestIdleCallback(() => heavyInit())
Measure: TBT ↓, INP/FID improve.
17. Use HTTP/2 multiplexing and connection keep-alive
Why: faster parallel requests with fewer connections. Action: use modern CDN and server configs. Measure: waterfall becomes denser; FCP improves especially on many small resources.
18. Preconnect critical origins (CDN, APIs) and use DNS-prefetch
Why: reduces DNS/TCP/TLS time. Example:
html
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /><link rel="dns-prefetch" href="//api.example.com" />
Measure: small latency savings; LCP and FCP improve on cold starts_
19. Optimize Fonts: subset, variable fonts, and font-display: swap
Why: fonts can block text rendering and cause CLS / FCP delays. Action: use next/font with subsets or serve subsetted WOFF2, set display: swap. Measure: CLS ↓ and perceived load faster.
20. Monitor Real User Metrics (RUM) and iterate
Why: Lighthouse is lab; real users reveal patterns. Action: add RUM for Core Web Vitals (web-vitals library) and track trends. npm:
npm i web-vitals
Example:
js
import { getCLS, getFID, getLCP } from 'web-vitals'getLCP(metric => sendToAnalytics(metric))
Measure: sustainable improvements in real LCP/INP/CLS after fixes.
Short before/after mini-examples with measurable gains
Remove synchronous analytics script Before:
html
<script src="https://tracker.com/track.js"></script>
After:
jsx
<Script src="https://tracker.com/track.js" strategy="afterInteractive" />
Expected: TBT -50–200ms; Lighthouse perf +2–6.
Dynamic import a heavy chart Before bundle: First Load JS 450KB
js
import Chart from 'heavy-chart-lib'
After:
js
const Chart = dynamic(() => import('heavy-chart-lib'), { ssr:false })
Expected: First Load JS -100–300KB; LCP -200–800ms; Lighthouse +3–10.
Switch Google Fonts -> next/font Before: render-blocking stylesheet, CLS visible After: next/font localizes fonts, less FOIT/FOUT Expected: CLS reduced to 0.01–0.02; Lighthouse +1–4.
Measurable KPIs to track (and how)
Lighthouse Performance score (0–100) — run in incognito, mobile throttling.
LCP (ms) — target < 2.5s.
INP / FID (ms) — target < 100ms.
CLS — target < 0.1 (ideally <0.05).
First Load JS (kB) — from next build output or bundle analyzer.
TBT (ms) — lower for better interactivity.
TTFB (ms) — server latency. Record values before and after each grouped change.
Suggested prioritized rollout (fast wins → deeper changes)
Defer third-party scripts (next/script)
Optimize images with next/image + modern formats
Switch to next/font and preload critical fonts
Analyze & remove heavy imports (bundle analyzer)
Use dynamic imports for non-critical components
Implement caching headers and CDN
Convert pages to SSG/SSR server components where possible
Profile and reduce main-thread tasks
Deploy to edge / enable Brotli / HTTP/2
Continuous RUM monitoring
Rankable keyword suggestions (long and short tails)
Next.js performance optimization (high)
Lighthouse Next.js (high)
Next.js Lighthouse checklist (medium)
speed up Next.js app (medium)
reduce Next.js bundle size (medium)
Next.js dynamic import performance (low-medium)
next/font performance benefits (low-medium)
next/image LCP optimization (low-medium)
best practices Next.js performance (high)
decrease TBT Next.js (low)
improve LCP Next.js (low)
Next.js SSR vs SSG performance (medium)
Next.js Core Web Vitals guide (medium)
optimize fonts Next.js (low)
defer third-party scripts Next.js (low)
Use these in headings, meta title, meta description, and H2s for SEO.
Final checklist table (20 items, single-line actionable)
Quick action
1 Upgrade Next/React/Node npm i next@latest react@latest; enable Turbopack
2 Bundle analysis Use @next/bundle-analyzer; remove heavy deps_
3 Replace heavy libs Swap or import specific functions
4 Dynamic imports dynamic(() => import(...), { ssr:false })
5 Server components/SSG Convert pages to server/SSG where possible
6 next/image Use [Image blocked: No description] with AVIF/WebP, sizes
7 Priority preload images/fonts priority prop + rel=preload
8 next/font Migrate to next/font/google or local fonts
9 next/script for 3rd-party strategy='afterInteractive' or 'lazyOnload'
10 Remove unused CSS Use CSS modules/Tailwind purge
11 Split critical JS SSR/SSG, dynamic imports
12 Cache headers Set Cache-Control via next.config or CDN
13 Edge deploy/streaming Use edge platform and streaming SSR
14 Compression Enable Brotli/gzip on host
15 Avoid huge NEXT_DATA Lazy-load big data
16 Reduce main-thread work Profile and offload work/web workers
17 HTTP/2 & keep-alive Use modern CDN/server config
18 Preconnect/DNS-prefetch Add rel=preconnect/dns-prefetch tags
19 Fonts: subset/variable Use subsets, variable fonts, display:swap
20 RUM monitoring web-vitals + analytics, track trends
Comments
No comments yet.