|
| 1 | +--- |
| 2 | +description: Next.js performance optimization — caching, bundle size, and Core Web Vitals |
| 3 | +globs: ["**/app/**", "**/components/**", "**/*.tsx"] |
| 4 | +alwaysApply: false |
| 5 | +--- |
| 6 | + |
| 7 | +# Performance & Caching |
| 8 | + |
| 9 | +## Data Fetching Rules |
| 10 | +1. Use parallel fetching with `Promise.all()` when data is independent: |
| 11 | +```tsx |
| 12 | +// ✅ CORRECT: Parallel — both requests fire simultaneously |
| 13 | +const [user, posts] = await Promise.all([ |
| 14 | + getUser(userId), |
| 15 | + getPosts(userId), |
| 16 | +]) |
| 17 | + |
| 18 | +// ❌ WRONG: Sequential — second request waits for first to finish |
| 19 | +const user = await getUser(userId) |
| 20 | +const posts = await getPosts(userId) |
| 21 | +``` |
| 22 | + |
| 23 | +2. NEVER fetch in loops (N+1 problem): |
| 24 | +```tsx |
| 25 | +// ❌ N+1: Fires 100 queries |
| 26 | +for (const id of userIds) { |
| 27 | + const user = await supabase.from('profiles').select().eq('id', id).single() |
| 28 | +} |
| 29 | + |
| 30 | +// ✅ Batch: Fires 1 query |
| 31 | +const { data } = await supabase.from('profiles').select().in('id', userIds) |
| 32 | +``` |
| 33 | + |
| 34 | +3. Use `unstable_cache()` for expensive server-side computations: |
| 35 | +```tsx |
| 36 | +import { unstable_cache } from 'next/cache' |
| 37 | + |
| 38 | +const getCachedPosts = unstable_cache( |
| 39 | + async (userId: string) => { |
| 40 | + return supabase.from('posts').select().eq('user_id', userId) |
| 41 | + }, |
| 42 | + ['user-posts'], |
| 43 | + { revalidate: 60, tags: ['posts'] } |
| 44 | +) |
| 45 | +``` |
| 46 | + |
| 47 | +## Bundle Size Rules |
| 48 | +- Dynamic import heavy libraries: `const { Chart } = await import('chart.js')` |
| 49 | +- Use `next/dynamic` for client components that don't need SSR: |
| 50 | +```tsx |
| 51 | +import dynamic from 'next/dynamic' |
| 52 | +const RichTextEditor = dynamic(() => import('@/components/editor'), { |
| 53 | + ssr: false, |
| 54 | + loading: () => <div className="animate-pulse h-40 bg-muted rounded" />, |
| 55 | +}) |
| 56 | +``` |
| 57 | +- NEVER import entire icon libraries: `import { Search } from 'lucide-react'` not `import * as icons` |
| 58 | + |
| 59 | +## Image Optimization |
| 60 | +- ALWAYS use `next/image` with explicit `width` and `height` |
| 61 | +- Set `priority={true}` on above-the-fold hero images for LCP |
| 62 | +- Use `placeholder="blur"` with `blurDataURL` for smooth loading |
| 63 | +- Serve modern formats: Next.js automatically converts to WebP/AVIF |
| 64 | + |
| 65 | +## Core Web Vitals Targets |
| 66 | +- LCP (Largest Contentful Paint): < 2.5s |
| 67 | +- FID (First Input Delay): < 100ms |
| 68 | +- CLS (Cumulative Layout Shift): < 0.1 |
0 commit comments