
Perf Expert
- 7 installs
- 13 repo stars
- Updated January 26, 2026
- hardikpandya/perf-analyzer
Audits a website for Core Web Vitals, accessibility, and SEO issues using Lighthouse and delivers a prioritized, actionable fix plan with file paths.
About
A frontend performance skill that runs Lighthouse, identifies the stack, and categorizes issues by severity with expected impact. A developer uses it to improve LCP, INP, CLS, accessibility, and SEO scores on a site.
- Establishes Lighthouse baselines and targets for LCP, INP, CLS, TTFB
- Focus modes for performance, accessibility, or SEO
Perf Expert by the numbers
- 7 all-time installs (skills.sh)
- Ranked #852 of 1,352 Code Review & Quality skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/hardikpandya/perf-analyzer --skill perf-expertAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 7 |
|---|---|
| repo stars | ★ 13 |
| Last updated | January 26, 2026 |
| Repository | hardikpandya/perf-analyzer ↗ |
What it does
Audits a website for Core Web Vitals, accessibility, and SEO issues using Lighthouse and delivers a prioritized, actionable fix plan with file paths.
Files
Performance expert
You are a senior frontend performance engineer. When this skill is invoked, conduct a comprehensive audit and deliver an actionable improvement plan.
What you deliver
Every audit must include:
1. Current state assessment — Run Lighthouse, identify the tech stack, note existing optimizations 2. Issues found — Categorized by severity (critical, moderate, minor) 3. Actionable fixes — Specific code changes with file paths and line numbers 4. Expected impact — What each fix improves and by how much 5. Priority order — What to fix first for maximum impact
Usage
/perf-expert # Full audit
/perf-expert performance # Performance focus
/perf-expert a11y # Accessibility focus
/perf-expert seo # SEO focusAudit methodology
Step 1: Establish baselines
Run Lighthouse and note current scores:
npx lighthouse https://yoursite.com --output json --output-path baseline.jsonExtract the metrics that matter:
| Metric | Target | Google ranking factor? |
|---|---|---|
| LCP (Largest contentful paint) | < 2.5s | Yes |
| INP (Interaction to next paint) | < 200ms | Yes |
| CLS (Cumulative layout shift) | < 0.1 | Yes |
| TTFB (Time to first byte) | < 800ms | No, but affects LCP |
Step 2: Identify the stack
Check for package.json, _config.yml, next.config.js, vite.config.js. Each framework has different bottlenecks and optimization paths.
Step 3: Audit each category
Performance — Scripts, fonts, images, bundle size. See references/performance.md.
Accessibility — Focus states, skip links, semantic HTML, keyboard navigation. See references/accessibility.md.
SEO — Title tags, meta descriptions, robots.txt, canonical URLs.
Browser compatibility — Safari bugs, minifier issues. See references/browser-gotchas.md.
Step 4: Deliver the report
Use the format in references/report-template.md. Always include:
- What you found (with evidence)
- How to fix it (with code)
- Why it matters (impact on metrics)
- Priority order (what to fix first)
Quick reference
Critical issues (fix immediately)
- Render-blocking scripts without
defer - Fonts preloaded as wrong format (OTF declared as WOFF2)
- Missing image dimensions causing layout shift
outline: nonewithout:focus-visiblereplacement- Missing skip link
High impact fixes
| Fix | Typical improvement |
|---|---|
Add defer to scripts | -200-500ms to TTI |
| Switch to WOFF2 fonts | -30% font size |
Add font-display: swap | Eliminates invisible text |
| Exclude unused fonts | Often 10-70MB saved |
| Add image dimensions | CLS drops to near zero |
Commands to run
# Full Lighthouse audit
npx lighthouse https://site.com --output html --view
# Scan entire site
npx unlighthouse --site https://site.com
# Accessibility audit
npx pa11y https://site.com
# Check build size
du -sh _site/ dist/ build/ .next/ 2>/dev/nullExample output format
Your audit report should look like this:
## Performance audit for example.com
### Current scores
- Performance: 67
- Accessibility: 82
- Best practices: 92
- SEO: 100
### Critical issues
#### 1. Render-blocking scripts (Performance: -15 points)
**Problem**: 3 scripts in `<head>` block rendering.
**Files**:
- `_includes/head.html:12` — analytics.js
- `_includes/head.html:15` — app.js
- `_includes/head.html:18` — utils.js
**Fix**:<!-- Before --> <script src="/js/app.js"></script>
<!-- After --> <script src="/js/app.js" defer></script>
**Impact**: Estimated +10-15 performance points, -300ms to FCP.
---
#### 2. Font preload MIME mismatch (Best practices: -8 points)
**Problem**: Preloading OTF files but declaring as WOFF2.
**File**: `_includes/head.html:8`
**Fix**:<!-- Before --> <link rel="preload" href="/fonts/body.otf" as="font" type="font/woff2">
<!-- After --> <link rel="preload" href="/fonts/body.woff2" as="font" type="font/woff2" crossorigin>
**Impact**: Eliminates console warning, proper font caching.
---
### Recommended priority
1. Add defer to scripts (quick win, big impact)
2. Fix font preloads (prevents double downloads)
3. Add image dimensions (fixes CLS)
4. Implement skip link (accessibility requirement)
### Expected results after fixes
- Performance: 67 → 85-90
- Accessibility: 82 → 95+
- CLS: 0.15 → < 0.05License
MIT
.DS_Store
MIT License
Copyright (c) 2026 Hardik Pandya
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
perf-analyzer
A Claude Code skill that delivers comprehensive frontend performance audits with actionable improvement plans.
This isn't a checklist. It's a methodology for diagnosing performance issues and fixing them systematically.
What you get
When you invoke /perf-expert, you receive:
1. Current state assessment with Lighthouse scores and Core Web Vitals 2. Issues found categorized by severity (critical, moderate, minor) 3. Specific fixes with file paths, line numbers, and code changes 4. Expected impact for each fix 5. Priority order for maximum impact with minimum effort
Skill structure
perf-expert/
├── SKILL.md # Core instructions (~180 lines)
├── references/
│ ├── performance.md # Scripts, fonts, images, bundles
│ ├── accessibility.md # Focus, ARIA, keyboard, contrast
│ ├── browser-gotchas.md # Safari bugs, minifier issues
│ └── report-template.md # Audit summary format
├── README.md
└── LICENSEInstallation
Claude Code: Add this folder as a skill.
mkdir -p .claude/skills/perf-expert
curl -o .claude/skills/perf-expert/SKILL.md \
https://raw.githubusercontent.com/hardikpandya/perf-analyzer/main/SKILL.mdDownload reference files for deep dives:
mkdir -p .claude/skills/perf-expert/references
curl -o .claude/skills/perf-expert/references/performance.md \
https://raw.githubusercontent.com/hardikpandya/perf-analyzer/main/references/performance.md
curl -o .claude/skills/perf-expert/references/accessibility.md \
https://raw.githubusercontent.com/hardikpandya/perf-analyzer/main/references/accessibility.md
curl -o .claude/skills/perf-expert/references/browser-gotchas.md \
https://raw.githubusercontent.com/hardikpandya/perf-analyzer/main/references/browser-gotchas.md
curl -o .claude/skills/perf-expert/references/report-template.md \
https://raw.githubusercontent.com/hardikpandya/perf-analyzer/main/references/report-template.mdClaude Projects: Upload SKILL.md and reference files to project knowledge.
API calls: Include SKILL.md in your system prompt. Reference files load on demand.
Usage
/perf-expert # Full audit
/perf-expert performance # Performance focus
/perf-expert a11y # Accessibility focus
/perf-expert seo # SEO focusWhat makes this different
It delivers actionable fixes, not vague advice
Generic advice like "optimize images" is useless. This skill tells you:
- Which files to change and what line numbers
- The exact code to add or remove
- How much improvement to expect
It's battle-tested
Every recommendation comes from real production issues:
- The Safari bug where CSS variables in
@keyframessilently fail - The Jekyll minifier that breaks
calc()expressions - The invisible CLS from web fonts that only shows on slow 3G
It knows the targets
| Metric | Good | Needs work | Poor |
|---|---|---|---|
| LCP | < 2.5s | 2.5-4s | > 4s |
| INP | < 200ms | 200-500ms | > 500ms |
| CLS | < 0.1 | 0.1-0.25 | > 0.25 |
| TTFB | < 800ms | 800ms-1.8s | > 1.8s |
Lighthouse scores are vanity metrics. Core Web Vitals are what Google ranks you on.
Files
| File | Purpose |
|---|---|
SKILL.md | Core instructions and audit methodology |
references/performance.md | Scripts, fonts, images, bundle optimization |
references/accessibility.md | Focus states, ARIA, keyboard navigation |
references/browser-gotchas.md | Safari bugs, minifier issues, edge cases |
references/report-template.md | Audit report format |
Real results
Developed while optimizing hvpandya.com:
- Font bundle: 74MB → 3MB (excluded 48 unused font families)
- LCP: 4.2s → 2.3s (font preloading +
font-display: swap) - Accessibility score: 88 → 98 (focus states, ARIA labels, keyboard nav)
- Fixed: Safari animation bug that silently broke emoji reactions
When to skip this
- Prototypes: Ship fast, optimize later
- Internal tools: User tolerance is higher
- MVP validation: Learn first, polish second
Performance optimization has diminishing returns. Know when good enough is good enough.
Contributing
Found a technique that's not covered? Discovered a browser quirk? Open a PR.
The best contributions include:
- The problem you encountered
- Why the obvious solution didn't work
- What actually fixed it
- How to verify the fix
License
MIT
Author
Accessibility deep dive
Accessibility isn't charity. It's good engineering. Accessible sites are more usable for everyone.
Focus states: the keyboard user's lifeline
The problem: Designers remove focus outlines because they're "ugly." Keyboard users can't see where they are.
Find the anti-pattern:
grep -r "outline.*none" --include="*.css"The fix:
/* Delete this */
/* *:focus { outline: none; } */
/* Add this */
:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}
/* Optional: explicitly remove for mouse users */
:focus:not(:focus-visible) {
outline: none;
}Why `:focus-visible`? It only shows for keyboard navigation, not mouse clicks.
---
Skip links: respect your users' time
Imagine tabbing through a 50-item navigation on every page.
The fix:
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<header><!-- nav items --></header>
<main id="main-content">...</main>
</body>.skip-link {
position: absolute;
top: -100%;
left: 16px;
padding: 8px 16px;
background: #000;
color: #fff;
z-index: 9999;
}
.skip-link:focus { top: 16px; }---
Semantic HTML: divs are not buttons
The problem: <div onclick="..."> looks like a button but isn't one. Screen readers don't announce it. Keyboard users can't reach it.
Find offenders:
grep -r "div.*onclick\|span.*onclick" --include="*.html" --include="*.jsx"
grep -r 'role="button"' --include="*.html"The fix:
<!-- Before -->
<div class="btn" onclick="submit()">Submit</div>
<!-- After -->
<button type="button" onclick="submit()">Submit</button>| Don't | Do |
|---|---|
<div> with click handler | <button> |
<div> for navigation | <nav> |
<div> for main content | <main> |
<span> for links | <a href="..."> |
---
ARIA: the last resort
ARIA is for when HTML semantics aren't enough. Native elements are always better.
Icon-only buttons need labels:
<!-- Bad: screen reader hears "button" -->
<button><svg><!-- X icon --></svg></button>
<!-- Good: screen reader hears "Close dialog, button" -->
<button aria-label="Close dialog"><svg><!-- X icon --></svg></button>Dynamic content needs live regions:
<div aria-live="polite" aria-atomic="true">
<span class="count">5 reactions</span>
</div>---
Keyboard navigation
Test manually: Tab through your entire page. Can you reach everything? Can you activate it?
Custom elements need keyboard handlers:
element.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleClick();
}
});---
Focus traps for modals
When a modal opens, Tab should cycle within it.
function trapFocus(modal) {
const focusable = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const first = focusable[0];
const last = focusable[focusable.length - 1];
modal.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
if (e.key === 'Escape') closeModal();
});
first.focus();
}
let previousFocus;
function openModal() {
previousFocus = document.activeElement;
modal.hidden = false;
trapFocus(modal);
}
function closeModal() {
modal.hidden = true;
previousFocus?.focus();
}---
Headings: the screen reader's table of contents
Screen reader users navigate by headings. Skipping levels is like a book with missing chapters.
Rules:
- One
<h1>per page - Never skip levels (h1 → h3 is wrong)
- Use headings for structure, not styling
<!-- Correct -->
<h1>Article title</h1>
<h2>Section</h2>
<h3>Subsection</h3>
<!-- Wrong -->
<h1>Article title</h1>
<h3>Section</h3>---
Color contrast
WCAG AA requires:
- 4.5:1 for normal text
- 3:1 for large text (18px+ or 14px+ bold)
- 3:1 for UI components
Common failures:
- Placeholder text (
#999on white = 2.85:1) - Muted text (
#868e96on white = 3.32:1)
Use browser DevTools color picker or WebAIM Contrast Checker.
Browser gotchas
Real bugs from production that will waste your time if you don't know about them.
Safari: CSS variables in @keyframes
The bug: CSS custom properties don't work in @keyframes in Safari. It silently fails.
/* This silently fails in Safari */
@keyframes fly {
to { transform: translate(var(--end-x), var(--end-y)); }
}The fix: Use Web Animations API instead:
element.animate([
{ transform: 'translateY(0)' },
{ transform: `translateY(${endY}px)` }
], {
duration: 500,
easing: 'ease-out',
fill: 'forwards'
});---
CSS minifiers breaking calc()
The bug: Some minifiers remove spaces in calc() expressions with custom properties, breaking them.
/* Before minification */
padding: calc(var(--spacing) * 2);
/* After aggressive minification (broken) */
padding: calc(var(--spacing)*2);The fix: Test your production build, not just dev. Or configure your minifier to preserve spaces in calc().
---
MIME type mismatches
The bug: You declare a font as WOFF2 but the file is actually OTF. Browser gets confused, sometimes fails silently.
<!-- Wrong: file is OTF, type says WOFF2 -->
<link rel="preload" href="/fonts/body.otf" as="font" type="font/woff2" crossorigin>
<!-- Correct: match type to actual file -->
<link rel="preload" href="/fonts/body.woff2" as="font" type="font/woff2" crossorigin>---
Mixed content warnings
The bug: HTTPS page loads HTTP resource. Browser may block it or show warnings.
Find it:
grep -r "http://" --include="*.html" --include="*.css" --include="*.js" | grep -v localhostThe fix: Change all URLs to https:// or use protocol-relative //.
---
Preload without crossorigin
The bug: Font preload without crossorigin attribute won't match the actual font request, causing a double download.
<!-- Wrong: will download font twice -->
<link rel="preload" href="/fonts/body.woff2" as="font" type="font/woff2">
<!-- Correct: add crossorigin even for same-origin fonts -->
<link rel="preload" href="/fonts/body.woff2" as="font" type="font/woff2" crossorigin>---
Layout shift from web fonts
The bug: Text renders with fallback font, then shifts when custom font loads. This counts against CLS.
The fix:
1. Use font-display: swap (accepts the shift but shows text immediately) 2. Match fallback font metrics to custom font using Font Style Matcher 3. Or use font-display: optional (may not load custom font on slow connections)
---
iOS Safari 100vh bug
The bug: 100vh includes the address bar height on iOS Safari, causing content to be cut off.
The fix:
/* Old way */
height: 100vh;
/* New way - respects actual viewport */
height: 100dvh;
/* Fallback for older browsers */
height: 100vh;
height: 100dvh;---
Debugging tips
1. Test on real devices: Simulators don't catch everything 2. Test on slow connections: Chrome DevTools → Network → Slow 3G 3. Test production builds: Dev mode often hides issues 4. Check console for silent failures: Especially font loading and CORS errors
Performance deep dive
Scripts: the render blockers
The problem: Scripts in <head> without defer or async block HTML parsing.
Find them:
grep -r "<script" --include="*.html" | grep -v "defer\|async\|type=\"module\""The fix:
<!-- Before: blocks rendering -->
<script src="app.js"></script>
<!-- After: loads in parallel, executes after parsing -->
<script src="app.js" defer></script>When to use what:
| Attribute | Behavior | Use for |
|---|---|---|
defer | Download parallel, execute in order after HTML parsed | App bundles, anything that needs DOM |
async | Download parallel, execute immediately when ready | Analytics, tracking, independent scripts |
type="module" | Deferred by default, strict mode | Modern ES modules |
| None | Blocks everything | Almost never |
The trap: Don't use both defer and async. async wins and execution order is lost.
---
Fonts: the silent killer
Fonts are the #1 cause of slow LCP on text-heavy sites.
The problem: Browser won't paint text until fonts load. On slow connections, users see nothing for seconds.
Find issues:
# Check what's being preloaded
grep -r "preload.*font" --include="*.html"
# Find @font-face declarations
grep -r "@font-face" --include="*.css"
# List all font files
find . -type f \( -name "*.woff2" -o -name "*.woff" -o -name "*.otf" \) | head -20The fix:
1. Use WOFF2: 30% smaller than WOFF, supported everywhere since 2018.
<!-- Wrong -->
<link rel="preload" href="/fonts/body.otf" as="font" crossorigin>
<!-- Right -->
<link rel="preload" href="/fonts/body.woff2" as="font" type="font/woff2" crossorigin>2. Preload only critical fonts: Usually 2-3 max. The ones used above the fold.
3. Add font-display: swap:
@font-face {
font-family: 'Body';
src: url('/fonts/body.woff2') format('woff2');
font-display: swap;
}4. Exclude unused fonts from build:
# Jekyll _config.yml
exclude:
- assets/fonts/unused-family/I've seen font directories go from 74MB to 3MB by excluding unused families.
The trap: font-display: swap causes a flash of unstyled text (FOUT). The alternative—invisible text for 3 seconds—is worse.
---
Images: the obvious wins
Always specify dimensions (prevents layout shift):
<img src="hero.jpg" width="1200" height="600" alt="Hero">Lazy load below-fold images:
<img src="photo.jpg" loading="lazy" alt="Description">Use modern formats:
<picture>
<source srcset="image.avif" type="image/avif">
<source srcset="image.webp" type="image/webp">
<img src="image.jpg" alt="Fallback">
</picture>The trap: Don't lazy load the LCP image. That's the opposite of what you want.
---
The hidden bloat
Check your build output:
du -sh _site/
find _site/ -type f -size +100k | sort -k1 -hCommon culprits:
- Unused font families: Shipped the whole type family when you only use regular and bold
- Source maps in production: Great for debugging, terrible for users
- Unoptimized images: A 4MB PNG that should be a 200KB WebP
Audit report template
Use this format to summarize findings after running an audit.
---
Performance audit
Fixed
- Added
deferto X render-blocking scripts - Switched font preloads from OTF to WOFF2 (saved XXX KB)
- Excluded X unused font weights from build (saved X.X MB)
- Added
loading="lazy"to X below-fold images - Added
width/heightto X images
Manual review needed
- Hero image is X.X MB — consider compression or WebP
- Third-party script (analytics/chat/etc.) adds XXX ms to TTI
- Consider lazy loading below-fold components
---
Accessibility audit
Fixed
- Replaced global
outline: nonewith:focus-visiblestyles - Added skip link to main layout
- Converted X
<div onclick>to<button>elements - Added
aria-labelto X icon-only buttons - Added keyboard handlers to custom interactive elements
Manual review needed
- Color contrast on
.class-name(currently X.XX:1, needs 4.5:1) - X images need descriptive alt text (list paths)
- Heading hierarchy: h1 → h3 skip on
/page-name
---
SEO audit
Fixed
- Removed duplicate
<title>tag from template - Added meta descriptions to X pages
- Created
robots.txtwith sitemap reference - Added canonical URLs
Manual review needed
- X pages missing meta descriptions
- Title on
/page-nameexceeds 60 characters
---
Best practices audit
Fixed
- Corrected MIME types on font preloads
- Removed X
console.logstatements - Fixed mixed content (HTTP → HTTPS)
Manual review needed
- Safari animation bug potential in
/path/to/file.js - Review third-party scripts for security
---
Core Web Vitals
| Metric | Before | After | Target |
|---|---|---|---|
| LCP | X.Xs | X.Xs | < 2.5s |
| CLS | X.XX | X.XX | < 0.1 |
| INP | — | — | < 200ms |
Notes:
- INP requires real user data (lab tests measure TBT instead)
- Field data available at PageSpeed Insights
---
Summary
| Category | Before | After |
|---|---|---|
| Performance | XX | XX |
| Accessibility | XX | XX |
| Best practices | XX | XX |
| SEO | XX | XX |
Key wins
1. 2. 3.
Remaining work
1. 2.