Performance Audit Skill
Degree of freedom: MIXED — what to optimize [HIGH freedom];
measure-first Sentry/vitals and EXPLAIN [LOW freedom — run exactly].
Audit-and-fix exception. Measure, then optimize. JS payload →
audit-bundle-size. Instant navigations →enhance-web-instant-nav. Breaking point →test-load. Timeouts/retries →audit-resilience.
How to reason
- Observe — quote LCP/INP/CLS, p95, or
EXPLAIN/ N+1 loop (file:line) - Interpret — is the user waiting on bytes, a query, or main-thread work?
- Classify — vital-miss / N+1 / missing-index / bundle / image / re-render
- Severity — poor LCP/INP or p95 >> target on a live path = Critical
Worked example
Observe: Sentry p95
/users= 2.3s; handlerfindAllthen per-userOrder.findByUserId(app/api/users/route.ts:31). Interpret: N+1; TTFB/LCP suffer on that page. Classify: N+1. Severity: Critical (p95 far over 200ms). Finding:/users| N+1 | eager-load orders | then re-measure
Step 0: Gather Performance Data [LOW freedom — run exactly]
Before optimizing, measure. Collect data from multiple sources.
Production Data (Sentry)
If Sentry performance monitoring is enabled, fetch real production metrics:
sentry:search_events
{
"organizationSlug": "<ORG_SLUG>",
"projectSlug": "<PROJECT_SLUG>",
"regionUrl": "<REGION_URL>",
"query": "slowest transactions by p95 duration in last 7 days",
"limit": 20
}
sentry:search_events
{
"organizationSlug": "<ORG_SLUG>",
"projectSlug": "<PROJECT_SLUG>",
"regionUrl": "<REGION_URL>",
"query": "web vitals LCP INP CLS performance scores in last 7 days",
"limit": 20
}
Check for performance-related issues:
sentry:search_issues
{
"organizationSlug": "<ORG_SLUG>",
"projectSlugOrId": "<PROJECT_SLUG>",
"regionUrl": "<REGION_URL>",
"query": "performance issues slow queries N+1 in last 30 days",
"limit": 20
}
Research Current Benchmarks
Fetch current performance targets:
firecrawl:firecrawl_search
{
"query": "web vitals thresholds good score <current year>",
"limit": 3,
"sources": [{ "type": "web" }]
}
Field Data (source of truth) [LOW freedom — run exactly]
Lab (Lighthouse) is for diagnosis; field decides pass/fail.
- CrUX / PageSpeed Insights field tab for the origin and top 5 URLs by traffic — record p75 LCP / INP / CLS (mobile first).
web-vitals/attributionin the app (not the base build): it names the LCP element, the INP interaction target, and the CLS-shifting node.- Lighthouse CI with a
budget.json(seereferences/loading-priority-2026.md§Budgets). Fail the PR on regression, not on absolute score.
Record all three in the report's "Production Metrics" block before touching code.
Verification protocol [LOW freedom — run exactly]
# Field (source of truth): CrUX / PSI field tab → p75 LCP, INP, CLS
# Lab, repeatable, budgeted:
npx @lhci/cli autorun --config=lighthouserc.json
# Early Hints present?
curl -sv https://example.com/ 2>&1 | grep -E "< HTTP/.* 103"
# Speculation: DevTools → Application → Speculative loads
# bfcache: DevTools → Application → Back/forward cache → Test
# LCP element: web-vitals/attribution → onLCP(({attribution}) => attribution.element)
Performance Targets
Core Web Vitals
| Metric | Good | Needs Work | Poor |
|---|---|---|---|
| LCP (Largest Contentful Paint) | <2.5s | 2.5-4s | >4s |
| INP (Interaction to Next Paint) | <200ms | 200-500ms | >500ms |
| CLS (Cumulative Layout Shift) | <0.1 | 0.1-0.25 | >0.25 |
Other Key Metrics
- TTFB: <800ms field p75 (Google threshold); aim <200ms at the edge
- FCP: <1.8s
- Long tasks: none >50ms on the critical interaction path (INP driver)
- LCP resource load delay: <100ms (gap between TTFB and the LCP image request starting — usually "image not discoverable / not preloaded")
- bfcache: eligible on every content page (DevTools → Application → Back/forward cache)
Frontend Performance Audit [HIGH freedom]
Bundle Size Analysis
npm run build -- --analyze # framework-specific
npx source-map-explorer 'dist/**/*.js'
Checklist:
- Total bundle <200KB gzipped
- No single chunk >100KB gzipped
- Tree shaking working (no unused exports in bundle)
- Heavy libraries lazy-loaded (charts, editors, maps)
- No moment.js (use date-fns or dayjs)
- No lodash full import (use lodash-es or specific imports)
Code Splitting
- Route-based splitting (each page loads its own chunk)
- Component-level splitting for heavy components (
lazy()/dynamic()) - Below-fold content deferred
Image Optimization
- WebP/AVIF format used (not PNG/JPEG for photos)
- Images sized appropriately (not serving 4K to mobile)
-
loading="lazy"on below-fold images - Responsive
srcSetfor different screen sizes - Image CDN used (Cloudinary, imgix, Vercel Image Optimization)
Loading Priority & Speculation [HIGH freedom on which to apply, LOW freedom on the LCP rules]
Deep syntax: references/loading-priority-2026.md. Instant-nav implementation → enhance-web-instant-nav.
LCP element (find it first with web-vitals/attribution or DevTools → Performance → LCP)
- LCP image is not
loading="lazy"— lazy on the LCP image is an automatic Critical finding - LCP image has
fetchpriority="high"(Next.js:<Image priority />, which also injects the preload) - LCP image is discoverable from HTML — not a CSS
background-image, not injected by client JS, not behind a hydration boundary -
<link rel="preload" as="image" imagesrcset imagesizes>when the LCP image is responsive and not already preloaded by the framework - Non-critical images:
loading="lazy" decoding="async";<picture>AVIF → WebP → JPEG fallback
Fonts
-
font-display: swap(oroptionalfor non-brand text); fallback metrics set (size-adjust,ascent-override) so swap causes no CLS —next/fontdoes this automatically, verify it's actually used - Preload only the above-fold weight(s); subset to used unicode ranges; self-host
Early Hints (103)
- Origin/CDN sends
103 Early Hintswithpreconnectfor the image/font CDN andpreloadfor the LCP image + critical CSS. Verify:curl -sv https://site/ 2>&1 | grep -i "< HTTP/.* 103" - Node:
res.writeEarlyHints({ link: [...] }); otherwise configure at the CDN
Speculation Rules (next-navigation speed)
-
<script type="speculationrules">present withprefetchdocument rules atmoderateeagerness for in-site links -
prerenderonly for high-probability next pages (pagination, "next article", product detail); eagernessmoderate/conservative - Excluded from speculation: logout, cart/checkout, auth, anything with side effects on GET, URLs with
?unlessexpects_no_vary_searchis set - Server ignores side-effects when
Sec-Purpose: prefetch;prerenderis present; analytics/ads guarded bydocument.prerendering+prerenderingchange - Full implementation →
enhance-web-instant-nav
bfcache
- No
unloadlisteners (usepagehide); noCache-Control: no-storeon HTML; no open WebSocket / IndexedDB transaction atpagehide - Verified green in DevTools → Application → Back/forward cache → "Test back/forward cache"
Render strategy (Next.js 16 / React 19)
- Server Components by default;
'use client'only at interactive leaves - Slow data wrapped in
<Suspense>so the shell streams; static shell + dynamic holes (cacheComponents/'use cache') instead of blocking the whole route - React Compiler enabled (
reactCompiler: true) before any manualmemo/useMemo/useCallback— manual memo only where Profiler proves the Compiler missed it
INP (responsiveness)
- No task >50ms on tap/click/keypress paths; break work with
scheduler.yield()(fallbacksetTimeout(0)) orstartTransition - No synchronous layout reads (
offsetHeight,getBoundingClientRect) inside input handlers -
content-visibility: auto+contain-intrinsic-sizeon long off-screen sections - Third-party scripts:
async/defer, Next<Script strategy="lazyOnload">(or"worker"via Partytown where safe), consent-gated, facades for embeds (lite-youtube, static map image → iframe on click)
CLS
- Every
<img>,<video>,<iframe>, ad slot haswidth/heightoraspect-ratio - Late-arriving UI (banners, toasts, cookie bars) reserves space or overlays — never pushes content
React Performance (if applicable)
- React Compiler on; manual memo only with Profiler evidence (see Loading Priority & Speculation)
- No unnecessary re-renders (React DevTools Profiler)
- Long lists virtualized (react-window, @tanstack/react-virtual)
- No inline object/array creation in JSX props
- Context providers scoped narrowly (not wrapping entire app for local state)
CSS Performance
- No layout thrashing (reads before writes)
- Animations use
transform/opacity(GPU-accelerated) - No
@importchains (bundled instead) - Critical CSS inlined for above-fold content
- Fonts subset and preloaded (
<link rel="preload">)
Backend Performance Audit [HIGH freedom]
Database Queries
N+1 Detection:
// BAD: N+1
const users = await User.findAll();
for (const user of users) {
const orders = await Order.findByUserId(user.id);
}
// GOOD: eager loading
const users = await User.findAll({ include: [Order] });
Checklist:
- N+1 queries eliminated (eager loading, joins, batch loading)
- Indexes on frequently queried columns (WHERE, JOIN, ORDER BY)
- SELECT only needed columns (no
SELECT *) - Pagination on all list endpoints
- Connection pooling configured
- Slow query logging enabled
Index verification: [LOW freedom — run exactly]
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = '123';
API Response Optimization
- Response compression enabled (gzip/brotli)
- Appropriate cache headers (
Cache-Control,ETag) - No over-fetching (return only what the client needs)
- Pagination for list endpoints
- Response time <200ms for p95
Caching Strategy
| Layer | Tool | TTL | Use For |
|---|---|---|---|
| Browser | Cache-Control headers | Varies | Static assets, API responses |
| CDN | Vercel/CloudFront/Cloudflare | 1h-1d | Static pages, images |
| API | Redis/Memcached | 5m-1h | Expensive queries, computed data |
| ORM | Query cache | 1m-5m | Repeated identical queries |
Network Optimization
- CDN for static assets
- HTTP/2 or HTTP/3 enabled
- Preconnect to critical origins (
<link rel="preconnect">) - DNS prefetch for third-party domains
- API calls batched where possible (GraphQL, DataLoader)
Research-Driven Optimization [HIGH freedom]
For specific performance bottlenecks, research current solutions:
firecrawl:firecrawl_search
{
"query": "<framework> <specific bottleneck> performance optimization <current year>",
"limit": 5,
"sources": [{ "type": "web" }]
}
Then deep-read the best result:
firecrawl:firecrawl_scrape
{
"url": "<best-result-url>",
"formats": ["markdown"],
"onlyMainContent": true
}
Check official framework docs via Context7:
context7:resolve-library-id
{
"libraryName": "<framework>",
"query": "performance optimization"
}
Quick Wins (Highest Impact, Lowest Effort)
| Issue | Solution | Impact |
|---|---|---|
| LCP image lazy / not prioritized | fetchpriority="high" + preload; remove loading="lazy" from LCP |
High (−300 to −800 ms LCP) |
| Fast first page, slow second | Speculation Rules prefetch (moderate) |
High (next-nav TTFB ≈ 0) |
| Manual memo everywhere | Enable React Compiler | High (INP; delete redundant memo) |
| Large bundle | Code split routes, lazy load heavy libs | High |
| Slow images | WebP + lazy load + responsive | High |
| No caching | Add Cache-Control headers | High |
| N+1 queries | Eager load / batch | High |
| Missing indexes | Add database indexes | High |
| Unoptimized fonts | Subset + preload + font-display:swap | Medium |
| No compression | Enable gzip/brotli | Medium |
| Expensive re-renders | React.memo + useMemo | Medium |
| Layout shifts | Set explicit width/height on images/embeds | Medium |
Self-critique before applying fixes [LOW freedom — do not skip]
- Evidenced — a measured vital, p95, or
EXPLAIN, not "this looks slow" - Reproducible — same path still exceeds the target table
- Severity justified — Critical = poor vital or live-path p95
- Right owner — JS payload →
audit-bundle-size; breaking point →test-load; timeouts →audit-resilience - No-false-safety — no
memo/useMemowithout a measured re-render; measure again after the fix - LCP named — prove which element is LCP before recommending image fixes (no attribution data = guess, not audit)
Output: Performance Audit Report
## Performance Audit: [Project Name]
### Production Metrics (from Sentry)
- LCP: [value] — [good/needs work/poor]
- INP: [value] — [good/needs work/poor]
- CLS: [value] — [good/needs work/poor]
- Slowest transactions: [list with p95 times]
### Critical Issues (fix immediately)
| # | Area | Issue | Impact | Fix |
|---|------|-------|--------|-----|
| 1 | DB | N+1 on /users endpoint | p95 = 2.3s | Eager load orders relation |
### Optimizations (prioritized)
| # | Area | Issue | Impact | Effort | Fix |
|---|------|-------|--------|--------|-----|
| 1 | FE | Bundle 450KB gzipped | High | Low | Code split routes |
### Already Optimized
- [list of performance areas that are well-implemented]
### Monitoring Recommendations
- [what to add to track performance over time]
### Research Sources
- [URL] — [what optimization pattern it provided]