Install
$ agentstack add skill-ziniman-ai-instruct-web-performance ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
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
- [Core Web Vitals Overview](#core-web-vitals-overview)
- [LCP: Largest Contentful Paint](#lcp-largest-contentful-paint)
- [CLS: Cumulative Layout Shift](#cls-cumulative-layout-shift)
- [INP: Interaction to Next Paint](#inp-interaction-to-next-paint)
- [Image Optimization](#image-optimization)
- [Font Loading](#font-loading)
- [JavaScript Bundle Size](#javascript-bundle-size)
- [Legacy JavaScript and Browser Targets](#legacy-javascript-and-browser-targets)
- [Third-Party JavaScript](#third-party-javascript)
- [CSS Build Size](#css-build-size)
- [CDN and Caching](#cdn-and-caching)
- [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 | 4 s | | CLS | Visual instability from layout shifts | 0.25 | | INP | Responsiveness of all interactions | 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:
/* Critical CSS: only styles needed for the above-the-fold content */
body { margin: 0; font-family: system-ui, sans-serif; }
.hero { ... }
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:
// 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 `` 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 to add, weakest protection. Permits any inline style, including styles injected by XSS. Acceptable for apps with no elevated security requirements:
Content-Security-Policy: style-src 'self' 'unsafe-inline';
Option (b): nonce-based allowlisting (recommended for strict CSP)
Generate a random nonce per request on the server and add it to each inline `` tag. The CSP header must carry the same nonce:
Content-Security-Policy: style-src 'self' 'nonce-rAnd0mN0nce';
/* Critical CSS */
body { margin: 0; }
.hero { ... }
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:
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 `` block is present in the HTML source.
Reduce server response time (TTFB)
A slow Time to First Byte delays everything downstream.
Universal strategies:
- Serve static pages from a CDN edge node close to the user
- Avoid redirect chains before the HTML response (see [CDN and Caching](#cdn-and-caching))
- Cache rendered pages where content does not change per-request
Next.js App Router
Server Components that await slow database queries block initial HTML delivery:
- Move non-critical data fetching into child Server Components so the page shell renders immediately
- Use
loading.tsx(React Suspense boundaries) to stream the shell while data loads - Add
unstable_cacheorcache()to repeated queries that don't need to be fresh on every request
import { Suspense } from 'react';
// Shell renders immediately; DataList streams in when data is ready
export default function Page() {
return (
Page Title
}>
);
}
Preload the LCP image
When the LCP element is an image, add an explicit preload hint so the browser fetches it as early as possible:
Only preload the LCP element. Adding preload hints to multiple images creates competing requests and can worsen LCP.
Next.js App Router
For images rendered via next/image, use the priority prop - it generates the preload link automatically and disables lazy loading:
import Image from 'next/image';
fetchpriority and the LCP image
Two attributes are often confused when optimizing the LCP image - they are complementary, not alternatives:
loading="eager"tells the browser to fetch this image immediately without deferring it. It is the default for images in the initial viewport; it only matters when overridingloading="lazy"on an above-the-fold image.fetchpriority="high"tells the browser's resource scheduler to deprioritize other in-flight requests relative to this one. The image is fetched eagerly AND jumps the network queue ahead of competing CSS and font downloads.
An image can be fetched eagerly but still lose the priority race against render-blocking stylesheets. Use both attributes together on the LCP image and its preload hint:
The fetchpriority="high" on the `` is the more impactful placement: the browser's preload scanner runs before the DOM is parsed, so it can begin fetching the image while HTML is still streaming in. Without it, the browser may classify the preload as low priority and delay it behind CSS.
Diagnostic checklist: PageSpeed still reports slow LCP despite the image appearing in the initial HTML
Work through these in order:
- Render-blocking wrapper. A
'use client'component wrapping the LCP image in Next.js App Router delays its render until JS hydration completes. The `tag must appear in the raw HTML response - confirm withcurl -s https://yourdomain.com/ | grep 'hero'`. If the tag is absent from the curl output, it is being injected by JavaScript.
- **Preload hint in `
instead of.** The preload scanner only processes hints found in. Check:curl -s https://yourdomain.com/ | grep -B5 'preload.*image'- confirm theappears before`.
- Missing
fetchpriority="high"on the preload link. Check:curl -s https://yourdomain.com/ | grep 'fetchpriority'- if this returns nothing, the attribute is absent.
- Late-hydrating client component. In Next.js App Router, an image inside a
'use client'component that renders after a Suspense boundary will have itssrcset by JavaScript, not the initial HTML. The preload hint exists but the browser cannot match it to a real `` element until hydration, defeating the optimization.
- TTFB above 200 ms. If the server is slow, no preload optimization can compensate. Fix TTFB first - resource hints only recover time that the network is the bottleneck.
CLS: Cumulative Layout Shift
Applies when: any page with images, dynamically loaded content, or web fonts.
CLS measures how much the page layout shifts after initial render. Shifts are jarring and cause accidental taps on mobile.
Always set image dimensions
Every ` must have explicit width and height` attributes. Without these, the browser does not reserve space for the image, causing a layout shift when it loads:
For images that fill their container (unknown intrinsic dimensions), use CSS aspect-ratio on the container:
.image-container {
width: 100%;
aspect-ratio: 16 / 9;
overflow: hidden;
}
Avoid injecting content above existing content
Elements that load after the initial render and push content down cause high CLS. Common causes:
- Auth user interface components (avatar, username) rendering after hydration - reserve space with a fixed-size skeleton
- Toast notifications that push content down instead of overlaying it - use an overlay-based notification library positioned at the screen edge
- Ad or analytics scripts that inject banners - avoid this; if unavoidable, reserve the space before the script loads
Stabilize font loading
Web fonts that load after the initial render cause text to reflow (FOUT: Flash of Unstyled Text). Use font-display: optional for body fonts to avoid shifts, or font-display: swap if the FOUT is visually acceptable.
INP: Interaction to Next Paint
Applies when: any page with user interactions - especially dashboards, data-entry forms, and interactive app flows.
INP measures how quickly the browser responds to every tap, click, or key press. A 200 ms response budget is tight on slow devices.
DOM size and INP
Larger DOM trees slow down style recalculation, layout, and paint operations that happen during every interaction. Google recommends fewer than 1,500 DOM nodes, a maximum depth of 32, and no more than 60 children per parent node.
> Real-world example: PageSpeed Insights measured 366 DOM nodes and a maximum depth of 13 on a marketing home page. At this scale the impact is minor, but it establishes a baseline to watch as the page grows. For pages with deeply nested component trees or large data tables, DOM size becomes a meaningful INP contributor.
Long main-thread tasks
Any synchronous operation over 50 ms on the main thread will cause high INP for interactions that happen during or just after it. Long tasks appear in PageSpeed Insights under "Avoid long main-thread tasks" and in the Chrome DevTools Performance tab as red-marked task bars.
> Real-world example: PageSpeed Insights identified three long main-thread tasks on a marketing page: 80 ms and 64 ms from own JavaScript, and 60 ms from a third-party auth initialization script. The own-JS tasks should be profiled i
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: ziniman
- Source: ziniman/ai-instruct
- License: Apache-2.0
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.