PageSpeed Enhancer Skill

← Back to skills

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.

Category: General & Miscellaneous
Repo: antigravity-awesome-skills
Path: skills/pagespeed-enhancer/SKILL.md
Updated: 6/15/2026, 9:11:44 AM

AI Summary

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. It is useful for general automation, multi-purpose workflows, cross-disciplinary tasks, and utility skills. Source: antigravity-awesome-skills (skills/pagespeed-enhancer/SKILL.md).

PageSpeed Enhancer 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.


When to Use This Skill

  • User pastes a PageSpeed Insights report or mentions Lighthouse scores
  • User asks to improve Core Web Vitals (LCP, FCP, CLS, TBT, SI)
  • User needs help with render-blocking resources, unused JavaScript, image optimisation, security headers, ARIA compliance, or SEO meta-tag fixes
  • User asks "why is my LCP slow", "fix accessibility issues", "improve my SEO score", or "my site scores 80 on performance"
  • Any mention of PageSpeed, Lighthouse, Web Vitals, or site speed

High-Level Workflow

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

PHASE 1 — Ingest & Classify

When the user provides a PageSpeed Insights report (pasted text, screenshot, or URL):

  1. Extract the four pillar scores: Performance, Accessibility, Best Practices, SEO.
  2. Extract each flagged metric with its value and Lighthouse weight.
  3. Identify the critical path bottleneck (the single issue most responsible for the lowest pillar score).
  4. Output a Score Summary Table:
| 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.


PHASE 2 — Batch Scan (4 Sections)

Run all four section scans. Present as collapsible sections in output.

Batch A — Performance Scan

Audit these in order (highest Lighthouse weight first):

AuditMetric ImpactKey Questions
LCP breakdownLCPIs the LCP element lazily loaded? Is TTFB > 600ms? Is element render delay > 1s?
Render-blocking resourcesFCP, LCPWhich CSS/JS files block the critical path? Can they be deferred or inlined?
CSS @import rulesFCP, LCPAre 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 JavaScriptFCP, LCP, TBTWhat % of the main bundle is unused? Is code-splitting possible?
Network dependency treeLCPWhat is the critical path chain? Max latency?
Forced reflowsTBTWhich JS functions query geometry after DOM mutation?
Image deliveryFCP, LCPAre images in WebP/AVIF? Are above-fold images lazy-loaded?
Speed IndexSIIs page visually progressive or does it paint all at once?
CLS culpritsCLSAny images without width/height? Any late-injected content?
JavaScript execution timeTBTTotal parse + compile + evaluate time?
Long main-thread tasksTBTTasks > 50ms? Starting when?
Bundled asset sizesFCP, LCP, TBTCheck dist/ output: any single JS chunk > 500KB gzipped? CSS > 100KB? Code-splitting creating proper vendor chunks?

For each audit item, output:

  • Finding: What the report says
  • Root Cause: Why it's happening
  • Fix Category: Quick Win / Medium Effort / Refactor Required

Batch B — Accessibility Scan

Focus on any failed audits. For a 100-score page, still check:

CheckWhat to Verify
ARIA attribute correctnessAll aria-* attributes match element roles
Colour contrastAll text meets WCAG AA (4.5:1 normal, 3:1 large)
Image alt text qualityAlt text is descriptive, not filename-style
Keyboard navigationAll interactive elements reachable by Tab
Skip linksPresent and focusable
Heading hierarchyNo skipped levels (h1 → h2 → h3)
Touch target sizeMin 44×44px on mobile
Form labelsEvery input has an associated label
lang attribute<html lang="en"> present and valid BCP 47
font-displaySet to swap or optional to prevent FOIT

Batch C — Best Practices Scan

Security headers are often unflagged by Lighthouse score but are critical. Check ALL deployment targets:

CheckHeader/SettingWhere to ConfigureSeverity
Content Security PolicyContent-Security-Policynetlify.toml [[headers]] / vercel.json "headers"🔴 High
Cross-Origin-Opener-PolicyCOOP headerSame as above🔴 High
Clickjacking protectionX-Frame-Options or CSP frame-ancestorsSame as above🔴 High
HSTS configurationStrict-Transport-Security with includeSubDomains + preloadSame as above🟡 Medium
Trusted Types (DOM XSS)CSP require-trusted-types-for 'script'Same as above🟡 Medium
X-Content-Type-Optionsnosniff headerSame as above🟡 Medium
Referrer-Policystrict-origin-when-cross-originSame as above🟡 Medium
Permissions-PolicyRestrict camera/mic/geolocationSame as above🟡 Medium
Third-party cookiesAny SameSite=None cookies without Secure?🟡 Medium
Deprecated APIsAny browser-deprecated JS APIs in use?🟢 Low
Source mapsAre source maps deployed for debugging?🟢 Low

When both netlify.toml and vercel.json exist, check BOTH. Each has a different syntax (TOML vs JSON).

Batch D — SEO Scan

CheckWhat to Verify
<title> tagPresent, 50–60 chars, includes primary keyword
Meta descriptionPresent, 150–160 chars, compelling
Canonical tag<link rel="canonical"> points to correct URL
hreflangPresent if multilingual; correct language codes
robots.txtValid, not blocking key resources
Structured dataJSON-LD present; run Schema validator
Image alt attributesEvery <img> has meaningful alt
Link descriptivenessNo "click here" / "read more" link text
CrawlabilityNo noindex on important pages
HTTP status200 on main page and critical resources
SPA meta injectionIf using react-helmet-async / Next.js Head: verify via "View Page Source", not DevTools Elements — meta tags may be JS-injected

