From antigravity-awesome-skills
Scans, audits, and fixes web performance issues across all four Lighthouse pillars (Performance, Accessibility, Best Practices, SEO) using a structured batch workflow. Use for Core Web Vitals improvements or PageSpeed Insights reports.
How this skill is triggered — by the user, by Claude, or both
Slash command
/antigravity-awesome-skills:pagespeed-enhancerThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
A structured, batch-wise audit-and-fix workflow for all four Lighthouse pillars. Always follow the batch flow in order. Never jump straight to fixes without completing the scan and risk assessment phases.
A structured, batch-wise audit-and-fix workflow for all four Lighthouse pillars. Always follow the batch flow in order. Never jump straight to fixes without completing the scan and risk assessment phases.
PHASE 1 → Ingest Report & Parse Scores
PHASE 2 → Batch Scan (4 sections, parallel analysis)
PHASE 3 → Consolidated Risk Report (changes ranked by impact vs risk)
PHASE 4 → Fix Batches (applied in safe order: low-risk → high-risk)
PHASE 5 → Verification Checklist
When the user provides a PageSpeed Insights report (pasted text, screenshot, or URL):
| Pillar | Score | Status | Critical Issue |
|-----------------|-------|---------|-------------------------------------|
| Performance | 80 | ⚠️ Warn | LCP 4.0s — element render delay |
| Accessibility | 100 | ✅ Pass | — |
| Best Practices | 100 | ✅ Pass | CSP missing (unscored) |
| SEO | 100 | ✅ Pass | — |
Then proceed immediately to Phase 2 without waiting for user input unless the report is ambiguous.
Run all four section scans. Present as collapsible sections in output.
Audit these in order (highest Lighthouse weight first):
| Audit | Metric Impact | Key Questions |
|---|---|---|
| LCP breakdown | LCP | Is the LCP element lazily loaded? Is TTFB > 600ms? Is element render delay > 1s? |
| Render-blocking resources | FCP, LCP | Which CSS/JS files block the critical path? Can they be deferred or inlined? |
CSS @import rules | FCP, LCP | Are external stylesheets loaded via @import url() in CSS? This is 2x render-blocking — browser must fetch CSS, parse it, then fetch imported CSS. Use <link> instead. |
| Unused JavaScript | FCP, LCP, TBT | What % of the main bundle is unused? Is code-splitting possible? |
| Network dependency tree | LCP | What is the critical path chain? Max latency? |
| Forced reflows | TBT | Which JS functions query geometry after DOM mutation? |
| Image delivery | FCP, LCP | Are images in WebP/AVIF? Are above-fold images lazy-loaded? |
| Speed Index | SI | Is page visually progressive or does it paint all at once? |
| CLS culprits | CLS | Any images without width/height? Any late-injected content? |
| JavaScript execution time | TBT | Total parse + compile + evaluate time? |
| Long main-thread tasks | TBT | Tasks > 50ms? Starting when? |
| Bundled asset sizes | FCP, LCP, TBT | Check dist/ output: any single JS chunk > 500KB gzipped? CSS > 100KB? Code-splitting creating proper vendor chunks? |
For each audit item, output:
Focus on any failed audits. For a 100-score page, still check:
| Check | What to Verify |
|---|---|
| ARIA attribute correctness | All aria-* attributes match element roles |
| Colour contrast | All text meets WCAG AA (4.5:1 normal, 3:1 large) |
| Image alt text quality | Alt text is descriptive, not filename-style |
| Keyboard navigation | All interactive elements reachable by Tab |
| Skip links | Present and focusable |
| Heading hierarchy | No skipped levels (h1 → h2 → h3) |
| Touch target size | Min 44×44px on mobile |
| Form labels | Every input has an associated label |
lang attribute | <html lang="en"> present and valid BCP 47 |
font-display | Set to swap or optional to prevent FOIT |
Security headers are often unflagged by Lighthouse score but are critical. Check ALL deployment targets:
| Check | Header/Setting | Where to Configure | Severity |
|---|---|---|---|
| Content Security Policy | Content-Security-Policy | netlify.toml [[headers]] / vercel.json "headers" | 🔴 High |
| Cross-Origin-Opener-Policy | COOP header | Same as above | 🔴 High |
| Clickjacking protection | X-Frame-Options or CSP frame-ancestors | Same as above | 🔴 High |
| HSTS configuration | Strict-Transport-Security with includeSubDomains + preload | Same as above | 🟡 Medium |
| Trusted Types (DOM XSS) | CSP require-trusted-types-for 'script' | Same as above | 🟡 Medium |
| X-Content-Type-Options | nosniff header | Same as above | 🟡 Medium |
| Referrer-Policy | strict-origin-when-cross-origin | Same as above | 🟡 Medium |
| Permissions-Policy | Restrict camera/mic/geolocation | Same as above | 🟡 Medium |
| Third-party cookies | Any SameSite=None cookies without Secure? | — | 🟡 Medium |
| Deprecated APIs | Any browser-deprecated JS APIs in use? | — | 🟢 Low |
| Source maps | Are source maps deployed for debugging? | — | 🟢 Low |
When both netlify.toml and vercel.json exist, check BOTH. Each has a different syntax (TOML vs JSON).
| Check | What to Verify |
|---|---|
<title> tag | Present, 50–60 chars, includes primary keyword |
| Meta description | Present, 150–160 chars, compelling |
| Canonical tag | <link rel="canonical"> points to correct URL |
| hreflang | Present if multilingual; correct language codes |
| robots.txt | Valid, not blocking key resources |
| Structured data | JSON-LD present; run Schema validator |
| Image alt attributes | Every <img> has meaningful alt |
| Link descriptiveness | No "click here" / "read more" link text |
| Crawlability | No noindex on important pages |
| HTTP status | 200 on main page and critical resources |
| SPA meta injection | If using react-helmet-async / Next.js Head: verify via "View Page Source", not DevTools Elements — meta tags may be JS-injected |
After completing all four batch scans, output a consolidated Risk vs Impact Matrix:
| Fix | Impact Score | Risk Level | Effort | Priority |
|----------------------------------|-------------|------------|----------|----------|
| Add defer/async to non-critical JS | High (LCP -0.8s est) | 🟢 Low | 1h | P1 |
| Convert images to WebP/AVIF | Medium (LCP -0.3s) | 🟢 Low | 2h | P1 |
| Add CSP header | Security | 🟡 Medium | 3h | P2 |
| Code-split main JS bundle | High (TBT -20ms) | 🟡 Medium | 1 day | P2 |
| Fix forced reflows | Medium (TBT -15ms) | 🔴 High | 2 days | P3 |
| Add HSTS preload | Security | 🟡 Medium | 30min | P2 |
Risk Level Definitions:
Always recommend: fix P1 (Low Risk, High Impact) items first, then P2, then P3.
Apply fixes in risk order. For each fix, provide:
Examples from common audits:
F1.1 — Move CSS @import to <link> tag
CSS @import url() is 2x render-blocking. Move to <link> in <head>:
/* Before: in index.css */
@import url('https://fonts.googleapis.com/css2?family=Inter&display=swap');
<!-- After: in index.html <head> -->
<link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Inter&display=swap" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter&display=swap" media="print" onload="this.media='all'" />
<noscript><link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter&display=swap" /></noscript>
F1.2 — Defer render-blocking CSS (if not above-fold critical)
<!-- Before -->
<link rel="stylesheet" href="/assets/index.css">
<!-- After: load async, apply on load -->
<link rel="preload" href="/assets/index.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/assets/index.css"></noscript>
F1.3 — Fix broken preconnect (crossorigin mismatch)
<!-- Before (broken — no crossorigin on font CDN) -->
<link rel="preconnect" href="https://api.rss2json.com">
<!-- After -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Only preconnect origins used in critical path, max 4 -->
F1.4 — Convert images to WebP
# Using cwebp
cwebp -q 80 input.jpeg -o output.webp
# Using sharp (Node.js)
sharp('image.jpeg').webp({ quality: 80 }).toFile('image.webp')
# macOS fallback (sips built-in)
sips -s format webp input.jpeg --out output.webp
# Python Pillow fallback
python3 -c "
from PIL import Image
Image.open('input.jpg').save('output.webp', 'WebP', quality=80)
"
F1.5 — Add explicit image dimensions (CLS fix)
<!-- Before -->
<img src="hero.webp" alt="...">
<!-- After -->
<img src="hero.webp" alt="..." width="800" height="400">
F1.6 — Add security headers (netlify.toml)
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-Content-Type-Options = "nosniff"
Referrer-Policy = "strict-origin-when-cross-origin"
Strict-Transport-Security = "max-age=31536000; includeSubDomains; preload"
Cross-Origin-Opener-Policy = "same-origin"
Permissions-Policy = "camera=(), microphone=(), geolocation=()"
Content-Security-Policy = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self' https://api.rss2json.com"
F1.7 — Add security headers (vercel.json)
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "X-Frame-Options", "value": "DENY" },
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
{ "key": "Strict-Transport-Security", "value": "max-age=31536000; includeSubDomains; preload" },
{ "key": "Cross-Origin-Opener-Policy", "value": "same-origin" },
{ "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" },
{ "key": "Content-Security-Policy", "value": "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self' https://api.rss2json.com" }
]
}
]
}
F1.8 — Self-host Google Fonts (eliminate external CSS request)
Download woff2 files and serve them locally to remove the Google Fonts CSS round-trip entirely:
# 1. Download woff2 files from Google Fonts CSS URL
# Open https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap
# in a browser, then download each woff2 URL listed in the @font-face blocks.
# 2. Place files in public/fonts/ or src/assets/fonts/
public/fonts/
inter-v12-latin-400.woff2
inter-v12-latin-700.woff2
# 3. Add @font-face CSS (load once, no external request)
/* src/styles/fonts.css */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/inter-v12-latin-400.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/inter-v12-latin-700.woff2') format('woff2');
}
/* Remove the old Google Fonts <link> from index.html */
/* Before: */
<link href="https://fonts.googleapis.com/css2?family=Inter&display=swap" rel="stylesheet">
/* After: just use the font-family normally */
body { font-family: 'Inter', sans-serif; }
Result: Zero external CSS requests, faster FCP/LCP, no FOIT risk, and works offline.
F1.9 — Resize oversized icons
Icons (favicon, apple-touch-icon, OG image) should never be > 50KB. Check and resize:
python3 -c "
from PIL import Image
img = Image.open('favicon.png')
img.resize((192, 192)).save('favicon.png', 'PNG', optimize=True)
img.resize((32, 32)).save('favicon-32x32.png', 'PNG', optimize=True)
img.resize((16, 16)).save('favicon-16x16.png', 'PNG', optimize=True)
"
F2.1 — Remove LCP element lazy loading
The LCP element must NEVER be lazy-loaded:
<!-- Before: wrong — LCP image is lazy -->
<img src="hero.webp" loading="lazy" ...>
<!-- After: eager load the above-fold LCP element -->
<img src="hero.webp" loading="eager" fetchpriority="high" ...>
F2.2 — Preload LCP image
⚠️ Only works for files in public/ or with stable URLs. If using Vite/Webpack (content-hashed filenames), use <picture> + fetchPriority="high" instead:
<!-- For stable URLs (public/ directory): -->
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high">
<!-- For hashed filenames (Vite/Rollup): use component-level approach -->
<picture>
<source srcSet={webpImage} type="image/webp" />
<img src={jpgImage} fetchPriority="high" loading="eager" width="1920" height="1080" />
</picture>
F2.3 — Reduce unused JS (Vite/Rollup config)
// vite.config.js — enable manual chunking
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
rss: ['rss-parser'],
}
}
}
}
F2.4 — Eliminate forced reflows
// Before: reads layout property inside animation loop
element.addEventListener('scroll', () => {
const h = element.offsetHeight; // triggers reflow
doSomething(h);
});
// After: cache geometry reads outside event handlers
const h = element.offsetHeight; // read once
element.addEventListener('scroll', () => {
doSomething(h);
});
F2.5 — Optimise DOM size
If DOM > 1,500 elements:
F3.1 — External API in critical path (e.g. api.rss2json.com)
Current: HTML → JS bundle → external API (adds 1,574ms to critical path)
Solution: Move external API calls to build time or server-side:
// Option A: Fetch at build time (Astro/Next.js SSG)
export async function getStaticProps() {
const res = await fetch('https://api.rss2json.com/v1/api.json?rss_url=...');
const data = await res.json();
return { props: { posts: data.items }, revalidate: 3600 };
}
// Option B: Edge function / serverless proxy
// Cache RSS response at CDN edge, return stale-while-revalidate
F3.2 — Content Security Policy (full CSP)
Build the CSP iteratively:
Content-Security-Policy-Report-OnlyBefore deploying any fix batch, run these checks:
Build:
□ npm run build (or equivalent) — exits 0
□ npm run lint / typecheck — no new errors vs baseline
□ Inspect dist/ output:
- No single JS chunk > 500KB (gzipped)
- CSS < 100KB
- Code-splitting created separate vendor chunks
Asset verification:
□ For Vite/Rollup/Webpack: preload <link> in index.html won't match hashed filenames.
Use fetchPriority="high" + <picture> on the component instead.
□ Favicons and icons are < 50KB each (not multi-MB source images used as icons)
□ WebP/AVIF versions exist alongside originals
Deploy target:
□ If dual-deployed (Netlify + Vercel), verify headers on BOTH
□ If using SPA framework: verify meta tags via "View Page Source", not DevTools Elements
(react-helmet-async injects at runtime — check prerendered/SSR output)
After deploying each fix batch, verify:
Performance:
□ Re-run PageSpeed Insights on mobile AND desktop
□ LCP < 2.5s (Good)
□ FCP < 1.8s (Good)
□ TBT < 200ms (Good)
□ CLS < 0.1 (Good)
□ SI < 3.4s (Good)
Accessibility:
□ Run axe DevTools browser extension
□ Navigate page with keyboard only (Tab, Shift+Tab, Enter, Space)
□ Test with screen reader (NVDA/VoiceOver)
□ Check contrast with browser DevTools accessibility panel
Best Practices:
□ Verify security headers at https://securityheaders.com
□ Check HTTPS: no mixed content warnings in DevTools
□ Run Lighthouse Best Practices audit again
SEO:
□ Validate structured data at https://search.google.com/test/rich-results
□ Check robots.txt at /robots.txt
□ Verify canonical tag in page source (View Source, not DevTools)
□ Submit updated sitemap to Google Search Console
| Metric | Good | Needs Work | Poor |
|---|---|---|---|
| FCP | < 1.8s | 1.8–3.0s | > 3.0s |
| LCP | < 2.5s | 2.5–4.0s | > 4.0s |
| TBT | < 200ms | 200–600ms | > 600ms |
| CLS | < 0.1 | 0.1–0.25 | > 0.25 |
| SI | < 3.4s | 3.4–5.8s | > 5.8s |
User: "My site scores 65 on Performance. LCP is 4.2s."
Agent:
User: "Why is my LCP slow?"
Agent:
After each fix batch, log what changed and whether it caused build failures:
| Fix | File(s) Modified | Build Pass? | Errors | Revert Steps |
|---|---|---|---|---|
F1.1 — CSS @import → <link> | index.html, src/styles/*.css | □ Yes □ No | Restore original <link> tags | |
| F1.2 — Defer render-blocking CSS | index.html | □ Yes □ No | Remove media="print" + onload | |
| F1.4 — WebP conversion | public/images/*.webp | □ Yes □ No | Delete .webp files, restore originals | |
| F1.5 — Image dimensions | src/components/*.tsx | □ Yes □ No | Remove width/height/loading attrs | |
| F1.6 — Security headers (Netlify) | netlify.toml | □ Yes □ No | Delete the [[headers]] block | |
| F1.7 — Security headers (Vercel) | vercel.json | □ Yes □ No | Remove the "headers" array entry | |
| F1.8 — Self-host fonts | public/fonts/*.woff2, src/styles/fonts.css, index.html | □ Yes □ No | Delete font files, remove @font-face, restore Google Fonts <link> | |
| F1.9 — Resize icons | public/favicon*, public/apple-touch-icon*, public/og-image* | □ Yes □ No | Restore original icon files | |
| F2.1 — LCP eager loading | src/components/*.tsx | □ Yes □ No | Change loading="eager" back to loading="lazy" | |
| F2.2 — Preload LCP image | index.html or src/components/*.tsx | □ Yes □ No | Remove <link rel="preload"> or revert <picture> | |
| F2.3 — Code-split JS | vite.config.ts | □ Yes □ No | Remove manualChunks config | |
| F2.4 — Fix forced reflows | src/**/*.ts | □ Yes □ No | Revert geometry caching changes | |
| F2.5 — Optimise DOM | src/components/*.tsx | □ Yes □ No | Restore removed hidden nodes | |
| F3.1 — External API to build time | src/**/*.ts, config files | □ Yes □ No | Restore client-side fetch | |
| F3.2 — CSP headers | netlify.toml / vercel.json | □ Yes □ No | Remove or relax CSP directives |
If Build Pass? is No, run npm run build to see the exact error, revert the failed fix immediately, and re-test before applying the next batch.
See references/ for deep-dives:
references/performance-deep-dive.md — LCP, CLS, TBT root cause treesreferences/security-headers.md — Complete CSP/HSTS/COOP referencereferences/image-optimization.md — WebP/AVIF conversion pipelinesnpx claudepluginhub sickn33/antigravity-awesome-skills --plugin antigravity-bundle-aas-python-api-builderAnalyzes Lighthouse audit results and proposes prioritized improvements for Performance (Core Web Vitals: LCP, INP, CLS), Accessibility, Best Practices, and SEO.
Diagnoses web performance issues, fixes Core Web Vitals (LCP, INP, CLS), optimizes bundles, assets, and render performance. Use for slow pages or dropping traffic due to speed.
Audits websites for performance, accessibility, SEO, and best practices using Lighthouse metrics like Core Web Vitals, resource optimization, and contrast checks.