Install
openclaw skills install @heroinyan-stack/nextjs-perf-optimizerAudit Next.js apps for Core Web Vitals, bundle size, rendering strategies, image optimization, and data-fetching issues with actionable code fixes and impact...
openclaw skills install @heroinyan-stack/nextjs-perf-optimizerAudit and optimize Next.js applications for Core Web Vitals, bundle size, and runtime performance. Detects LCP/CLS/INP issues, unnecessary client-side JavaScript, missing image optimization, and database query N+1s — outputs specific code fixes with projected improvements.
Next.js performance issues are rarely about one thing — they compound: oversized bundles slow TTFB, client-side rendering hurts LCP, unoptimized images tank CLS, and heavy re-renders spike INP. This skill systematically audits each layer and provides copy-paste fixes.
Activate when the user:
Analyze each metric and identify root causes:
| Common Cause | Detection Method | Fix |
|---|---|---|
| Large hero image unoptimized | Check <img> vs next/image | Switch to next/image with priority |
| Slow server response (TTFB) | Check getServerSideProps/RSC query time | Move to SSG or add caching |
| Render-blocking JS/CSS | Check _app.tsx imports | Use dynamic() for below-fold components |
| Web font loading | Check @font-face usage | Use next/font with display: swap |
| Client-side data fetching | Check useEffect + fetch on mount | Move to Server Component or getStaticProps |
| Common Cause | Detection Method | Fix |
|---|---|---|
| Images without dimensions | Check <img> without width/height | Add width and height or use next/image |
| Dynamic content injection | Check for ads/embeds loading late | Reserve space with min-height |
| Web fonts causing reflow | Check FOIT/FOUT behavior | Use next/font or size-adjust |
| Async hydration | Check next/dynamic without ssr: false | Add loading skeleton with fixed dimensions |
| Common Cause | Detection Method | Fix |
|---|---|---|
| Heavy event handlers | Check onClick with expensive logic | Debounce/throttle, use startTransition |
| Expensive re-renders | Check component tree without memo | Add React.memo, useMemo, useCallback |
| Synchronous layout calculations | Check useLayoutEffect usage | Use useEffect instead |
| Large client-side state | Check context with frequent updates | Split context, use useSyncExternalStore |
Scan all imports and identify:
## Bundle Analysis
### Server Components vs Client Components
| File | Type | Issue | Fix |
|------|------|-------|-----|
| app/page.tsx | Server ✅ | — | — |
| components/Chart.tsx | Client ⚠️ | `"use client"` but only uses d3 | Split: data fetch in Server, render in Client |
| components/Header.tsx | Client ⚠️ | Entire header is client for theme toggle | Split: only toggle button is client |
### Heavy Dependencies
| Package | Bundle Size | Used Features | Alternative | Saving |
|---------|------------|---------------|------------|--------|
| moment.js | 67KB | format, parse | date-fns (tree-shakeable) | 60KB |
| lodash | 72KB | get, debounce | es-toolkit | 65KB |
| chart.js | 200KB | line chart only | visx or lightweight-charts | 150KB |
### Dynamic Import Opportunities
| Component | Current | Recommendation | Saving |
|-----------|---------|---------------|--------|
| Dashboard | Static import | `dynamic(() => import('./Dashboard'), { ssr: false })` | 45KB from initial |
| RichEditor | Static import | `dynamic(() => import('./RichEditor'), { loading: () => <Skeleton /> })` | 80KB from initial |
| Charts | Static import | `dynamic(() => import('./Charts'))` | 200KB from initial |
Check each page route for optimal rendering strategy:
| Route | Current | Recommendation | Reason |
|---|---|---|---|
/ (homepage) | SSR | SSG + ISR (revalidate: 3600) | Content changes hourly, not per-request |
/blog/[slug] | SSR | SSG + ISR (revalidate: 86400) | Blog posts change rarely |
/dashboard | SSR | Server Component + Client Islands | Auth check on server, interactivity on client |
/api/products | Route Handler | Edge Runtime | No DB access needed, use external API |
/pricing | SSR | SSG | Pricing rarely changes |
## Image Optimization
### Issues Found
1. 🔴 `<img>` used instead of `next/image` in `app/about/page.tsx`
- Current: `<img src="/hero.jpg" />` (unoptimized, no responsive)
- Fix: `<Image src="/hero.jpg" width={1920} height={1080} priority alt="Hero" />`
2. 🔴 No `priority` on above-fold images in `app/page.tsx`
- LCP element (hero image) loads with normal priority
- Fix: Add `priority` prop to hero `next/image`
3. 🟡 No `sizes` attribute on responsive images
- Browser downloads full-size image for all viewports
- Fix: `sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"`
4. 🟡 Using PNG for photos instead of WebP/AVIF
- PNG hero image: 850KB
- WebP equivalent: 180KB (79% smaller)
- Fix: `next/image` auto-serves WebP/AVIF with proper Accept header
Check for N+1 queries and unnecessary serialization:
## Data Fetching Issues
### N+1 Query in `app/blog/page.tsx`
- Current: fetches all posts, then fetches author for each post (12 queries)
- Fix: Use Prisma `include` or `select` to fetch with join
```ts
// Before (N+1)
const posts = await prisma.post.findMany();
const postsWithAuthors = await Promise.all(
posts.map(post => prisma.user.findUnique({ where: { id: post.authorId } }))
);
// After (1 query)
const postsWithAuthors = await prisma.post.findMany({
include: { author: { select: { name: true, avatar: true } } }
});
### Step 6: Generate Optimization Report
```markdown
# ⚡ Next.js Performance Report
## Before vs After (Projected)
| Metric | Current | Target | Method |
|--------|---------|--------|--------|
| LCP | 3.8s | 1.9s | Image priority + font optimization + SSG |
| CLS | 0.21 | 0.02 | Image dimensions + skeleton loaders |
| INP | 340ms | 150ms | Component memoization + startTransition |
| Lighthouse Score | 52 | 95+ | All above fixes combined |
| Initial JS Bundle | 380KB | 142KB | Dynamic imports + dependency swaps |
| TTFB | 850ms | 120ms | ISR + Edge runtime |
## Priority Fixes (Do in This Order)
### 🔴 Critical (Do Today)
1. Switch `<img>` to `next/image` with `priority` on LCP element
2. Move homepage from SSR to SSG + ISR
3. Replace moment.js with date-fns
4. Add `dynamic()` import for Charts component
### 🟡 Important (This Week)
5. Add `sizes` to all responsive images
6. Memoize expensive list items
7. Fix N+1 query in blog page
8. Switch /pricing to SSG
### 🔵 Nice to Have (This Sprint)
9. Switch /api/products to Edge runtime
10. Add loading.tsx for all route segments
11. Implement `next/font` for all custom fonts