PHASE 3 — Risk Report

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:

  • 🟢 Low: Config/header change, no code change. Rollback in < 5 min.
  • 🟡 Medium: Build config or asset pipeline change. Test in staging first.
  • 🔴 High: JavaScript refactor, architectural change. Requires full QA cycle.

Always recommend: fix P1 (Low Risk, High Impact) items first, then P2, then P3.


PHASE 4 — Fix Batches

Apply fixes in risk order. For each fix, provide:

  1. What to change — file, line, specific change
  2. Before (code snippet)
  3. After (code snippet)
  4. Expected metric improvement — estimated delta
  5. How to verify — what to check after deploying

Fix Batch 1 — Quick Wins (Low Risk, deploy immediately)

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)
"

Fix Batch 2 — Medium Effort (staging test recommended)

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:

  • Use virtual scrolling for long lists (react-virtual, TanStack Virtual)
  • Lazy-render off-screen sections
  • Remove hidden/display:none nodes that never become visible

Fix Batch 3 — Refactor Required (full QA cycle)

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:

  1. Deploy in report-only mode first: Content-Security-Policy-Report-Only
  2. Check browser console for violations for 48h
  3. Whitelist required origins
  4. Promote to enforcement mode

PRE-DEPLOY GATE

Before 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)

PHASE 5 — Verification Checklist

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

Output Format Conventions

  • Always label outputs: [SCAN], [RISK], [FIX], [VERIFY]
  • Use emoji severity indicators: 🔴 Critical / 🟡 Warning / 🟢 Pass / ℹ️ Info
  • Always show "Before" and "After" code for every fix
  • Always include estimated metric delta (e.g. "Est. LCP improvement: -0.8s")
  • Never suggest fixes that conflict with each other — sequence matters

Quick Reference: Metric Thresholds

MetricGoodNeeds WorkPoor
FCP< 1.8s1.8–3.0s> 3.0s
LCP< 2.5s2.5–4.0s> 4.0s
TBT< 200ms200–600ms> 600ms
CLS< 0.10.1–0.25> 0.25
SI< 3.4s3.4–5.8s> 5.8s

Examples

Example 1: User pastes a PageSpeed report

User: "My site scores 65 on Performance. LCP is 4.2s."

Agent:

  1. Parses the score summary table — identifies LCP as critical bottleneck
  2. Runs Batch A scan — finds lazy-loaded hero image and render-blocking CSS
  3. Outputs risk report: F1.1 (CSS @import → link) ranked P1, F1.5 (LCP image eager) ranked P1
  4. Applies Fix Batch 1, verifies with re-test

Example 2: User asks about slow LCP

User: "Why is my LCP slow?"

Agent:

  1. Asks for a PageSpeed report URL or pasted results
  2. Runs LCP-specific audit from Batch A — checks TTFB, element render delay, lazy loading
  3. Identifies the LCP element, its current loading strategy, and the critical path chain
  4. Recommends targeted fix (preload, eager loading, or server response time improvement)

Limitations

  • Does not run actual Lighthouse or PageSpeed tests — the user must provide the report or URL
  • Security header recommendations assume the user controls the deployment platform (Netlify, Vercel, etc.)
  • Fixes are general patterns; exact file paths and config syntax may vary by project setup
  • Does not cover server-level optimisations (CDN config, PHP opcode caching, database queries, etc.)
  • Image conversion commands assume the user has the required tools installed (cwebp, sharp, Pillow)
  • CSP guidance uses a report-only iterative approach — the final policy must be tuned to each project's actual resource origins

Change Log & Revert Checklist

After each fix batch, log what changed and whether it caused build failures:

FixFile(s) ModifiedBuild Pass?ErrorsRevert Steps
F1.1 — CSS @import → <link>index.html, src/styles/*.css□ Yes □ NoRestore original <link> tags
F1.2 — Defer render-blocking CSSindex.html□ Yes □ NoRemove media="print" + onload
F1.4 — WebP conversionpublic/images/*.webp□ Yes □ NoDelete .webp files, restore originals
F1.5 — Image dimensionssrc/components/*.tsx□ Yes □ NoRemove width/height/loading attrs
F1.6 — Security headers (Netlify)netlify.toml□ Yes □ NoDelete the [[headers]] block
F1.7 — Security headers (Vercel)vercel.json□ Yes □ NoRemove the "headers" array entry
F1.8 — Self-host fontspublic/fonts/*.woff2, src/styles/fonts.css, index.html□ Yes □ NoDelete font files, remove @font-face, restore Google Fonts <link>
F1.9 — Resize iconspublic/favicon*, public/apple-touch-icon*, public/og-image*□ Yes □ NoRestore original icon files
F2.1 — LCP eager loadingsrc/components/*.tsx□ Yes □ NoChange loading="eager" back to loading="lazy"
F2.2 — Preload LCP imageindex.html or src/components/*.tsx□ Yes □ NoRemove <link rel="preload"> or revert <picture>
F2.3 — Code-split JSvite.config.ts□ Yes □ NoRemove manualChunks config
F2.4 — Fix forced reflowssrc/**/*.ts□ Yes □ NoRevert geometry caching changes
F2.5 — Optimise DOMsrc/components/*.tsx□ Yes □ NoRestore removed hidden nodes
F3.1 — External API to build timesrc/**/*.ts, config files□ Yes □ NoRestore client-side fetch
F3.2 — CSP headersnetlify.toml / vercel.json□ Yes □ NoRemove 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.


References

See references/ for deep-dives:

  • references/performance-deep-dive.md — LCP, CLS, TBT root cause trees
  • references/security-headers.md — Complete CSP/HSTS/COOP reference
  • references/image-optimization.md — WebP/AVIF conversion pipelines

Related skills