Next.js Performance & Best Practices
Expert guidance for building high-performance Next.js applications with Server Components, optimal caching strategies, and production-ready patterns.
Core Principles
When working on Next.js projects, follow these fundamental rules:
1. Server Components First
**ALWAYS** start with Server Components by defaultOnly add `"use client"` when absolutely necessary for: - Interactive elements requiring hooks
- Browser APIs (localStorage, window, etc.)
- Event handlers
Fetch data in Server Components and pass as props to Client Components**NEVER** create Client Components that fetch data in `useEffect` - fetch on server instead2. Performance Optimization
Use Next.js `unstable_cache` for static and reference dataImplement parallel data fetching with `Promise.all()` instead of sequential awaitsApply dynamic imports for modals, heavy components, and rarely-used featuresMonitor bundle size constantly and optimize using code splittingAdd composite database indexes for common query patternsMeasure against performance targets: FCP < 1s, TTI < 2s, initial bundle < 100KB gzipped3. Minimalist Architecture
Keep implementation simple and avoid over-engineeringFollow YAGNI principle: don't add features until actually neededApply DRY but don't abstract prematurelyMaintain single responsibility per componentFocus on shipping complete, working features4. Full Functionality Requirements
Complete every feature before shippingImplement proper error handling with graceful fallbacksShow loading indicators for all async operationsValidate on both client AND serverEnsure accessibility with ARIA labels and keyboard navigationImplementation Patterns
Server Component Pattern (Preferred)
```javascript
// ✅ CORRECT: Server Component fetching data
export default async function Page() {
const data = await fetchData();
return <ClientForm data={data} />;
}
// ❌ INCORRECT: Client Component fetching data
"use client";
export default function Page() {
const [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData);
}, []);
return <Form data={data} />;
}
```
Parallel Data Fetching
```javascript
// ✅ CORRECT: Parallel fetching
const [data1, data2, data3] = await Promise.all([
fetchData1(),
fetchData2(),
fetchData3(),
]);
// ❌ INCORRECT: Sequential fetching (creates waterfall)
const data1 = await fetchData1();
const data2 = await fetchData2();
const data3 = await fetchData3();
```
Dynamic Imports for Code Splitting
```javascript
// ✅ CORRECT: Lazy load heavy components
const HeavyModal = dynamic(() => import('./HeavyModal'), {
ssr: false,
loading: () => null,
});
```
Caching Strategy
```javascript
// Reference data (static, rarely changes)
export const getReferenceData = unstable_cache(
async () => fetchFromDB(),
['reference-data'],
{ revalidate: 3600, tags: ['reference-data'] }
);
// User-specific data (dynamic, always fresh)
export async function getUserData(userId) {
return await fetchUserData(userId);
}
```
File Organization
Naming Conventions
**Files**: `kebab-case.js` (e.g., `new-invoice-form.js`)**Components**: `PascalCase` (e.g., `NewInvoiceForm`)**Utilities**: `camelCase` (e.g., `formatCurrency`)**Constants**: `UPPER_SNAKE_CASE` (e.g., `MAX_ITEMS`)Directory Structure
```
app/
├── (route-groups)/ # Route groups for layouts
├── [dynamic]/ # Dynamic routes
├── api/ # API routes
├── components/ # Reusable components
│ ├── ui/ # Basic UI components
│ └── ... # Feature components
└── lib/ # Utilities and services
└── services/ # Data fetching services
```
Security & Data Protection
Multi-tenancy Rules
**ALWAYS** scope database queries by `user_id`**NEVER** trust client-side user_id values**VALIDATE** user ownership on server side for every operationUse pattern: `WHERE user_id = $1 AND ...`Authentication & Authorization
Verify authentication server-side onlyUse HTTP-only cookies for session managementHash passwords with bcrypt (minimum 10 rounds)Expire tokens and validate on serverData Validation
Use Zod schemas for validationValidate on both client AND serverSanitize all user inputEscape output to prevent XSS attacksError Handling & Loading States
Proper Error Handling
```javascript
// ✅ CORRECT: Comprehensive error handling
try {
const data = await fetchData();
return <Success data={data} />;
} catch (error) {
console.error('Error:', error);
return <Error message="Something went wrong" />;
}
```
Loading States with Suspense
```javascript
// In parent layout or page
<Suspense fallback={<Loading />}>
<AsyncPage />
</Suspense>
```
Database Optimization
Add composite indexes for common query patternsUse partial indexes with `WHERE` clauses when applicableOptimize for actual query patterns, not theoretical onesRun `ANALYZE` after adding indexes to update statisticsBundle Optimization
Use `@next/bundle-analyzer` regularly to monitor bundle sizeSplit large components into smaller chunksLazy load routes, modals, and heavy featuresUse ES modules and avoid default exports for utilities (better tree-shaking)Component Design Principles
Keep components small and focused on single responsibilityPrefer props over context when possibleCompose small components into larger onesDesign for reusability without over-generalizationCode Quality Standards
Documentation
Comment the **WHY**, not the **WHAT**Document complex business logicUse TODO comments for future improvementsAvoid commenting obvious codeEnvironment Variables
Document all environment variables in READMEValidate required env vars on application startupNever commit secrets to version controlType environment variables using TypeScript or JSDocPre-Implementation Checklist
Before implementing any new feature, verify:
[ ] Is this a Server Component? (should default to yes)[ ] Can data be fetched in parallel?[ ] Is caching appropriate for this data?[ ] Are database indexes optimized?[ ] Is error handling implemented?[ ] Are loading states shown?[ ] Is validation on both client and server?[ ] Is the component accessible?[ ] Is the code documented?[ ] Is the bundle size acceptable?Performance Success Metrics
Target these benchmarks for production applications:
**FCP** (First Contentful Paint): < 1s**TTI** (Time to Interactive): < 2s**Bundle**: Initial bundle < 100KB (gzipped)**LCP** (Largest Contentful Paint): < 2.5s**CLS** (Cumulative Layout Shift): < 0.1Deployment Best Practices
Use static generation when possibleApply ISR (Incremental Static Regeneration) for semi-static contentUse SSR only when absolutely necessaryMonitor bundle size and build time in CI/CDKey Reminders
**Performance, simplicity, and full functionality are not optional - they are requirements.**
Default to Server Components unless you need interactivityFetch data in parallel whenever possibleCache reference data, keep user data freshValidate security on the serverHandle errors gracefullyShow loading states for async operationsKeep bundle size minimalDocument complex decisionsShip complete features only