From react-developer
Audit a React/Next.js application or component for performance issues — bundle size, rendering, Core Web Vitals.
npx claudepluginhub hpsgd/turtlestack --plugin react-developerThis skill is limited to using the following tools:
Audit $ARGUMENTS for performance issues.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
Designs and optimizes AI agent action spaces, tool definitions, observation formats, error recovery, and context for higher task completion rates.
Designs, implements, and audits WCAG 2.2 AA accessible UIs for Web (ARIA/HTML5), iOS (SwiftUI traits), and Android (Compose semantics). Audits code for compliance gaps.
Audit $ARGUMENTS for performance issues.
Goal: Identify what is shipped to the client and whether it needs to be.
Investigation commands:
# Check bundle size (Next.js)
npx next info 2>/dev/null
ls -la .next/static/chunks/ 2>/dev/null | sort -k5 -rn | head -20
# Check for large dependencies in client bundles
grep -rn "from '" --include="*.tsx" --include="*.ts" | grep -v "node_modules\|test\|spec" | sed "s/.*from '//;s/'.*//" | sort | uniq -c | sort -rn | head -20
# Check package sizes
npx bundlephobia-cli <package-name> 2>/dev/null
Checklist:
| Issue | How to detect | Impact | Fix |
|---|---|---|---|
| Large libraries in client bundle | Import of moment, lodash (full), d3 in 'use client' components | HIGH | Use date-fns, lodash-es/cherry-pick, lighter alternatives |
| Unused imports | Imported but not referenced | LOW-MEDIUM | Remove unused imports (tree-shaking catches some) |
| Full library imports | import _ from 'lodash' vs import { map } from 'lodash-es' | HIGH | Use named imports from ESM packages |
| Polyfills not needed | Targeting modern browsers but shipping IE polyfills | MEDIUM | Check browserslist config |
| Duplicate dependencies | Multiple versions of the same package | MEDIUM | npm ls <package> to find duplication |
Rules:
Goal: Minimise the client-side JavaScript by keeping components on the server where possible.
# Find all 'use client' directives
grep -rn "'use client'" --include="*.tsx" --include="*.ts" | grep -v node_modules
# Find components that could be server components
# (no useState, useEffect, onClick, onChange, or browser APIs)
Decision matrix:
| Component uses | Server component? | Client component? |
|---|---|---|
| Only props, no state, no effects, no handlers | YES — keep on server | NO |
useState, useReducer | NO | YES — 'use client' required |
useEffect, useLayoutEffect | NO | YES |
Event handlers (onClick, onChange) | NO | YES |
Browser APIs (window, document, localStorage) | NO | YES |
| Third-party library requiring browser | NO | YES |
Data fetching (fetch, database query) | YES — prefer server | Possible but suboptimal |
| Static rendering of data | YES | NO |
Common misplacements:
| Pattern | Problem | Fix |
|---|---|---|
Entire page marked 'use client' | Ships all page code to client | Extract interactive parts into client components, keep page as server component |
Layout component marked 'use client' | All children become client components | Move interactivity to a small client wrapper |
Data-fetching component marked 'use client' | Fetches on client instead of server | Move fetch to server component, pass data as props |
Provider wrapper forces 'use client' up | Cascades client boundary upward | Wrap only the minimal tree that needs the context |
Rules:
'use client' boundary should be pushed as far DOWN the component tree as possibleuseEffect + fetch for initial data in Next.js App RouterGoal: Components only re-render when their actual inputs change.
# Find potential re-render triggers
grep -rn "useMemo\|useCallback\|React.memo\|memo(" --include="*.tsx" --include="*.ts" | grep -v node_modules
grep -rn "useContext" --include="*.tsx" | grep -v node_modules
Common re-render causes:
| Cause | Detection | Fix |
|---|---|---|
| New object/array reference in props | style={{ color: 'red' }} or items={data.filter(...)} inline in JSX | Extract to variable or useMemo |
| New function reference in props | onClick={() => handleClick(id)} on every render | useCallback or extract handler |
| Context updates re-render all consumers | Large context with frequent updates | Split context by update frequency |
| Parent re-render cascades to all children | No memoisation boundary | React.memo on expensive children |
| State stored too high | Global state for local concerns | Move state closer to where it's used |
Rules:
useMemo/useCallback/React.memo everywhere prophylactically. Only where measured re-renders cause visible jankGoal: No waterfalls, no over-fetching, no client-side fetches that could be server-side.
# Find data fetching patterns
grep -rn "useEffect.*fetch\|useQuery\|useSWR\|useInfiniteQuery\|getServerSideProps\|getStaticProps" --include="*.tsx" --include="*.ts" | grep -v node_modules
grep -rn "async function.*Page\|async function.*Layout\|export async" --include="*.tsx" | grep -v node_modules
Waterfall detection:
| Pattern | Problem | Fix |
|---|---|---|
Sequential fetches in useEffect | Fetch A, wait, then fetch B | Promise.all([fetchA(), fetchB()]) |
| Parent fetches, child fetches on mount | Child waits for parent render before starting its fetch | Parallel fetch in parent, pass data as props |
| Client-side fetch after server render | Empty shell rendered, then data filled in | Move fetch to server component |
| N+1 fetches in a list | One fetch per list item | Batch into single query, use DataLoader pattern |
Rules:
async/await — not in client components with useEffectconst [a, b] = await Promise.all([fetchA(), fetchB()])fetch, but custom calls need manual dedupunstable_cache for expensive computations, revalidate for time-based freshnessuseEffect for data that's known at request timeGoal: Images are right-sized, right-format, and lazy-loaded.
# Find image usage
grep -rn "<img\|<Image\|background-image\|backgroundImage" --include="*.tsx" --include="*.css" | grep -v node_modules
Checklist:
| Check | Pass | Fail |
|---|---|---|
Using next/image | <Image> component with width/height or fill | Raw <img> tags without optimisation |
sizes attribute | sizes="(max-width: 768px) 100vw, 50vw" | Missing sizes (serves full-width image to all viewports) |
| Priority for LCP image | priority prop on the largest above-the-fold image | LCP image lazy-loaded |
| No layout shift | width/height provided or fill with aspect-ratio container | Images pop in and shift content |
| Format | WebP/AVIF via next/image automatic optimisation | Unoptimised PNG/JPEG served directly |
| Lazy loading | Default in next/image (or explicit loading="lazy") | All images loaded eagerly |
Rules:
<img> should be a <Image> from next/image (or equivalent optimisation)priority — never lazy-loadedsizes attribute is mandatory for responsive images — without it, the browser downloads the largest sizeplaceholder="blur" with blurDataURLGoal: Only load the JavaScript needed for the current view.
# Find dynamic imports
grep -rn "dynamic(\|lazy(\|import(" --include="*.tsx" --include="*.ts" | grep -v node_modules
# Find large components that should be split
wc -l **/*.tsx 2>/dev/null | sort -rn | head -20
Candidates for dynamic import:
| Component type | Why split | Example |
|---|---|---|
| Modals/dialogs | Not visible on page load | const Modal = dynamic(() => import('./modal')) |
| Charts/graphs | Heavy library, below the fold | const Chart = dynamic(() => import('./chart'), { ssr: false }) |
| Rich text editors | Very large bundle, interactive only | dynamic(() => import('./editor'), { ssr: false }) |
| Admin panels | Rarely accessed by most users | Route-based splitting (automatic in Next.js) |
| Heavy form components | Only visible after user interaction | Dynamic import on interaction trigger |
Rules:
{ ssr: false } for components that require browser APIsdynamic(() => import('./x'), { loading: () => <Skeleton /> })# Find arbitrary Tailwind values (code smell)
grep -rn "\[.*px\]\|\[.*rem\]\|\[.*em\]\|\[.*%\]\|\[#" --include="*.tsx" | grep "className" | grep -v node_modules
Checklist:
| Issue | Impact | Fix |
|---|---|---|
Arbitrary values (w-[347px]) | Breaks design system consistency | Use standard Tailwind scale values |
| Inline styles | Not purged, not responsive, not themeable | Convert to Tailwind classes |
| Unused CSS | Larger stylesheet | Tailwind purges automatically in production — verify build config |
@apply overuse | Defeats Tailwind's utility model | Use @apply only for base element styles, not component classes |
Rank every finding by impact:
| Level | Criteria | Examples |
|---|---|---|
| HIGH | Affects Core Web Vitals (LCP, CLS, INP), visible to users, measurable | Large client bundle, waterfall data fetching, unoptimised LCP image |
| MEDIUM | Measurable performance impact but not immediately visible | Unnecessary re-renders, missing code splitting, suboptimal caching |
| LOW | Minor inefficiency, cosmetic, or developer experience only | Arbitrary Tailwind values, unused imports, missing useMemo on cheap computation |
## Performance Audit: [scope]
### Summary
- **Critical issues:** [count]
- **High impact:** [count]
- **Medium impact:** [count]
- **Low impact:** [count]
### Findings
| # | Impact | Category | Finding | Location | Recommendation |
|---|---|---|---|---|---|
| 1 | HIGH | Bundle | moment.js (300KB) in client bundle | `src/utils/date.ts:1` | Replace with date-fns (20KB) |
| 2 | HIGH | Data fetching | Waterfall: 3 sequential fetches | `src/app/page.tsx:15-25` | Use `Promise.all()` |
| 3 | MEDIUM | Server/Client | Dashboard page entirely `'use client'` | `src/app/dashboard/page.tsx:1` | Extract interactive widget to client component |
### Detailed Analysis
[For each HIGH finding: current behaviour, measured impact, specific fix with code example]
### Quick Wins
[Ordered list of fixes that are high-impact and low-effort]
### Recommendations (prioritised)
1. [Highest impact fix]
2. [Second highest]
3. [Third highest]
/react-developer:component-from-spec — when performance issues require rebuilding a component, use the component spec skill to design the replacement.