Performance is a product feature. In 2025, Google’s INP replaces FID, image CDNs are table stakes, and real-user monitoring determines rankings. This guide gives you practical, copy‑paste tactics to hit 95+ on Lighthouse and sustain it in CI.
Framework Selection: Choose the right foundation with our Next.js vs SvelteKit vs Nuxt comparison, or explore low-code platforms for faster builds.
Core Web Vitals Targets (2025)
- LCP < 2.0s (mobile), preferably < 1.6s on Wi‑Fi
- INP < 200ms (p95)
- CLS < 0.1
Performance Budgets That Prevent Regressions
Images: From LCP to Lazy‑Load Strategy
The Biggest Wins
<!-- Explicit sizes + priority hero -->
<img src="/hero-1200.webp" width="1200" height="630" fetchpriority="high" loading="eager" alt="Hero" />
<!-- Lazy below the fold -->
<img src="/card-600.webp" width="600" height="400" loading="lazy" alt="Card" />
- Serve AVIF/WebP, set width/height, avoid layout shifts
- Use
fetchpriority="high"for the single LCP hero - Use responsive srcset and CDNs (Image CDN or platform-native)
Practical Budgets for Images
- Hero (LCP) image ≤ 120KB (AVIF/WebP)
- Total image bytes per route (mobile) ≤ 1MB
- Avoid more than 1 eager image per route (usually only the LCP hero)
Fonts Without Jank
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet" />
- Always use
display=swap - Self‑host if you need ultimate stability; subset for latin only
Font Budgets
- Max 2 font families; max 3 weights total
- Preload only what you actually render above the fold
JavaScript Discipline and Hydration Strategy
- Defer non‑critical scripts; remove dead code
- Prefer server rendering/streaming; hydrate islands only
- Replace heavy deps with native APIs or micro‑libs
<script src="/analytics.js" defer></script>
Hydration Strategy
- Prefer server rendering/streaming; hydrate only interactive islands
- Split bundles per route; avoid monolithic client code
- Replace heavy UI libs with native elements where possible
CSS Strategy and Critical Path
- Extract critical CSS for above‑the‑fold
- PostCSS + cssnano; purge unused utilities
- Prefer container queries over JS layout hacks
Critical CSS
- Inline ≤ 14KB critical CSS for top routes
- Lazy‑load the rest and cache aggressively
INP: Fixing Interaction Latency
// Break long tasks
requestIdleCallback(() => heavyInit());
// Yield to the main thread
setTimeout(stepChunk, 0);
- Avoid long tasks > 50ms; chunk and schedule
- Virtualize lists; avoid sync layout thrash
Event Handler Hygiene
- Keep handlers small; debounce input where sensible
- Avoid forced synchronous reflows; batch DOM reads/writes
CLS: Making Layout Stable
- Reserve space for images, ads, embeds
- Avoid injecting DOM above content post load
Layout Stability Tips
- Use
aspect‑ratioand explicit sizes for media - Defer UI banners/modals below existing content; reserve slots if needed
CI Automation (Lighthouse CI) and PR Gates
npx @lhci/cli autorun --collect.numberOfRuns=3 --assert.preset=lighthouse:recommended
- Fail PRs if LCP > 2.5s or score < 90
- Track regressions with lhci and custom thresholds
Example lhci Configuration (package.json)
{
"lhci": {
"collect": { "numberOfRuns": 3, "staticDistDir": "dist" },
"assert": {
"assertions": {
"categories:performance": ["error", { "minScore": 0.9 }],
"interactive": ["error", { "maxNumericValue": 3500 }]
}
}
}
}
Real‑User Monitoring (RUM) Setup
import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP(console.log); onINP(console.log); onCLS(console.log);
- Send to GA4/Amplitude; focus on p75/p95, not averages
Minimal RUM Snippet
import { onLCP, onINP, onCLS } from 'web-vitals';
function send(metric){
navigator.sendBeacon?.('/rum', JSON.stringify(metric));
}
onLCP(send); onINP(send); onCLS(send);
Store metrics server‑side, build weekly dashboards, and watch for regressions after releases.
Common Pitfalls and Anti‑Patterns
- Loading multiple analytics/ads tags synchronously
- Injecting banners above the hero after load (CLS)
- Hydrating entire pages when 2–3 islands suffice
- Shipping heavy UI kits for simple pages
- Unbounded client state and large JSON payloads
Checklist for Launch
- LCP < 2.0s (mobile), INP < 200ms (p95), CLS < 0.1
- 1 eager image (the LCP hero), others lazy‑loaded
- Fonts: max 2 families/3 weights, display=swap
- JS bundles split by route; non‑critical scripts deferred
- Critical CSS inlined; rest lazy‑loaded
- LHCI budgets enforced in CI; RUM pipeline active
FAQ: Frequently Asked Questions
Why is INP failing while Lighthouse passes?
INP measures real interactions in the field and is impacted by device variability, slow CPUs, and long tasks. Lighthouse is a lab test. To fix: break long tasks using setTimeout/requestIdleCallback, avoid heavy global event listeners, virtualize long lists, and reduce hydration by preferring server rendering and islands. Track p75/p95 with RUM so you see real‑world issues.
Should I inline critical CSS?
Yes on high‑traffic routes to improve LCP. Keep inlined CSS ≤ ~14KB and ensure it styles above‑the‑fold content only. Load the remaining stylesheet asynchronously with strong caching. Validate via Chrome DevTools Coverage to avoid shipping unused CSS.
Is image CDN necessary?
If you serve multiple sizes/devices, yes. A CDN automates format negotiation (AVIF/WebP), DPR variants, resizing, and cache headers. This reduces server work and improves LCP across geographies. For small sites, build responsive srcset carefully and revisit as traffic grows.
Can I hit 95+ without refactor?
Often, yes. Start with the big three: optimize the LCP hero image, defer non‑critical scripts, and cut bundle size by 30–50% through code‑splitting and removing dead deps. Reserve layout slots to kill CLS. Only refactor when fundamentals are exhausted.
How do I maintain scores over time?
Enforce budgets in CI so regressions fail PRs; instrument RUM to track p75/p95 in production; review weekly dashboards; and assign an explicit performance owner. Pair performance reviews with dependency updates to prevent bloat.
What’s a good approach to third‑party scripts?
Audit all third‑parties quarterly. Load non‑critical tags with defer, consider server‑side tagging where possible, and use a consent manager. Remove anything without measurable ROI. Lazy‑load widgets below the fold.
How do I debug layout shifts quickly?
Use Chrome Performance panel with the “Layout Shift Regions” option, or PerformanceInsights to identify shifting DOM nodes. Add explicit width/height or aspect‑ratio boxes; avoid inserting DOM above content post load.
What dev tools should I standardize on?
Lighthouse CI for lab checks, web‑vitals for field metrics, Chrome DevTools (Coverage, Performance), Bundle Analyzer, and your platform’s image CDN tooling.
References (External)
- Web Vitals (official) — LCP, INP, CLS guidance
- Lighthouse CI — automation and budgets
- Chrome DevTools — Performance, Coverage