---
name: performance-profiler
description: Performance profiling and optimization for web apps — Core Web Vitals (LCP, INP, CLS), Lighthouse audits, bundle analysis, backend profiling (CPU, memory, DB queries), N+1 detection, caching strategies (Redis, CDN, HTTP), and performance budgets. Use when user asks to improve performance, run Lighthouse audit, profile a Node.js app, optimize Core Web Vitals, reduce bundle size, or investigate slow response times. Do NOT use for database schema optimization (use db-sculptor), Docker image optimization (use docker), or CDN configuration.
triggers:
- "performance"
- "optimize"
- "slow"
- "Lighthouse"
- "Core Web Vitals"
- "LCP"
- "INP"
- "CLS"
- "bundle size"
- "profiling"
- "N+1 query"
- "caching"
- "response time"
negatives:
- "schema optimization"
- "Docker image optimization"
- "CDN configuration"
- "database indexes"
license: MIT
compatibility: opencode
metadata:
workflow: quality
audience: developers
version: "4.0.0"
author: shokunin
allowed-tools: Read Bash Write WebFetch
Performance Profiler
Find and fix performance issues from frontend to backend. Based on Google Core Web Vitals, Lighthouse, Chrome DevTools, and production patterns from WebPageTest and clinic.js.
Sub-Commands
| Command |
Description |
audit |
Run comprehensive performance audit (frontend + backend) |
vitals |
Audit Core Web Vitals specifically (LCP, INP, CLS, TBT) |
lcp |
Optimize Largest Contentful Paint |
inp |
Fix Interaction to Next Paint (long tasks) |
bundle |
Analyze bundle size and split strategy |
backend |
Profile Node.js/Python backend performance |
budget |
Set and verify performance budgets |
Workflow
Step 1: Run Lighthouse audit (exact command)
npx lighthouse https://example.com --preset=desktop --output=html --output-path=./lighthouse-report.html
npx lighthouse https://example.com --preset=desktop --output=json --output-path=./lighthouse.json
Target scores:
| Metric |
Target |
Severity if missed |
| Performance |
> 90 |
Critical |
| LCP (Largest Contentful Paint) |
< 2.5s |
Critical |
| INP (Interaction to Next Paint) |
< 200ms |
Critical |
| CLS (Cumulative Layout Shift) |
< 0.1 |
Critical |
| TBT (Total Blocking Time) |
< 200ms |
High |
| FCP (First Contentful Paint) |
< 1.8s |
Medium |
| Speed Index |
< 3.4s |
Medium |
Step 2: Fix Core Web Vitals
LCP optimization (exact fixes)
<!-- 1. Preload LCP image -->
<link rel="preload" as="image" href="hero.webp" fetchpriority="high">
<!-- 2. LCP image: no lazy loading -->
<img src="hero.webp" width="1200" height="600" fetchpriority="high" alt="">
<!-- 3. Inline critical CSS in <head> -->
<style>
.hero { display: grid; min-height: 100dvh; }
.hero img { width: 100%; height: auto; aspect-ratio: 2/1; }
</style>
<!-- 4. Defer non-critical CSS -->
<link rel="preload" href="styles.css" as="style"
<!-- 5. Self-host fonts with font-display: swap -->
@font-face {
font-family: 'Geist';
src: url('/fonts/geist.woff2') format('woff2');
font-display: swap;
}
LCP sub-parts breakdown:
- TTFB (Time to First Byte): < 800ms. Optimize server response. CDN. Caching.
- Resource load delay: Preload + fetchpriority + no render-blocking.
- Resource load time: Compress, CDN, modern format (WebP/AVIF).
- Element render delay: Inline critical CSS. No layout-shifting JS above fold.
INP optimization (long task breakup)
// Break up long tasks with scheduler.yield()
async function processLargeDataset(items: Item[]) {
const CHUNK_SIZE = 50
for (let i = 0; i < items.length; i += CHUNK_SIZE) {
const chunk = items.slice(i, i + CHUNK_SIZE)
processChunk(chunk)
if (i + CHUNK_SIZE < items.length) {
// Yield to main thread every 50ms
await new Promise(resolve => setTimeout(resolve, 0))
}
}
}
// Or use scheduler.yield() (Chrome 115+)
for (let i = 0; i < items.length; i += CHUNK_SIZE) {
processChunk(items.slice(i, i + CHUNK_SIZE))
await scheduler.yield()
}
Common INP causes:
- Expensive event handlers (click, keydown, input)
- Synchronous layout reads/writes (layout thrashing)
- Large DOM manipulations in single frame
- JSON.parse() on large payloads (> 50KB)
CLS optimization
/* Reserve space for images */
img, video, iframe {
width: 100%;
height: auto;
aspect-ratio: attr(width) / attr(height);
}
/* Reserve space for dynamic content */
.ad-container {
min-height: 250px;
}
/* Prevent layout shift from web fonts */
body {
font-display: swap;
}
/* Fixed size for injected elements */
.cookie-banner {
min-height: 80px;
}
Step 3: Profile backend
# Node.js CPU profile
node --cpu-prof --cpu-prof-interval=1 server.js
# Analyze: clinic doctor -- node server.js
# Node.js heap snapshot
node --heapsnapshot server.js
# Load in Chrome DevTools > Memory tab
# Python profiling
python -m cProfile -o output.prof server.py
# Analyze: snakeviz output.prof
| Symptom |
Likely cause |
Fix |
| High CPU |
N+1 queries, synchronous crypto |
Add eager loading. Use async operations. |
| High memory |
No streaming. Array growth. |
Stream responses. Paginate results. |
| High latency p99 |
DB lock contention |
Add indexes. Use read replicas. |
| GC pauses |
High allocation rate |
Pool objects. Reduce allocations. Use Buffer pools. |
| Connection timeouts |
Pool exhaustion |
Increase pool size. Add connection queue. |
Step 4: Bundle analysis
# Next.js
ANALYZE=true next build
# Vite/Rollup
npx vite-bundle-visualizer
# Generic
npx source-map-explorer dist/**/*.js
| Target |
Budget |
| Total JS |
< 300KB (gzipped) |
| Total CSS |
< 50KB (gzipped) |
| Total fonts |
< 100KB |
| First load JS |
< 100KB (gzipped) |
| Total image payload |
< 500KB |
| Largest chunk |
< 150KB (gzipped) |
Step 5: Caching strategy decision tree
| What |
Cache where |
TTL |
| Static assets (JS, CSS, images) |
CDN + browser |
1 year (versioned filenames) |
| API responses (public, stable) |
CDN + HTTP Cache-Control |
5 min to 1h (stale-while-revalidate) |
| Dynamic data (user-specific) |
Redis |
30s to 5min |
| Database query results |
Application memory (LRU) |
10s |
| Full pages (public) |
CDN + Edge caching |
5 min |
| Auth tokens |
Never cache |
— |
Step 6: Performance budgets in CI
{
"ci": {
"assert": {
"assertions": {
"categories:performance": ["error", { "minScore": 0.9 }],
"largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
"interactive": ["error", { "maxNumericValue": 3800 }],
"total-blocking-time": ["error", { "maxNumericValue": 200 }],
"cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
"resource-summary:script:size": ["warn", { "maxNumericValue": 300000 }]
}
}
}
}
npx lighthouse https://staging.example.com --output=json | \
npx lighthouse-ci --collect --assert
Production Checklist
Anti-Patterns
| Anti-pattern |
Fix |
| Optimizing without measuring |
Always measure first. Field data > lab data. |
| Only testing on dev machine |
Test on real devices with throttling (3G, 4x CPU slowdown) |
| Cache everything |
Cache only what changes infrequently. Stale cache is worse than no cache. |
| Lazy loading above-fold images |
Only lazy load below-fold. LCP image must load immediately. |
| Bundle splitting too aggressively |
Split at route boundaries. Not by component. |
| Optimizing non-bottleneck |
Use Lighthouse + RUM to find actual bottleneck. |
| Ignoring mobile |
Mobile is 2-4x slower than desktop. Optimize for mobile first. |
| No performance budget |
CI must fail when budget exceeded. |
Sources
- web.dev — Core Web Vitals
- Lighthouse documentation
- Addy Osmani — Performance optimization patterns
- Paul Lewis — RequestAnimationFrame, compositor-only properties
- Node.js performance guide
- clinic.js — Node.js profiling
- WebPageTest — Real device testing
- Chrome UX Report — Field data for Core Web Vitals
Error Handling
| Cause |
Fix |
| Lighthouse audit crashes on SPA with hash routing |
Use --chrome-flags="--disable-web-security" or serve localhost via static server, never file:// |
| Lighthouse scores vary 5+ points between runs |
Normal. Run 3-5 times, use median. Always audit Incognito with no extensions. Variance from network jitter and CPU scheduling |
| Bundle analyzer can't parse source maps — blank treemap |
Verify devtool: 'hidden-source-map' in webpack/Vite. Regenerate build with source maps enabled. Check sourceMapFilename matches |
| RUM field data shows much worse LCP than lab (Lighthouse) |
Lab is synthetic (fast CPU/network). Field data from real users on slow devices. Trust field data. Optimize for p75, not median |
| Performance budget assertion fails CI on every PR |
Investigate which chunk exceeded budget immediately. Treat as build failure. Block merge until resolved or budget explicitly raised with justification |
| Chrome DevTools Performance tab freezes on large trace (>30s) |
Record shorter traces (5-10s max). Use --user-data-dir with clean profile. Export HAR for sharing instead of full trace |
clinic doctor fails with "Cannot find module" |
Works only with Node.js < 22. For Node 22+, use node --cpu-prof and analyze with Chrome DevTools. clinic is community-maintained, lagging Node releases |
| WebPageTest results vary significantly by test location |
Test from 2+ geographic regions. Use medianRun and repeatView in WPT API. Document which locations were tested in audit report |
Checklist
1---2name: performance-profiler3description: ---4---5---6name: performance-profiler7description: Performance profiling and optimization for web apps — Core Web Vitals (LCP, INP, CLS), Lighthouse audits, bundle analysis, backend profiling (CPU, memory, DB queries), N+1 detection, caching strategies (Redis, CDN, HTTP), and performance budgets. Use when user asks to improve performance, run Lighthouse audit, profile a Node.js app, optimize Core Web Vitals, reduce bundle size, or investigate slow response times. Do NOT use for database schema optimization (use db-sculptor), Docker image optimization (use docker), or CDN configuration.8triggers:9 - "performance"10 - "optimize"11 - "slow"12 - "Lighthouse"13 - "Core Web Vitals"14 - "LCP"15 - "INP"16 - "CLS"17 - "bundle size"18 - "profiling"19 - "N+1 query"20 - "caching"21 - "response time"22negatives:23 - "schema optimization"24 - "Docker image optimization"25 - "CDN configuration"26 - "database indexes"27license: MIT28compatibility: opencode29metadata:30 workflow: quality31 audience: developers32 version: "4.0.0"33 author: shokunin34allowed-tools: Read Bash Write WebFetch35---363738# Performance Profiler3940Find and fix performance issues from frontend to backend. Based on Google Core Web Vitals, Lighthouse, Chrome DevTools, and production patterns from WebPageTest and clinic.js.4142## Sub-Commands4344| Command | Description |45|---------|-------------|46| `audit` | Run comprehensive performance audit (frontend + backend) |47| `vitals` | Audit Core Web Vitals specifically (LCP, INP, CLS, TBT) |48| `lcp` | Optimize Largest Contentful Paint |49| `inp` | Fix Interaction to Next Paint (long tasks) |50| `bundle` | Analyze bundle size and split strategy |51| `backend` | Profile Node.js/Python backend performance |52| `budget` | Set and verify performance budgets |5354## Workflow5556### Step 1: Run Lighthouse audit (exact command)5758```bash59npx lighthouse https://example.com --preset=desktop --output=html --output-path=./lighthouse-report.html60npx lighthouse https://example.com --preset=desktop --output=json --output-path=./lighthouse.json61```6263Target scores:64| Metric | Target | Severity if missed |65|--------|--------|-------------------|66| Performance | > 90 | Critical |67| LCP (Largest Contentful Paint) | < 2.5s | Critical |68| INP (Interaction to Next Paint) | < 200ms | Critical |69| CLS (Cumulative Layout Shift) | < 0.1 | Critical |70| TBT (Total Blocking Time) | < 200ms | High |71| FCP (First Contentful Paint) | < 1.8s | Medium |72| Speed Index | < 3.4s | Medium |7374### Step 2: Fix Core Web Vitals7576#### LCP optimization (exact fixes)7778```html79<!-- 1. Preload LCP image -->80<link rel="preload" as="image" href="hero.webp" fetchpriority="high">8182<!-- 2. LCP image: no lazy loading -->83<img src="hero.webp" width="1200" height="600" fetchpriority="high" alt="">8485<!-- 3. Inline critical CSS in <head> -->86<style>87 .hero { display: grid; min-height: 100dvh; }88 .hero img { width: 100%; height: auto; aspect-ratio: 2/1; }89</style>9091<!-- 4. Defer non-critical CSS -->92<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">9394<!-- 5. Self-host fonts with font-display: swap -->95@font-face {96 font-family: 'Geist';97 src: url('/fonts/geist.woff2') format('woff2');98 font-display: swap;99}100```101102**LCP sub-parts breakdown:**1031. TTFB (Time to First Byte): < 800ms. Optimize server response. CDN. Caching.1042. Resource load delay: Preload + fetchpriority + no render-blocking.1053. Resource load time: Compress, CDN, modern format (WebP/AVIF).1064. Element render delay: Inline critical CSS. No layout-shifting JS above fold.107108#### INP optimization (long task breakup)109110```typescript111// Break up long tasks with scheduler.yield()112async function processLargeDataset(items: Item[]) {113 const CHUNK_SIZE = 50114115 for (let i = 0; i < items.length; i += CHUNK_SIZE) {116 const chunk = items.slice(i, i + CHUNK_SIZE)117 processChunk(chunk)118119 if (i + CHUNK_SIZE < items.length) {120 // Yield to main thread every 50ms121 await new Promise(resolve => setTimeout(resolve, 0))122 }123 }124}125126// Or use scheduler.yield() (Chrome 115+)127for (let i = 0; i < items.length; i += CHUNK_SIZE) {128 processChunk(items.slice(i, i + CHUNK_SIZE))129 await scheduler.yield()130}131```132133**Common INP causes:**134- Expensive event handlers (click, keydown, input)135- Synchronous layout reads/writes (layout thrashing)136- Large DOM manipulations in single frame137- JSON.parse() on large payloads (> 50KB)138139#### CLS optimization140141```css142/* Reserve space for images */143img, video, iframe {144 width: 100%;145 height: auto;146 aspect-ratio: attr(width) / attr(height);147}148149/* Reserve space for dynamic content */150.ad-container {151 min-height: 250px;152}153154/* Prevent layout shift from web fonts */155body {156 font-display: swap;157}158159/* Fixed size for injected elements */160.cookie-banner {161 min-height: 80px;162}163```164165### Step 3: Profile backend166167```bash168# Node.js CPU profile169node --cpu-prof --cpu-prof-interval=1 server.js170# Analyze: clinic doctor -- node server.js171172# Node.js heap snapshot173node --heapsnapshot server.js174# Load in Chrome DevTools > Memory tab175176# Python profiling177python -m cProfile -o output.prof server.py178# Analyze: snakeviz output.prof179```180181| Symptom | Likely cause | Fix |182|---------|-------------|-----|183| High CPU | N+1 queries, synchronous crypto | Add eager loading. Use async operations. |184| High memory | No streaming. Array growth. | Stream responses. Paginate results. |185| High latency p99 | DB lock contention | Add indexes. Use read replicas. |186| GC pauses | High allocation rate | Pool objects. Reduce allocations. Use Buffer pools. |187| Connection timeouts | Pool exhaustion | Increase pool size. Add connection queue. |188189### Step 4: Bundle analysis190191```bash192# Next.js193ANALYZE=true next build194195# Vite/Rollup196npx vite-bundle-visualizer197198# Generic199npx source-map-explorer dist/**/*.js200```201202| Target | Budget |203|--------|--------|204| Total JS | < 300KB (gzipped) |205| Total CSS | < 50KB (gzipped) |206| Total fonts | < 100KB |207| First load JS | < 100KB (gzipped) |208| Total image payload | < 500KB |209| Largest chunk | < 150KB (gzipped) |210211### Step 5: Caching strategy decision tree212213| What | Cache where | TTL |214|------|-----------|-----|215| Static assets (JS, CSS, images) | CDN + browser | 1 year (versioned filenames) |216| API responses (public, stable) | CDN + HTTP Cache-Control | 5 min to 1h (stale-while-revalidate) |217| Dynamic data (user-specific) | Redis | 30s to 5min |218| Database query results | Application memory (LRU) | 10s |219| Full pages (public) | CDN + Edge caching | 5 min |220| Auth tokens | Never cache | — |221222### Step 6: Performance budgets in CI223224```json225{226 "ci": {227 "assert": {228 "assertions": {229 "categories:performance": ["error", { "minScore": 0.9 }],230 "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],231 "interactive": ["error", { "maxNumericValue": 3800 }],232 "total-blocking-time": ["error", { "maxNumericValue": 200 }],233 "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],234 "resource-summary:script:size": ["warn", { "maxNumericValue": 300000 }]235 }236 }237 }238}239```240241```bash242npx lighthouse https://staging.example.com --output=json | \243 npx lighthouse-ci --collect --assert244```245246## Production Checklist247248- [ ] Lighthouse Performance > 90 (mobile throttled)249- [ ] LCP < 2.5s (p75 field data)250- [ ] INP < 200ms (p75 field data)251- [ ] CLS < 0.1 (p75 field data)252- [ ] Bundle: JS < 300KB, CSS < 50KB (gzipped)253- [ ] Images: WebP/AVIF, srcset, lazy loading below fold254- [ ] Fonts: self-hosted, font-display: swap, subset255- [ ] Critical CSS inlined, rest deferred256- [ ] Hero image preloaded with fetchpriority="high"257- [ ] Server response < 200ms (p95)258- [ ] N+1 queries detected and fixed259- [ ] CDN + HTTP caching configured260- [ ] CI: Lighthouse budget enforcement261- [ ] RUM (Real User Monitoring) configured262263## Anti-Patterns264265| Anti-pattern | Fix |266|-------------|-----|267| Optimizing without measuring | Always measure first. Field data > lab data. |268| Only testing on dev machine | Test on real devices with throttling (3G, 4x CPU slowdown) |269| Cache everything | Cache only what changes infrequently. Stale cache is worse than no cache. |270| Lazy loading above-fold images | Only lazy load below-fold. LCP image must load immediately. |271| Bundle splitting too aggressively | Split at route boundaries. Not by component. |272| Optimizing non-bottleneck | Use Lighthouse + RUM to find actual bottleneck. |273| Ignoring mobile | Mobile is 2-4x slower than desktop. Optimize for mobile first. |274| No performance budget | CI must fail when budget exceeded. |275276## Sources277278- web.dev — Core Web Vitals279- Lighthouse documentation280- Addy Osmani — Performance optimization patterns281- Paul Lewis — RequestAnimationFrame, compositor-only properties282- Node.js performance guide283- clinic.js — Node.js profiling284- WebPageTest — Real device testing285- Chrome UX Report — Field data for Core Web Vitals286287## Error Handling288289| Cause | Fix |290|-------|-----|291| Lighthouse audit crashes on SPA with hash routing | Use `--chrome-flags="--disable-web-security"` or serve localhost via static server, never `file://` |292| Lighthouse scores vary 5+ points between runs | Normal. Run 3-5 times, use median. Always audit Incognito with no extensions. Variance from network jitter and CPU scheduling |293| Bundle analyzer can't parse source maps — blank treemap | Verify `devtool: 'hidden-source-map'` in webpack/Vite. Regenerate build with source maps enabled. Check `sourceMapFilename` matches |294| RUM field data shows much worse LCP than lab (Lighthouse) | Lab is synthetic (fast CPU/network). Field data from real users on slow devices. Trust field data. Optimize for p75, not median |295| Performance budget assertion fails CI on every PR | Investigate which chunk exceeded budget immediately. Treat as build failure. Block merge until resolved or budget explicitly raised with justification |296| Chrome DevTools Performance tab freezes on large trace (>30s) | Record shorter traces (5-10s max). Use `--user-data-dir` with clean profile. Export HAR for sharing instead of full trace |297| `clinic doctor` fails with "Cannot find module" | Works only with Node.js < 22. For Node 22+, use `node --cpu-prof` and analyze with Chrome DevTools. clinic is community-maintained, lagging Node releases |298| WebPageTest results vary significantly by test location | Test from 2+ geographic regions. Use `medianRun` and `repeatView` in WPT API. Document which locations were tested in audit report |299300## Checklist301302- [ ] Skill loads without errors in the AI agent303- [ ] YAML frontmatter is valid (description, compatibility, audience)304- [ ] Workflow section provides clear step-by-step instructions305- [ ] Error handling section covers common failure modes306- [ ] All referenced files (references/, scripts/, assets/) exist307- [ ] Skill triggers correctly for intended use cases308- [ ] No broken links or missing resources