Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Optimize Next.js 15 applications for performance, Core Web Vitals, and production best practices using App Router patterns
.claude/skills/aiskillstore-nextjs-optimization/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-14 | ✗→✓ | ▲ Improved | 210% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 181% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 223% | 0% |
| case-06 | ✓→✓ | = Same ✓ | 180% | 0% |
| case-07 | ✓→✓ | = Same ✓ | 231% | 0% |
Optimize Next.js applications to achieve:
Auto-invoke when:
next in dependencies)Tools: Read, Grep
bash # Read package.json # Check for "next": "^15.0.0" or higher
app/ directorylayout.tsx, page.tsx filespages/ directoryGoal: Choose optimal rendering for each page/component
Tools: Read, Grep, Edit
When to use:
Pattern:
typescript// app/dashboard/page.tsx export default async function DashboardPage() { const data = await fetchData(); // Runs on server return <Dashboard data={data} />; }
Check for violations:
bash# Search for "use client" in components that don't need it grep -r "use client" app/ | grep -v "onClick\|useState\|useEffect"
When to use:
Pattern:
typescript// app/components/Counter.tsx 'use client'; export default function Counter() { const [count, setCount] = useState(0); return <button onClick={() => setCount(count + 1)}>{count}</button>; }
Optimization: Keep client components small and leaf nodes
When to use:
Pattern:
typescriptexport const revalidate = 3600; // Revalidate every hour export default async function BlogPost({ params }) { const post = await getPost(params.slug); return <Article post={post} />; }
When to use:
Pattern:
typescriptexport const revalidate = 60; // Revalidate every minute export async function generateStaticParams() { const products = await getProducts(); return products.map((p) => ({ slug: p.slug })); }
Goal: Optimize images for performance and Core Web Vitals
Tools: Grep, Read, Edit
Find unoptimized images:
bashgrep -rn "<img " app/ src/
Replace with:
typescriptimport Image from 'next/image'; <Image src="/hero.jpg" alt="Hero image" width={1200} height={600} priority // For above-the-fold images placeholder="blur" // Optional blur-up effect />
Read next.config.js:
javascriptmodule.exports = { images: { remotePatterns: [ { protocol: 'https', hostname: 'example.com', }, ], formats: ['image/avif', 'image/webp'], // Modern formats }, };
Goal: Eliminate FOUT/FOIT and improve font loading
Tools: Read, Edit
Pattern:
typescript// app/layout.tsx import { Inter } from 'next/font/google'; const inter = Inter({ subsets: ['latin'], display: 'swap', // Prevent FOIT variable: '--font-inter', }); export default function RootLayout({ children }) { return ( <html lang="en" className={inter.variable}> <body>{children}</body> </html> ); }
typescriptimport localFont from 'next/font/local'; const customFont = localFont({ src: './fonts/CustomFont.woff2', display: 'swap', variable: '--font-custom', });
Goal: Minimize waterfalls and optimize cache
Tools: Read, Grep, Edit
Anti-pattern (Sequential):
typescriptconst user = await getUser(); const posts = await getPosts(user.id); // Waits for user
Optimized (Parallel):
typescriptconst [user, posts] = await Promise.all([ getUser(), getPosts(), ]);
Pattern:
typescriptimport { Suspense } from 'react'; export default function Page() { return ( <> <Header /> <Suspense fallback={<Skeleton />}> <SlowComponent /> </Suspense> <Footer /> </> ); }
typescript// Aggressive caching fetch('https://api.example.com/data', { next: { revalidate: 3600 }, // Cache for 1 hour }); // No caching fetch('https://api.example.com/data', { cache: 'no-store', // Always fresh }); // Opt out of caching export const dynamic = 'force-dynamic';
Goal: Reduce JavaScript bundle size
Tools: Bash, Read, Edit
bash# Add to package.json scripts npm run build npx @next/bundle-analyzer
Find large components:
bashfind app -name "*.tsx" -exec wc -l {} \; | sort -rn | head -10
Split with dynamic imports:
typescriptimport dynamic from 'next/dynamic'; const HeavyComponent = dynamic(() => import('./HeavyComponent'), { loading: () => <Skeleton />, ssr: false, // Skip SSR if not needed });
Check for barrel exports:
bashgrep -rn "export \* from" app/
Replace with specific imports:
typescript// Anti-pattern import { Button, Card, Modal } from '@/components'; // Optimized import { Button } from '@/components/Button';
Goal: Perfect SEO and social sharing
Tools: Read, Edit
typescript// app/layout.tsx export const metadata = { title: { default: 'My App', template: '%s | My App', }, description: 'Description for SEO', openGraph: { title: 'My App', description: 'Description for social sharing', images: ['/og-image.jpg'], }, twitter: { card: 'summary_large_image', }, };
typescriptexport async function generateMetadata({ params }) { const post = await getPost(params.slug); return { title: post.title, description: post.excerpt, openGraph: { images: [post.ogImage], }, }; }
Goal: Optimize next.config.js for production
Tools: Read, Edit
javascript// next.config.js module.exports = { reactStrictMode: true, poweredByHeader: false, // Security compress: true, // Gzip compression // Compiler optimizations compiler: { removeConsole: process.env.NODE_ENV === 'production', }, // Image optimization images: { formats: ['image/avif', 'image/webp'], deviceSizes: [640, 750, 828, 1080, 1200, 1920], }, // React Compiler (Next.js 15) experimental: { reactCompiler: true, }, };
json// package.json { "scripts": { "dev": "next dev --turbo" } }
Goal: Achieve perfect Lighthouse scores
Tools: Bash, Grep, Edit
Optimize:
priority on hero imagestypescript// Preload critical resources <link rel="preload" href="/hero.jpg" as="image" />
Optimize:
typescriptimport Script from 'next/script'; <Script src="https://analytics.example.com" strategy="lazyOnload" // Load after page interactive />
Optimize:
font-display: swapcss/* Reserve space for ads/banners */ .ad-container { min-height: 250px; }
Goal: Maximize cache hits and minimize server load
Tools: Read, Edit
typescript// app/dashboard/page.tsx export const revalidate = 3600; // ISR every hour export const dynamic = 'auto'; // Automatic optimization export const fetchCache = 'force-cache'; // Aggressive caching
typescript// Deduplicated and cached automatically const user = await fetch('https://api.example.com/user'); // Revalidate tag-based export const revalidate = 60; export const tags = ['user', 'profile'];
Goal: Track performance over time
Tools: Bash, WebSearch
typescript// app/layout.tsx import { SpeedInsights } from '@vercel/speed-insights/next'; export default function RootLayout({ children }) { return ( <html> <body> {children} <SpeedInsights /> </body> </html> ); }
bash# Run Lighthouse npx lighthouse http://localhost:3000 --view # Check Core Web Vitals npm run build npm run start npx lighthouse http://localhost:3000 --only-categories=performance
Run through this checklist:
next/image with proper dimensionsnext/font with display: swap<Suspense>next.config.js has production optimizationsmarkdown# Next.js Optimization Report ## Current Status - **Next.js Version**: 15.0.3 - **Rendering**: App Router - **React Version**: 19.0.0 ## Issues Found ### 🔴 Critical (3) 1. **Unoptimized Images**: 12 `<img>` tags found - Files: `app/page.tsx`, `app/about/page.tsx` - Fix: Replace with `next/image` 2. **Large Client Bundle**: 342 KB (target: < 200 KB) - Cause: Heavy chart library loaded synchronously - Fix: Use dynamic import for `Chart` component 3. **Missing Font Optimization**: Using Google Fonts via <link> - Fix: Migrate to `next/font/google` ### 🟡 Warnings (2) 1. **Sequential Data Fetching**: Waterfall detected in `app/dashboard/page.tsx` 2. **No Metadata**: Missing OpenGraph tags on 5 pages ## Optimizations Applied ✅ Enabled React Compiler in next.config.js ✅ Added image optimization config ✅ Configured proper cache headers ✅ Added Suspense boundaries to slow routes ## Performance Impact (Estimated) - **Load Time**: 3.2s → 1.8s (-44%) - **Bundle Size**: 342 KB → 198 KB (-42%) - **LCP**: 3.1s → 2.3s (✅ Good) - **FID**: 85ms → 45ms (✅ Good) - **CLS**: 0.15 → 0.05 (✅ Good) ## Next Steps 1. Replace 12 `<img>` tags with `next/image` 2. Split Chart component with dynamic import 3. Add metadata to 5 pages 4. Run Lighthouse to verify improvements
codebase-analysis - Detect Next.js project and versionquality-gates - Run build and verify no regressionsreact-patterns - Ensure React 19 best practicestesting-strategy - Add performance tests❌ Using 'use client' at the top level ❌ Not specifying image dimensions ❌ Synchronous data fetching ❌ Loading entire component libraries ❌ No Suspense boundaries ❌ Ignoring Core Web Vitals
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 78,261 | 130,749 | +67% | 1 | 1 | 0% | 5,626 | 4,248 | -24% | 0 | 0 | — |
case-02 | fail→fail | 58,608 | 58,501 | -0% | 1 | 1 | 0% | 5,009 | 4,083 | -18% | 0 | 0 | — |
case-03 | fail→fail | 63,706 | 25,320 | -60% | 1 | 1 | 0% | 4,381 | 4,198 | -4% | 0 | 0 | — |
case-04 | pass→pass | 13,439 | 25,684 | +91% | 1 | 1 | 0% | 2,379 | 6,691 | +181% | 0 | 0 | — |
case-05 | pass→pass | 13,511 | 14,567 | +8% | 1 | 1 | 0% | 1,644 | 5,311 | +223% | 0 | 0 | — |
case-06 | pass→pass | 16,550 | 12,329 | -26% | 1 | 1 | 0% | 2,114 | 5,924 | +180% | 0 | 0 | — |
case-07 | pass→pass | 17,245 | 10,356 | -40% | 1 | 1 | 0% | 1,767 | 5,840 | +231% | 0 | 0 | — |
case-08 | fail→fail | 16,813 | 17,005 | +1% | 1 | 1 | 0% | 2,677 | 5,942 | +122% | 0 | 0 | — |
case-09 | pass→pass | 13,784 | 18,710 | +36% | 1 | 1 | 0% | 2,172 | 6,211 | +186% | 0 | 0 | — |
case-10 | pass→pass | 18,826 | 16,109 | -14% | 1 | 1 | 0% | 2,577 | 5,461 | +112% | 0 | 0 | — |
case-11 | pass→pass | 39,188 | 23,812 | -39% | 1 | 1 | 0% | 3,157 | 6,813 | +116% | 0 | 0 | — |
case-12 | pass→pass | 16,709 | 17,488 | +5% | 1 | 1 | 0% | 2,867 | 5,927 | +107% | 0 | 0 | — |
case-13 | pass→pass | 18,133 | 14,429 | -20% | 1 | 1 | 0% | 1,880 | 5,299 | +182% | 0 | 0 | — |
case-14 | fail→pass | 19,077 | 18,078 | -5% | 1 | 1 | 0% | 1,933 | 5,985 | +210% | 0 | 0 | — |
case-15 | pass→pass | 17,952 | 23,015 | +28% | 1 | 1 | 0% | 2,816 | 7,030 | +150% | 0 | 0 | — |
case-16 | pass→pass | 22,431 | 8,013 | -64% | 1 | 1 | 0% | 1,698 | 5,193 | +206% | 0 | 0 | — |
case-17 | pass→pass | 31,384 | 7,535 | -76% | 1 | 1 | 0% | 1,539 | 5,196 | +238% | 0 | 0 | — |
case-18 | pass→pass | 22,709 | 15,492 | -32% | 1 | 1 | 0% | 3,291 | 7,310 | +122% | 0 | 0 | — |
case-19 | pass→pass | 13,277 | 16,242 | +22% | 1 | 1 | 0% | 2,942 | 6,612 | +125% | 0 | 0 | — |
case-20 | pass→pass | 12,243 | 11,144 | -9% | 1 | 1 | 0% | 1,995 | 6,149 | +208% | 0 | 0 | — |
case-21 | pass→pass | 15,309 | 7,401 | -52% | 1 | 1 | 0% | 1,666 | 5,034 | +202% | 0 | 0 | — |
case-22 | pass→pass | 9,061 | 11,751 | +30% | 1 | 1 | 0% | 1,851 | 5,234 | +183% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 19 counted toward the lift figure. The other 3 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +5 percentage points is the difference between those two pass rates over the 19 comparable cases. 3 cases got worse with the skill loaded, and they are included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.