Web Performance Guide
Applies to: Any website or web app | Updated: March 2026
A practical reference for measuring and improving web performance - covering Core Web Vitals, image and font optimization, JavaScript bundle size, CSS build size, CDN caching, third-party JavaScript impact, and validation tools.
Section 0: Before You Start
Answer these questions before making any performance changes. Each has a default - use it if the user hasn't said otherwise.
Q: Which pages are the priority targets? (landing page, dashboard, auth-gated app pages, all pages) Default: public-facing pages first - these are indexed by search engines and directly affect user experience. Auth-gated pages matter less for Core Web Vitals field data because CrUX only collects data from logged-in users on those routes.
Q: What is the current performance baseline? Default: unknown - run PageSpeed Insights on the target URL before making any changes, so you have a before/after comparison. Note the LCP element type (image or text), TTFB, and the specific audits flagged as failing.
Q: Are you optimizing for lab scores (Lighthouse) or field data (real users)? Default: both - but prioritize fixing field data issues flagged in Google Search Console > Core Web Vitals first. Lab scores are easier to game; field data reflects real users on real devices and networks.
Q: What framework or rendering model is the site using?
(plain HTML, SPA/Vite, Next.js App Router, Astro, Nuxt, WordPress)
Default: detect from config files (next.config.*, vite.config.*, astro.config.*) if visible; otherwise assume plain HTML. Framework-specific advice is in clearly labeled subsections throughout this guide.
Q: What image formats are currently in use?
Default: JPEG/PNG - check the public/ or assets/ directory and any image references in source before assuming.
Q: How are web fonts loaded?
(Google Fonts via <link>, @import in CSS, self-hosted, framework font utility)
Default: check the HTML <head> and any global CSS files before assuming.
Q: Is a CDN or hosting platform configured with custom cache headers? Default: no - most platforms (AWS Amplify, plain S3, some shared hosts) do not set long-lived cache on static assets by default. Check the hosting config before assuming.
Q: Is a browserslist target configured?
Default: no - without it, many transpilers and bundlers use a conservative target and ship legacy polyfills for features that modern browsers have supported for years.
AI assistant: Read the user's answers (or use the defaults above) before generating any code. Run PageSpeed Insights first if no baseline exists. Identify the LCP element type before optimizing images - if the LCP element is a
<p>or<h1>, TTFB and render-blocking CSS reduction matter more than image optimization. Skip framework-specific subsections that don't match the user's stack.
Contents
- Core Web Vitals Overview
- LCP: Largest Contentful Paint
- CLS: Cumulative Layout Shift
- INP: Interaction to Next Paint
- Image Optimization
- Font Loading
- JavaScript Bundle Size
- Legacy JavaScript and Browser Targets
- Third-Party JavaScript
- CSS Build Size
- CDN and Caching
- Measurement and Validation
Core Web Vitals Overview
Applies when: any public-facing page.
Core Web Vitals are Google's user-experience metrics, measured in the field via the Chrome User Experience Report (CrUX). They are ranking signals. The three metrics as of 2026:
| Metric | Measures | Good | Needs improvement | Poor |
|---|---|---|---|---|
| LCP | Loading speed of the largest visible element | < 2.5 s | 2.5 - 4 s | > 4 s |
| CLS | Visual instability from layout shifts | < 0.1 | 0.1 - 0.25 | > 0.25 |
| INP | Responsiveness of all interactions | < 200 ms | 200 - 500 ms | > 500 ms |
INP replaced FID (First Input Delay) in March 2024. FID only measured the first interaction; INP measures every interaction throughout the visit. A page that passes INP must remain responsive throughout the entire session, not just at initial load.
Field data appears in Google Search Console after a URL accumulates enough traffic. Until then, use PageSpeed Insights lab data (Lighthouse) as a proxy.
LCP: Largest Contentful Paint
Applies when: any page with a hero section, large image, or above-the-fold text block.
Identify the LCP element before optimizing. The LCP element is not always an image. On text-heavy marketing pages it is often a <p> or <h1>. When the LCP element is text, the highest-impact fixes are TTFB reduction and eliminating render-blocking CSS - not image optimization.
Real-world example: On a marketing home page, PageSpeed Insights identified the LCP element as a
<p>paragraph tag, not an image. TTFB was 610 ms and element render delay was 230 ms. The correct optimization targets were redirect chains (adding 607 ms before the first byte) and render-blocking CSS chunks - not image format conversion.
Eliminate render-blocking resources
Render-blocking resources delay the LCP element from painting. CSS files loaded as <link rel="stylesheet"> in <head> block rendering until they download and parse.
Real-world example: PageSpeed Insights flagged two render-blocking CSS chunks on a marketing page - 13.6 KiB and 1.2 KiB - adding approximately 400 ms to LCP. These were a global stylesheet and a component stylesheet generated by the framework's default CSS chunking behavior.
Universal approach: Inline critical CSS (the styles needed to render above-the-fold content) directly into the HTML <head>. Load the rest of the stylesheet asynchronously:
<style>
/* Critical CSS: only styles needed for the above-the-fold content */
body { margin: 0; font-family: system-ui, sans-serif; }
.hero { ... }
</style>
<link rel="stylesheet" href="/styles.css" media="print" onload="this.media='all'" />
<noscript><link rel="stylesheet" href="/styles.css" /></noscript>
Next.js App Router
Next.js 15 generates separate CSS chunks for globals.css and component styles. Two experimental options reduce or eliminate the render-blocking effect:
Option 1: Enable CSS inlining.
The experimental.inlineCss flag embeds CSS directly into the HTML <head> instead of linking external files, eliminating separate CSS download requests:
// next.config.ts
const nextConfig: NextConfig = {
experimental: {
inlineCss: true,
},
};
This is experimental as of Next.js 15. Test in staging before deploying. Real-world reports show Lighthouse scores improving from 94 to 100 after enabling this flag.
Option 2: Use cssChunking: 'strict'.
Loads CSS in exact import order, which can reduce out-of-order loading penalties:
// next.config.ts
const nextConfig: NextConfig = {
experimental: {
cssChunking: 'strict',
},
};
inlineCss is the stronger fix for LCP. Neither fully resolves render-blocking CSS in Next.js 15; this is a known framework-level issue tracked in the Next.js repository.
CSP interaction with inlined critical CSS
Inlining critical CSS is one of the most effective LCP improvements, but it has a silent failure mode: if a Content-Security-Policy header is active with a style-src directive that does not allow inline styles, the browser silently blocks the inlined <style> block. The page renders without styles, LCP worsens, and no build-time warning is produced - the only signal is a CSP violation in the browser console.
Three approaches, in order of security:
Option (a): allow 'unsafe-inline' in style-src
Easiest t