--- name: web-performance description: 'Use this skill whenever the user is improving Core Web Vitals or page load performance, or mentions slow pages, PageSpeed Insights / Lighthouse failures, LCP/CLS/INP scores, bundle size, image/font optimization, or third-party script impact. Covers Core Web Vitals, image and font optimization, JavaScript bundle size, CSS build size, CDN caching, third-party JavaScript impact, and measurement tools. Skip for backend latency tuning, database query optimization, or CI build speed.' --- # 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 ``, `@import` in CSS, self-hosted, framework font utility) Default: check the HTML `` 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 `

` or `

`, TTFB and render-blocking CSS reduction matter more than image optimization. Skip framework-specific subsections that don't match the user's stack. --- ## Contents 1. [Core Web Vitals Overview](#core-web-vitals-overview) 2. [LCP: Largest Contentful Paint](#lcp-largest-contentful-paint) 3. [CLS: Cumulative Layout Shift](#cls-cumulative-layout-shift) 4. [INP: Interaction to Next Paint](#inp-interaction-to-next-paint) 5. [Image Optimization](#image-optimization) 6. [Font Loading](#font-loading) 7. [JavaScript Bundle Size](#javascript-bundle-size) 8. [Legacy JavaScript and Browser Targets](#legacy-javascript-and-browser-targets) 9. [Third-Party JavaScript](#third-party-javascript) 10. [CSS Build Size](#css-build-size) 11. [CDN and Caching](#cdn-and-caching) 12. [Measurement and Validation](#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 `

` or `

`. 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 `

` 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 `` in `` 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 ``. Load the rest of the stylesheet asynchronously: ```html ``` #### 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 `` instead of linking external files, eliminating separate CSS download requests: ```ts // 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: ```ts // 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 ` ``` The nonce must change on every request - a static nonce is functionally equivalent to `'unsafe-inline'`. This approach requires server-side nonce injection (available in Next.js middleware and most edge runtimes). **Option (c): hash-based allowlisting (works for static content)** Compute the SHA-256 hash of the exact inline style content and list it in `style-src`. Only that precise block is permitted - any change to the styles requires updating the hash: ``` Content-Security-Policy: style-src 'self' 'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='; ``` Generate the hash for your critical CSS block: ```bash printf 'body { margin: 0; }' | openssl dgst -sha256 -binary | openssl base64 ``` **Detecting the problem:** after deploying inlined critical CSS, open Chrome DevTools > Console and look for messages beginning with `Refused to apply inline style because it violates the following Content Security Policy directive`. The page may appear completely unstyled even though the `