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