
Accelint Nextjs Best Practices
- 314 installs
- 21 repo stars
- Updated August 4, 2026
- gohypergiant/agent-skills
accelint-nextjs-best-practices is a frontend agent skill that applies Next.js App Router performance, security, and data-fetching patterns for developers writing Server Components, Server Actions, and API routes.
About
accelint-nextjs-best-practices is a Next.js optimization skill at version 1.1 in gohypergiant/agent-skills, designed for AI agents working with the App Router and Pages Router. The SKILL.md and AGENTS.md catalog incorrect versus correct examples for waterfall elimination via early parallel fetches and Promise.allSettled, request deduplication with React.cache(), RSC serialization minimization, authenticated Server Actions, strategic Suspense boundaries, non-blocking after() operations, and Server versus Client Component boundary decisions. Agents auto-activate when writing Server Components, implementing data fetching, reviewing Next.js code for security or performance, or debugging RSC serialization duplication. Install with npx skills add gohypergiant/agent-skills --skill accelint-nextjs-best-practices. Developers reach for accelint-nextjs-best-practices when Next.js 13+ pages feel slow, Server Actions lack auth checks, or HTML payloads bloat from over-serialized props. The skill complements accelint-react-best-practices for hook-level patterns and focuses on Next-specific APIs like headers, cookies, and streaming rather than generic React guidance alone.
- accelint-nextjs-best-practices
- Development
Accelint Nextjs Best Practices by the numbers
- 314 all-time installs (skills.sh)
- Ranked #1,300 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/gohypergiant/agent-skills --skill accelint-nextjs-best-practicesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 314 |
|---|---|
| repo stars | ★ 21 |
| Last updated | August 4, 2026 |
| Repository | gohypergiant/agent-skills ↗ |
How do you optimize Next.js App Router performance?
For development and infrastructure management.
Who is it for?
Full-stack developers on Next.js 13+ App Router who need agent-guided fixes for waterfalls, RSC bloat, and unauthenticated Server Actions.
Skip if: Non-Next React SPAs, Vue/Nuxt projects, or teams needing only generic React hook optimization without Next.js-specific APIs.
When should I use this skill?
The user writes Server Components, implements Server Actions, debugs Next.js waterfalls, or reviews App Router code for performance and security.
What you get
Refactored Server Components, secured Server Actions, parallel fetch patterns, Suspense boundaries, and reduced RSC serialization payloads.
- Optimized data-fetch patterns
- Secured Server Action code
- Suspense boundary placements
By the numbers
- Skill metadata version 1.1 in gohypergiant/agent-skills
- Covers Next.js App Router from Next.js 13+ including Server Actions and React.cache()
Files
Next.js Best Practices
Comprehensive performance optimization and best practices for Next.js applications, designed for AI agents and LLMs working with Next.js code.
When to Activate This Skill
Use this skill when the task involves:
Writing Next.js Code
- Creating Server Components or Client Components
- Implementing Server Actions with
"use server" - Writing API route handlers
- Setting up data fetching in RSC (React Server Components)
- Implementing Suspense boundaries
- Using Next.js-specific APIs (
headers(),cookies(),after())
Refactoring Next.js Code
- Optimizing server-side data fetching
- Reducing RSC serialization overhead
- Converting sequential to parallel operations
- Restructuring component composition for better performance
- Migrating between Server and Client Components
Performance Optimization
- Eliminating server-side waterfalls
- Reducing response times in API routes and Server Actions
- Minimizing data transfer at RSC boundaries
- Implementing request deduplication with
React.cache() - Using
after()for non-blocking operations
Next.js-Specific Issues
- Authentication/authorization in Server Actions
- RSC serialization duplication problems
- Import optimization (barrel file issues)
- Server vs Client Component decision-making
- Parallel data fetching patterns
Code Review
- Reviewing Next.js code for performance anti-patterns
- Identifying security issues in Server Actions
- Checking proper Server/Client Component boundaries
- Ensuring proper authentication patterns
- Validating Suspense boundary placement
When NOT to Use This Skill
Do not activate for:
- React-specific optimizations (use
accelint-react-best-practicesskill) - Build configuration (webpack, turbopack) unless Next.js-specific
- General TypeScript/JavaScript questions (use
accelint-ts-best-practicesskill) - Deployment/hosting configuration
- Testing setup (use
accelint-ts-testingskill)
Example Trigger Phrases
This skill should activate when users say things like:
Performance Issues:
- "This Next.js API route is slow"
- "My Server Component is blocking the entire page"
- "Optimize this Server Action"
- "The page takes forever to load data"
- "There's a waterfall in my data fetching"
Security Issues:
- "Add authentication to this Server Action"
- "This Server Action needs authorization"
- "Secure this API route"
- "Validate input in this Server Action"
Debugging Issues:
- "Why is my RSC props so large?"
- "This data is being duplicated in the HTML"
- "My imports are slow in development"
- "Should this be a Server or Client Component?"
Code Review:
- "Review this Next.js code for performance issues"
- "Is this Server Action secure?"
- "Can you optimize this data fetching?"
- "Check if this component should be server or client"
Refactoring:
- "Parallelize these data fetches"
- "Reduce the serialization size"
- "Convert this to use Suspense"
- "Optimize this barrel import"
How to Use
This skill uses a progressive disclosure structure to minimize context usage:
1. Start with the Overview (AGENTS.md)
Read AGENTS.md for a concise overview of all rules with one-line summaries.
2. Load Specific Rules as Needed
When you identify a relevant optimization, load the corresponding reference file for detailed implementation guidance:
General Patterns:
- prevent-waterfall-chains.md (1.1)
- parallelize-independent-operations.md (1.2)
- strategic-suspense-boundaries.md (1.3)
Server-Side Performance:
- server-actions-security.md (2.1)
- avoid-duplicate-serialization.md (2.2)
- minimize-serialization.md (2.3)
- parallel-data-fetching.md (2.4)
- react-cache-deduplication.md (2.5)
- use-after-non-blocking.md (2.6)
Misc:
- avoid-barrel-imports.md (3.1)
- server-vs-client-component.md (3.2)
Quick References:
- quick-checklist.md
- compound-patterns.md
Automation Scripts:
- scripts/ - Helper scripts to detect anti-patterns
3. Apply the Pattern
Each reference file contains:
- ❌ Incorrect examples showing the anti-pattern
- ✅ Correct examples showing the optimal implementation
- Explanations of why the pattern matters
- Performance impact metrics
- Related patterns and references
4. Use the Report Template
When this skill is invoked for Next.js code review, use the standardized report format:
Template: `assets/output-report-template.md`
The report format provides:
- Executive Summary with impact assessment
- Severity levels (Critical, High, Medium, Low) for prioritization
- Impact analysis (performance, security, data transfer, maintainability)
- Categorization (Server Actions, RSC Serialization, Data Fetching, Component Architecture)
- Pattern references linking to detailed guidance in references/
- Summary table for tracking all issues
When to use the report template:
- Skill invoked directly via
/accelint-nextjs-best-practices <path> - User asks to "review Next.js code" or "audit Next.js app" across file(s), invoking skill implicitly
When NOT to use the report template:
- User asks to "fix this Server Action" (direct implementation)
- User asks "what's wrong with this code?" (answer the question)
- User requests specific fixes (apply fixes directly without formal report)
Examples
Example 1: Optimizing Server Action Security
Task: "Add authentication to this Server Action"
Approach: 1. Read AGENTS.md overview 2. Identify issue: Server Action needs authentication 3. Load server-actions-security.md 4. Apply authentication pattern with validation
Example 2: Eliminating Waterfalls
Task: "This page loads slowly with multiple fetches"
Approach: 1. Read AGENTS.md overview 2. Identify issue: Sequential data fetching 3. Load prevent-waterfall-chains.md and parallelize-independent-operations.md 4. Start operations immediately and use Promise.allSettled()
Example 3: Reducing Serialization
Task: "The HTML response is huge with user data"
Approach: 1. Read AGENTS.md overview 2. Identify issue: Over-serialization at RSC boundary 3. Load minimize-serialization.md 4. Pass only necessary fields, transform on client
Additional Resources
Official Next.js documentation:
Next.js Best Practices
Note:
This document is mainly for agents and LLMs to follow when maintaining, generating, or refactoring Next.js code. Humans may also find it useful, but guidance here is optimized for automation and consistency by AI-assisted workflows.
---
Abstract
Comprehensive performance optimization guide for Next.js applications, designed for AI agents and LLMs. Each rule includes one-line summaries with links to detailed examples in references/. Load reference files only when implementing a specific pattern.
Focus: Next.js App Router patterns including Server Components, Server Actions, RSC serialization, and server-side optimization.
- For React specific patterns, use the
accelint-react-best-practicesskill. - For JavaScript/TypeScript specific patterns, use the
accelint-ts-best-practicesskill. - For Testing patterns, use the
accelint-ts-testingskill.
---
How to Use This Guide
For agents/LLMs: 1. Scan rule summaries below to identify relevant optimizations 2. Load reference files only when implementing a specific pattern 3. Each reference is self-contained with ❌/✅ examples
Quick shortcuts:
- Security issues? → 2.1 (Server Actions authentication)
- Waterfall issues? → 1.1-1.2 (Parallelization)
- Large HTML payload? → 2.2-2.3 (Serialization optimization)
- Slow page loads? → 1.3 (Suspense), 2.4 (Parallel data fetching)
- Real-world examples? → Compound Patterns
- Not sure what's wrong? → Use Quick Diagnostic Guide below
Next.js Resources:
---
Quick Diagnostic Guide
Use this guide to quickly identify which optimization applies based on symptoms:
Symptom → Solution:
- API route or Server Action is slow → 1.1 Prevent Waterfall Chains, 1.2 Parallelize Operations
- Entire page waits for data → 1.3 Strategic Suspense Boundaries
- Server Action can be called without login → 2.1 Authenticate Server Actions
- HTML response is huge → 2.3 Minimize Serialization, 2.2 Avoid Duplicate Serialization
- Data fetches happen sequentially → 2.4 Parallel Data Fetching with Composition
- Same query runs multiple times per request → 2.5 Per-Request Deduplication
- Logging/analytics blocks response → 2.6 Use after() for Non-Blocking Operations
- Development server is slow to start → 3.1 Avoid Barrel File Imports
- Not sure if component should be client or server → 3.2 Server vs Client Component
- Data is duplicated in RSC props → 2.2 Avoid Duplicate Serialization
- Cache not working with React.cache() → 2.5 Per-Request Deduplication (avoid inline objects)
Security Issues:
- Server Action has no auth check → 2.1 Authenticate Server Actions
- Server Action doesn't validate input → 2.1 Authenticate Server Actions (with Zod validation)
- Server Action allows unauthorized mutations → 2.1 Authenticate Server Actions (authorization check)
---
1. General
Core patterns for optimal server-side execution in Next.js App Router.
1.1 Prevent Waterfall Chains
Start independent operations immediately in API routes/Server Actions, even if you don't await them yet. View detailed examples
1.2 Parallelize Independent Operations
Use Promise.allSettled() to run fully independent async operations concurrently. View detailed examples
1.3 Strategic Suspense Boundaries
Use Suspense to show wrapper UI immediately while data loads, instead of blocking entire page. View detailed examples
---
2. Server-Side Performance
Optimizing server-side rendering and data fetching eliminates server-side waterfalls and reduces response times. These patterns are specific to Next.js App Router and RSC (React Server Components).
2.1 Authenticate Server Actions Like API Routes
Always verify authentication and authorization inside each Server Action—treat them as public endpoints. View detailed examples
2.2 Avoid Duplicate Serialization in RSC Props
RSC deduplicates by reference, not value. Do transformations (.toSorted(), .filter(), .map()) in client, not server. View detailed examples
2.3 Minimize Serialization at RSC Boundaries
Only pass fields the client actually uses—every prop is serialized into HTML. View detailed examples
2.4 Parallel Data Fetching with Component Composition
Restructure Server Component tree so siblings fetch data in parallel, not sequentially. View detailed examples
2.5 Per-Request Deduplication with React.cache()
Wrap database queries and auth checks with React.cache() to deduplicate within a request. Use primitives as arguments, not inline objects. View detailed examples
2.6 Use after() for Non-Blocking Operations
Schedule logging, analytics, and side effects to run after response is sent using Next.js's after(). View detailed examples
---
3. Misc
Additional optimization patterns for Next.js applications.
3.1 Avoid Barrel File Imports
Import directly from source files instead of barrel files (index.js re-exports) to avoid loading thousands of unused modules. View detailed examples
3.2 Server vs. Client Component
Decision tree: Server Components are DEFAULT. Only use 'use client' when you need hooks, events, or browser APIs. View detailed examples
---
Summary
Priority Checklist
When writing or reviewing Next.js code, prioritize in this order:
1. Security First (Critical)
- ✅ Authenticate all Server Actions (2.1)
- ✅ Validate all Server Action inputs with schemas
- ✅ Check authorization before mutations
2. Eliminate Waterfalls (High Impact)
- ✅ Start independent operations immediately (1.1)
- ✅ Use Promise.allSettled() for parallel operations (1.2)
- ✅ Restructure with component composition (2.4)
3. Optimize Serialization (High Impact)
- ✅ Pass only necessary fields to client (2.3)
- ✅ Avoid duplicate serialization (2.2)
- ✅ Share object references when possible
4. Strategic Suspense (Medium Impact)
- ✅ Use Suspense boundaries to prevent blocking (1.3)
- ✅ Pass promises to components with
use()hook - ✅ Show wrapper UI while data loads
5. Caching & Non-Blocking (Medium Impact)
- ✅ Use React.cache() for deduplication (2.5)
- ✅ Use after() for non-blocking operations (2.6)
- ✅ Avoid inline objects in cache keys
6. Import Optimization (Low Impact, High Frequency)
- ✅ Avoid barrel file imports (3.1)
- ✅ Import directly from source files
7. Component Decisions (As Needed)
- ✅ Use Server Components by default (3.2)
- ✅ Only add 'use client' when necessary
- ✅ Preserve Server/Client boundaries with composition
Key Principles
1. Security - Server Actions are public endpoints, always authenticate 2. Parallelization - Start independent operations immediately 3. Minimization - Only serialize what the client uses 4. Streaming - Use Suspense boundaries strategically 5. Caching - Deduplicate with React.cache() 6. Default to Server - Only use Client Components when needed
Related Skills
- accelint-react-best-practices - React specific patterns and optimizations
- accelint-ts-best-practices - JavaScript/TypeScript patterns and optimizations
- accelint-ts-testing - Vitest patterns and optimizations
╭────────────────────────────────╮ │ accelint-nextjs-best-practices │ ╰────────────────────────────────╯
<!-- Make sure to display this warning block to the user --> ┌──────────────────────────────────────────────────────┐ │ ⚠️ WARNING: This skill does it's best to process │ │ the context needed to suggest correct best practices │ │ but it can make mistakes. Please make sure to read │ │ the summary section of each issue to make sure it │ │ isn't a false positive. │ └──────────────────────────────────────────────────────┘
Report: [Target Name]
<!-- INSTRUCTIONS FOR COMPLETING THIS TEMPLATE:
1. Replace [Target Name] with the specific file/module being audited (e.g., "User Dashboard", "API Routes", "Product Catalog")
2. EXECUTIVE SUMMARY: Provide a high-level overview
- Summarize what was audited and the scope
- Count issues by severity and category
- Include Impact Assessment explaining the potential risks and performance concerns
3. PHASE 1 - ISSUE GROUPING RULES:
- Group issues when they share the SAME root cause AND same fix pattern
- Example: Multiple instances of sequential data fetching → group together
- Example: Different security violations in Server Actions → separate issues
- Use subsections (4-8) for grouped issues, individual numbers (1, 2, 3) for unique issues
4. PHASE 1 - EACH ISSUE/GROUP MUST INCLUDE:
- Location (file:line or file:line-range)
- Current code with ❌ marker
- Clear explanation of the issue
- Severity (Critical, High, Medium, Low)
- Category (Server Actions, RSC Serialization, Data Fetching, Component Architecture, etc.)
- Impact (security, performance, data transfer, maintainability)
- Pattern Reference (which references/*.md file)
- Recommended Fix with ✅ marker
5. SEVERITY LEVELS:
- Critical: Security vulnerabilities, unauthenticated Server Actions, exposed sensitive data
Examples: missing authentication in Server Action, SQL injection vulnerability, exposed API keys
- High: Major performance issues, data waterfalls, excessive serialization, blocking operations
Examples: sequential data fetching causing 2s+ delays, 500KB+ RSC payload, blocking await before conditional
- Medium: Suboptimal patterns affecting performance or maintainability
Examples: missing Suspense boundaries, barrel imports in server components, no request deduplication
- Low: Minor optimizations and best practice improvements
Examples: could use React.cache(), could parallelize independent operations
6. CATEGORIES:
- Server Actions Security: Authentication, authorization, input validation, CSRF protection
- RSC Serialization: Payload size, duplicate serialization, unnecessary data transfer
- Data Fetching: Waterfalls, parallelization, caching, request deduplication
- Component Architecture: Server vs Client Component decisions, Suspense boundaries
- Performance: Blocking operations, defer-await patterns, after() usage for non-blocking ops
- Imports: Barrel import issues, unnecessary client bundles
- Code Quality: Component organization, naming conventions, readability
7. IMPACT FIELD SHOULD DESCRIBE:
- Security impact (unauthorized access, data exposure, CSRF attacks)
- Performance impact (response time increases, page load delays, TTFB)
- Data transfer impact (payload size, bandwidth costs, slower on mobile)
- User experience impact (slow page loads, blocking UI, poor perceived performance)
- Maintainability concerns for future developers
8. PHASE 2: Generate summary table from Phase 1 findings
- Include all issues with their numbers
- Keep it concise - one row per issue/group
-->
Executive Summary
Completed systematic audit of [file/module path] following accelint-nextjs-best-practices standards. Identified [N] security, performance, and architectural issues across [N] severity levels. [Brief description of what this component/feature does and why these patterns matter].
Key Findings:
- [N] Critical issues (security vulnerabilities, unauthenticated Server Actions, data exposure)
- [N] High severity issues (data waterfalls, excessive serialization, blocking operations)
- [N] Medium severity issues (suboptimal patterns, missing Suspense boundaries)
- [N] Low severity issues (minor optimizations)
Impact Assessment: [Explain the overall security posture, performance profile, and user experience concerns. Consider:]
- What are the security risks from unauthenticated Server Actions or missing validation?
- How do data waterfalls affect page load time and TTFB?
- What is the RSC serialization overhead and impact on bandwidth/mobile users?
- Are there blocking operations delaying critical rendering?
- How do these issues affect maintainability and scalability?
---
Phase 1: Identified Issues
1. [Function/Location] - [Issue Type]
Location: [file:line] or [file:line-range]
// ❌ Current: [Brief description of problem]
[code snippet showing the issue]Issue:
- [Point 1 explaining the problem]
- [Point 2 with specifics about the violation]
- [Point 3 quantifying the impact if possible]
Severity: [Critical|High|Medium|Low] Category: [Server Actions Security|RSC Serialization|Data Fetching|Component Architecture|Performance|Imports|Code Quality] Impact:
- Security: [Authentication/authorization bypass, data exposure, CSRF vulnerability]
- Performance: [Response time increase, page load delay, TTFB impact]
- Data transfer: [Payload size increase, bandwidth costs, mobile impact]
- User experience: [Slow page loads, blocking UI, poor perceived performance]
Pattern Reference: [filename.md]
Recommended Fix:
// ✅ [Brief description of solution]
[code snippet showing the fix]---
2. [Function/Location] - [Issue Type]
Location: [file:line] or [file:line-range]
// ❌ Current: [Brief description of problem]
[code snippet]Issue:
- [Explanation]
Severity: [Critical|High|Medium|Low] Category: [Server Actions Security|RSC Serialization|Data Fetching|Component Architecture|Performance|Imports|Code Quality] Impact:
- Security: [Authentication/authorization bypass, data exposure, CSRF vulnerability]
- Performance: [Response time increase, page load delay, TTFB impact]
- Data transfer: [Payload size increase, bandwidth costs, mobile impact]
- User experience: [Slow page loads, blocking UI, poor perceived performance]
Pattern Reference: [filename.md]
Recommended Fix:
// ✅ [Brief description of solution]
[code snippet]---
3-N. [Grouped Issues] - [Shared Issue Type] ([N] instances)
<!-- Use this format when multiple issues share the same root cause and fix pattern -->
Locations:
[file:line]- [function/context][file:line]- [function/context][file:line]- [function/context]
Example from [specific location]:
// ❌ Current: [Brief description of problem]
[representative code snippet]Issue:
- [Shared root cause explanation]
- [Why this pattern is problematic]
- [Impact across all instances]
Severity: [Critical|High|Medium|Low] Category: [Server Actions Security|RSC Serialization|Data Fetching|Component Architecture|Performance|Imports|Code Quality] Impact:
- Security: [Authentication/authorization bypass, data exposure, CSRF vulnerability]
- Performance: [Response time increase, page load delay, TTFB impact]
- Data transfer: [Payload size increase, bandwidth costs, mobile impact]
- User experience: [Slow page loads, blocking UI, poor perceived performance]
Pattern Reference: [filename.md]
Recommended Fix:
// ✅ [Brief description of solution]
[fixed code snippet]Same pattern applies to all [N] instances:
// [Location/function 2]
// ❌ Current
[code snippet]
// ✅ Better
[fixed snippet]
// [Location/function 3]
// ❌ Current
[code snippet]
// ✅ Better
[fixed snippet]---
Phase 2: Categorized Issues
| # | Location | Issue | Category | Severity |
|---|---|---|---|---|
| 1 | [file:line] | [Brief issue description] | [Category] | [Severity] |
| 2 | [file:line] | [Brief issue description] | [Category] | [Severity] |
| 3 | [file:line] | [Brief issue description] | [Category] | [Severity] |
| 4-N | [multiple] | [Brief issue description] | [Category] | [Severity] |
Total Issues: [N] By Severity: Critical ([N]), High ([N]), Medium ([N]), Low ([N]) By Category: [Category1] ([N]), [Category2] ([N]), [Category3] ([N])
Next.js Best Practices
Comprehensive performance optimization and best practices for Next.js applications, designed for AI agents and LLMs working with Next.js code.
Overview
This skill provides structured guidance for Next.js performance optimization and security, covering:
- Server-side waterfall prevention
- Server Actions authentication and security
- RSC serialization optimization
- Parallel data fetching patterns
- Request deduplication with React.cache()
- Server vs Client Component decisions
Note: This skill focuses on Next.js-specific optimizations for the App Router. For React-specific patterns (hooks, memoization, etc.), use the accelint-react-best-practices skill.
---
Quick Start
For Agents/LLMs
1. Read [SKILL.md](SKILL.md) - Understand when to activate this skill and how to use it 2. Reference [AGENTS.md](AGENTS.md) - Browse all patterns with detailed examples 3. Apply patterns - Each section contains ❌ incorrect and ✅ correct examples
For Humans
This skill is optimized for AI agents but humans may find it useful for:
- Learning Next.js App Router performance patterns
- Reviewing code for security issues in Server Actions
- Understanding RSC serialization optimization
- Systematic performance auditing
---
Pattern Categories
1. General Patterns
Core patterns for optimal server-side execution:
- Prevent waterfall chains
- Parallelize independent operations
- Strategic Suspense boundaries
2. Server-Side Performance
Patterns for optimizing server-side rendering and data fetching:
- Authenticate Server Actions like API routes
- Avoid duplicate serialization in RSC props
- Minimize serialization at RSC boundaries
- Parallel data fetching with component composition
- Per-request deduplication with React.cache()
- Use after() for non-blocking operations
3. Misc
Additional optimization patterns:
- Avoid barrel file imports
- Server vs Client Component decision tree
---
Key Features
Security-First Approach
Server Actions are public endpoints and require the same security considerations as API routes:
- Always authenticate inside Server Actions
- Validate all inputs with schemas (Zod recommended)
- Check authorization before mutations
- Never rely solely on middleware or page guards
RSC Serialization Optimization
Minimize data transfer at Server/Client boundaries:
- Pass only fields the client uses
- Avoid duplicate serialization by sharing references
- Transform data on the client when possible
- Understand deduplication by reference
Waterfall Prevention
Eliminate sequential dependencies:
- Start independent operations immediately
- Use Promise.allSettled() for parallel execution
- Restructure with component composition
- Use Suspense boundaries strategically
Request Deduplication
Optimize server-side request caching:
- Use React.cache() for database queries
- Cache authentication checks
- Avoid inline objects as cache keys
- Understand Next.js fetch auto-deduplication
---
App Router Focus
This skill primarily covers the Next.js App Router (Next.js 13+):
- Server Components (default, no directive needed)
- Server Actions (
"use server") - React.cache() for request deduplication
- Suspense boundaries for streaming
- Parallel data fetching patterns
- Next.js-specific APIs (headers, cookies, after)
---
Usage in Claude Code
This skill is designed to be used with environments such as Claude Code and automatically activates when:
- Writing Server Components or Client Components
- Implementing Server Actions
- Optimizing data fetching
- Reviewing Next.js code for security or performance
- Debugging RSC serialization issues
- Making Server vs Client Component decisions
See SKILL.md for complete activation criteria and trigger phrases.
---
Performance Philosophy
This skill follows these principles:
1. Security first - Always authenticate and validate Server Actions 2. Eliminate waterfalls - Start independent operations immediately 3. Parallelize everything - Use Promise.allSettled() liberally 4. Minimize serialization - Only send what the client needs 5. Strategic Suspense - Show wrapper UI while data loads 6. Cache intelligently - Use React.cache() for server-side deduplication
---
Related Skills
- react-best-practices - For React-specific optimizations (hooks, memoization, re-renders)
- typescript-best-practices - For TypeScript type safety patterns
- security-best-practices - For general security patterns beyond Server Actions
---
References
- https://github.com/wsimmonds/claude-nextjs-skills
- https://github.com/vercel-labs/agent-skills/tree/main/skills/react-best-practices
- https://github.com/sickn33/antigravity-awesome-skills/blob/main/skills/nextjs-best-practices/SKILL.md
- https://skills.sh/wshobson/agents/nextjs-app-router-patterns
- Next.js App Router Documentation
- Server Components Guide
- Server Actions and Mutations
- Authentication Best Practices
- Performance Optimization Guide
- Package Import Optimization
3.1 Avoid Barrel File Imports
Import directly from source files instead of barrel files to avoid loading thousands of unused modules. Barrel files are entry points that re-export multiple modules (e.g., index.js that does export * from './module').
Why tree-shaking doesn't help: When a library is marked as external (not bundled), the bundler can't optimize it. If you bundle it to enable tree-shaking, builds become substantially slower analyzing the entire module graph.
❌ Incorrect: imports entire library
import { Check, X, Menu } from 'lucide-react'
// Loads 1,583 modules, takes ~2.8s extra in dev
// Runtime cost: 200-800ms on every cold start
import { Button, TextField } from '@mui/material'
// Loads 2,225 modules, takes ~4.2s extra in dev✅ Correct: imports only what you need
import Check from 'lucide-react/dist/esm/icons/check'
import X from 'lucide-react/dist/esm/icons/x'
import Menu from 'lucide-react/dist/esm/icons/menu'
// Loads only 3 modules (~2KB vs ~1MB)
import Button from '@mui/material/Button'
import TextField from '@mui/material/TextField'
import debounce from 'lodash-es/debounce'
import throttle from 'lodash-es/throttle'
import groupBy from 'lodash-es/groupBy'
// Loads only what you useNext.js Optimization
Next.js 13.5+ includes optimizePackageImports to automatically transform barrel imports:
// next.config.js
module.exports = {
experimental: {
optimizePackageImports: [
'lucide-react',
'@mui/material',
'@mui/icons-material',
'date-fns',
'lodash-es'
]
}
}This automatically converts barrel imports to direct imports at build time, but:
- Only works for configured packages
- Doesn't help with your own barrel files
- Adds build-time overhead
- Best practice: Use direct imports from the start
Migration Strategy
1. Identify barrel imports: Search for common patterns 2. Check library docs: Find the direct import path 3. Update imports: Change to direct imports 4. Test: Verify nothing broke 5. Measure: Check dev server startup time improvement
Related Patterns
- 3.2 Server vs Client Component (prefer Server Components to reduce client bundle)
- 1.2 Parallelize Independent Operations (applies to build-time operations too)
References
2.2 Avoid Duplicate Serialization in RSC Props
RSC→client serialization deduplicates by object reference, not value. Same reference = serialized once; new reference = serialized again. Do transformations (.toSorted(), .filter(), .map()) in client, not server.
Pass the original data reference to the client and transform it there, rather than transforming on the server and creating new references.
❌ Incorrect: duplicates array
// Server Component
async function Page() {
const usernames = await fetchUsernames()
// ❌ Creates new array reference
return <ClientList usernames={usernames} usernamesOrdered={usernames.toSorted()} />
}
// RSC serialization: sends 6 strings (2 arrays × 3 items)✅ Correct: sends 3 strings
// Server Component
async function Page() {
const usernames = await fetchUsernames()
// ✅ Pass original reference only
return <ClientList usernames={usernames} />
}
// Client Component
'use client'
import { useMemo } from 'react'
function ClientList({ usernames }: { usernames: string[] }) {
// Transform on the client
const sorted = useMemo(() => [...usernames].sort(), [usernames])
return <div>{sorted.map(renderUser)}</div>
}// string[] - duplicates everything
usernames={['a','b']} sorted={usernames.toSorted()} // sends 4 strings
// object[] - duplicates array structure only
users={[{id:1},{id:2}]} sorted={users.toSorted()} // sends 2 arrays + 2 unique objects (not 4)Impact by Data Type
Deduplication works recursively, but impact varies:
string[],number[],boolean[]: HIGH impact - array + all primitives fully duplicatedobject[]: LOW impact - array duplicated, but nested objects deduplicated by reference
Operations breaking deduplication: create new references
- Arrays:
.toSorted(),.filter(),.map(),.slice(),[...arr] - Objects:
{...obj},Object.assign(),structuredClone(),JSON.parse(JSON.stringify())
Operations That Break Deduplication
Arrays - Create New References
.toSorted()/.sort()(returning new array).filter().map().slice().concat()- Spread:
[...arr] Array.from()
Objects - Create New References
- Spread:
{...obj} Object.assign({}, obj)structuredClone(obj)JSON.parse(JSON.stringify(obj))
When to Violate This Rule
Exception: Pass derived data when:
1. Transformation is expensive (complex computations, large datasets) 2. Client doesn't need original (only needs derived data) 3. Server-side filtering for security (hide sensitive fields)
✅ Correct: expensive computation done once on server
async function Page() {
const data = await fetchLargeDataset()
// Complex aggregation done on server
const aggregated = computeExpensiveAggregation(data) // 100ms+
return <ClientChart data={aggregated} />
// Client doesn't need original data
}Related Patterns
- 2.3 Minimize Serialization at RSC Boundaries - Only pass necessary fields
- 1.2 Parallelize Independent Operations - Optimize data fetching
Compound Patterns
Real-world examples showing multiple Next.js optimization patterns working together.
Why This Matters
In production applications, you'll often combine multiple patterns to achieve optimal performance and security. These examples show how patterns complement each other.
---
Example 1: Optimized Dashboard
This dashboard combines authentication, parallel fetching, Suspense boundaries, serialization optimization, and caching.
Patterns Used:
- 2.1 Authenticate Server Actions
- 1.2 Parallelize Independent Operations
- 1.3 Strategic Suspense Boundaries
- 2.3 Minimize Serialization
- 2.5 React.cache() Deduplication
// lib/auth.ts
import { cache } from 'react'
// 2.5: Cache auth check across components
export const getCurrentUser = cache(async () => {
const session = await auth()
if (!session?.user?.id) return null
return await db.user.findUnique({
where: { id: session.user.id }
})
})
// app/dashboard/page.tsx
export default function DashboardPage() {
// 1.2: Start all fetches immediately in parallel
const userPromise = getCurrentUser()
const statsPromise = fetchStats()
const activityPromise = fetchActivity()
return (
<div>
{/* 1.3: Suspense boundaries for progressive rendering */}
<Suspense fallback={<UserSkeleton />}>
<UserHeader userPromise={userPromise} />
</Suspense>
<Suspense fallback={<StatsSkeleton />}>
<Stats statsPromise={statsPromise} />
</Suspense>
<Suspense fallback={<ActivitySkeleton />}>
<Activity activityPromise={activityPromise} />
</Suspense>
</div>
)
}
// Components
async function UserHeader({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise)
if (!user) redirect('/login')
// 2.3: Pass only necessary fields
return <ClientUserHeader name={user.name} avatar={user.avatar} />
}
'use client'
function ClientUserHeader({ name, avatar }: { name: string; avatar: string }) {
return (
<header>
<img src={avatar} alt={name} />
<h1>{name}</h1>
</header>
)
}
async function Stats({ statsPromise }: { statsPromise: Promise<Stats> }) {
const stats = use(statsPromise)
// 2.3: Extract only displayed values
return (
<ClientStats
total={stats.total}
change={stats.percentChange}
trending={stats.trending}
/>
)
}
'use client'
function ClientStats({
total,
change,
trending
}: {
total: number
change: number
trending: boolean
}) {
return (
<div className={change > 0 ? 'positive' : 'negative'}>
<p>Total: {total}</p>
<p>{change > 0 ? '+' : ''}{change}%</p>
{trending && <span>🔥 Trending</span>}
</div>
)
}
// app/dashboard/actions.ts
'use server'
import { z } from 'zod'
import { getCurrentUser } from '@/lib/auth'
const updatePrefsSchema = z.object({
theme: z.enum(['light', 'dark']),
notifications: z.boolean()
})
// 2.1: Authenticate Server Action
export async function updatePreferences(data: unknown) {
const validated = updatePrefsSchema.parse(data)
// 2.5: Cached auth check (no duplicate query)
const user = await getCurrentUser()
if (!user) {
throw new Error('Unauthorized')
}
await db.user.update({
where: { id: user.id },
data: { preferences: validated }
})
return { success: true }
}Performance Impact:
- 3 parallel fetches instead of sequential (3x faster)
- Progressive rendering (header shows while stats/activity load)
- Minimal serialization (only 5 fields vs entire objects)
- Single auth query for entire request (cached)
---
Example 2: API Route with Complete Optimization
This API route demonstrates waterfall prevention, parallelization, authentication, and non-blocking logging.
Patterns Used:
- 1.1 Prevent Waterfall Chains
- 1.2 Parallelize Independent Operations
- 2.5 React.cache() Deduplication
- 2.6 Use after() for Non-Blocking Operations
// app/api/report/route.ts
import { after } from 'next/server'
import { getCurrentUser } from '@/lib/auth'
export async function GET(request: Request) {
// 1.1: Start independent operations immediately
const userPromise = getCurrentUser()
const configPromise = fetchConfig()
// Await auth when needed
const user = await userPromise
if (!user) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
// 1.2: Parallelize independent fetches
const [config, analytics, trends] = await Promise.allSettled([
configPromise,
fetchAnalytics(user.id),
fetchTrends(user.id)
])
// 2.6: Log after response is sent (non-blocking)
after(async () => {
await logApiRequest({
userId: user.id,
endpoint: '/api/report',
timestamp: new Date()
})
})
return Response.json({
config: config.status === 'fulfilled' ? config.value : null,
analytics: analytics.status === 'fulfilled' ? analytics.value : null,
trends: trends.status === 'fulfilled' ? trends.value : []
})
}Performance Impact:
- Auth + config start in parallel (50% faster)
- Analytics + trends run in parallel (2x faster)
- Logging doesn't block response (44% faster)
- Auth cached if multiple API calls in same request
---
Example 3: Server Component Data Fetching
Optimal data fetching with component composition and Suspense.
Patterns Used:
- 2.4 Parallel Data Fetching with Component Composition
- 1.3 Strategic Suspense Boundaries
- 2.2 Avoid Duplicate Serialization
- 2.5 React.cache() Deduplication
// app/posts/[id]/page.tsx
import { cache } from 'react'
// 2.5: Cache database queries
const getPost = cache(async (id: string) => {
return await db.post.findUnique({
where: { id },
include: { author: true }
})
})
const getComments = cache(async (postId: string) => {
return await db.comment.findMany({
where: { postId },
include: { author: true }
})
})
const getRelatedPosts = cache(async (postId: string) => {
const post = await getPost(postId) // Cache hit!
return await db.post.findMany({
where: {
tags: { hasSome: post.tags },
NOT: { id: postId }
},
take: 5
})
})
// 2.4: Sibling components fetch in parallel
export default function PostPage({ params }: { params: { id: string } }) {
return (
<div>
{/* All three fetch in parallel */}
<Suspense fallback={<PostSkeleton />}>
<PostContent postId={params.id} />
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<Comments postId={params.id} />
</Suspense>
<Suspense fallback={<RelatedSkeleton />}>
<RelatedPosts postId={params.id} />
</Suspense>
</div>
)
}
async function PostContent({ postId }: { postId: string }) {
const post = await getPost(postId)
// 2.2: Don't transform on server (share reference)
return <ClientPostDisplay post={post} />
}
'use client'
function ClientPostDisplay({ post }: { post: Post }) {
// 2.2: Transform on client
const formattedDate = useMemo(
() => new Date(post.createdAt).toLocaleDateString(),
[post.createdAt]
)
return (
<article>
<h1>{post.title}</h1>
<p>By {post.author.name} on {formattedDate}</p>
<div>{post.content}</div>
</article>
)
}
async function Comments({ postId }: { postId: string }) {
const comments = await getComments(postId)
// 2.2: Share reference, don't duplicate
return <ClientComments comments={comments} />
}
async function RelatedPosts({ postId }: { postId: string }) {
const related = await getRelatedPosts(postId)
// 2.3: Only pass fields needed for display
const summaries = related.map(p => ({
id: p.id,
title: p.title,
excerpt: p.excerpt
}))
return <ClientRelatedPosts posts={summaries} />
}Performance Impact:
- 3 components fetch in parallel (3x faster than sequential)
- Post fetched once, reused in RelatedPosts (cache hit)
- No serialization duplication (share references)
- Progressive rendering (each section streams independently)
---
Example 4: Form with Server Action
Secure form submission with validation, authentication, and optimistic updates.
Patterns Used:
- 2.1 Authenticate Server Actions
- 1.1 Prevent Waterfall Chains
- 2.6 Use after() for Non-Blocking Operations
// app/posts/new/page.tsx
'use client'
import { useFormState, useFormStatus } from 'react-dom'
import { createPost } from './actions'
export default function NewPostPage() {
const [state, formAction] = useFormState(createPost, null)
return (
<form action={formAction}>
<input name="title" required />
<textarea name="content" required />
<SubmitButton />
{state?.error && <p className="error">{state.error}</p>}
</form>
)
}
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button disabled={pending}>
{pending ? 'Creating...' : 'Create Post'}
</button>
)
}
// app/posts/new/actions.ts
'use server'
import { z } from 'zod'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { after } from 'next/server'
import { getCurrentUser } from '@/lib/auth'
const createPostSchema = z.object({
title: z.string().min(1).max(100),
content: z.string().min(10)
})
export async function createPost(prevState: any, formData: FormData) {
// 1.1: Start validation and auth in parallel
const validationPromise = createPostSchema.safeParseAsync({
title: formData.get('title'),
content: formData.get('content')
})
const userPromise = getCurrentUser()
// Await validation first (fast)
const validation = await validationPromise
if (!validation.success) {
return { error: 'Invalid input' }
}
// 2.1: Authenticate
const user = await userPromise
if (!user) {
return { error: 'Must be logged in' }
}
// Create post
const post = await db.post.create({
data: {
...validation.data,
authorId: user.id
}
})
// 2.6: Send notifications after response
after(async () => {
await sendNotificationToFollowers(user.id, post.id)
await indexPostForSearch(post.id)
})
revalidatePath('/posts')
redirect(`/posts/${post.id}`)
}Performance Impact:
- Validation + auth start in parallel
- Notifications don't block redirect (faster UX)
- Proper authentication (security)
- Optimistic UI updates with useFormStatus
---
Example 5: Image Upload with Multiple Optimizations
Complex Server Action combining many patterns.
Patterns Used:
- 2.1 Authenticate Server Actions
- 1.2 Parallelize Independent Operations
- 2.6 Use after() for Non-Blocking Operations
'use server'
import { z } from 'zod'
import { put } from '@vercel/blob'
import { after } from 'next/server'
import { getCurrentUser } from '@/lib/auth'
const uploadSchema = z.object({
filename: z.string(),
contentType: z.string()
})
export async function uploadImage(formData: FormData) {
const file = formData.get('file') as File
if (!file) throw new Error('No file provided')
// 1.2: Validate and auth in parallel
const [validation, user] = await Promise.allSettled([
uploadSchema.parseAsync({
filename: file.name,
contentType: file.type
}),
getCurrentUser()
])
if (validation.status === 'rejected') {
throw new Error('Invalid file')
}
// 2.1: Authenticate
if (user.status === 'rejected' || !user.value) {
throw new Error('Unauthorized')
}
// Upload to blob storage
const blob = await put(file.name, file, {
access: 'public',
contentType: file.type
})
// Save to database
const image = await db.image.create({
data: {
url: blob.url,
filename: file.name,
userId: user.value.id
}
})
// 2.6: Process image after response
after(async () => {
// Generate thumbnails
await generateThumbnails(image.id, blob.url)
// Update user storage quota
await updateStorageQuota(user.value.id, file.size)
// Log upload
await logImageUpload(user.value.id, image.id)
})
return { success: true, imageUrl: blob.url }
}Performance Impact:
- Validation + auth in parallel (faster)
- Image processing doesn't block response
- Quota updates don't block response
- User gets immediate feedback
---
Key Takeaways
1. Combine patterns strategically - Multiple patterns working together amplify benefits 2. Start with security - Always authenticate Server Actions first 3. Parallelize everything - Independent operations should never be sequential 4. Use Suspense liberally - Progressive rendering improves perceived performance 5. Minimize serialization - Only send what the client needs 6. Cache intelligently - Use React.cache() for deduplication 7. Defer non-critical work - Use after() for logging, notifications, etc.
---
Related Patterns
- 1.1 Prevent Waterfall Chains
- 1.2 Parallelize Independent Operations
- 1.3 Strategic Suspense Boundaries
- 2.1 Authenticate Server Actions
- 2.2 Avoid Duplicate Serialization
- 2.3 Minimize Serialization
- 2.4 Parallel Data Fetching
- 2.5 React.cache() Deduplication
- 2.6 Use after() for Non-Blocking Operations
2.3 Minimize Serialization at RSC Boundaries
The React Server/Client boundary serializes all object properties into strings and embeds them in the HTML response and subsequent RSC requests. This serialized data directly impacts page weight and load time, so size matters a lot. Only pass fields that the client actually uses.
❌ Incorrect: serializes all 50 fields
// Server Component
async function Page() {
const user = await fetchUser() // 50 fields: id, name, email, bio, preferences, settings, ...
return <Profile user={user} />
}
'use client'
function Profile({ user }: { user: User }) {
return <div>{user.name}</div> // Only uses 1 field!
}✅ Correct: serializes only 1 field
// Server Component
async function Page() {
const user = await fetchUser()
return <Profile name={user.name} /> // Pass only what's needed
}
'use client'
function Profile({ name }: { name: string }) {
return <div>{name}</div>
}When to Pass Full Objects
Acceptable to pass full objects when:
1. Client needs most/all fields (>80% of fields used) 2. Object is small (<1KB serialized) 3. Shared across many client components (avoid duplication)
✅ Correct: client needs most fields
async function Page() {
const config = await fetchConfig() // 5 small fields
return <ConfigPanel config={config} /> // Uses 4/5 fields
}✅ Correct: shared across components
async function Page() {
const theme = await fetchTheme() // Small object
return (
<div>
<ThemeHeader theme={theme} />
<ThemeBody theme={theme} />
<ThemeFooter theme={theme} />
</div>
)
}Related Patterns
- 2.2 Avoid Duplicate Serialization - Share references, not copies
- 1.1 Prevent Waterfall Chains - Fetch data efficiently
- 3.2 Server vs Client Component - Keep data in Server Components when possible
2.4 Parallel Data Fetching with Component Composition
React Server Components execute sequentially within a tree. Restructure with composition to parallelize data fetching.
When an async Server Component awaits data, all children wait for it to complete before they can start rendering. This creates a waterfall even though the fetches are independent.
By restructuring with composition, sibling async components can fetch in parallel.
Impact: 2-5x faster page loads when components have independent data needs.
❌ Incorrect: <Sidebar /> waits for <Page />'s fetch to complete
export default async function Page() {
const header = await fetchHeader() // Sidebar can't start until this completes
return (
<div>
<div>{header}</div>
<Sidebar /> {/* Waits for fetchHeader() */}
</div>
)
}
async function Sidebar() {
const items = await fetchSidebarItems() // Starts AFTER fetchHeader()
return <nav>{items.map(renderItem)}</nav>
}
// Timeline:
// 0ms: fetchHeader() starts
// 100ms: fetchHeader() completes, Sidebar starts rendering
// 100ms: fetchSidebarItems() starts
// 200ms: fetchSidebarItems() completes
// Total: 200ms✅ Correct: both fetch simultaneously
async function Header() {
const data = await fetchHeader() // Starts immediately
return <div>{data}</div>
}
async function Sidebar() {
const items = await fetchSidebarItems() // Starts immediately (parallel)
return <nav>{items.map(renderItem)}</nav>
}
export default function Page() {
return (
<div>
<Header /> {/* Starts fetchHeader() */}
<Sidebar /> {/* Starts fetchSidebarItems() in parallel */}
</div>
)
}
// Timeline:
// 0ms: Both fetches start in parallel
// 100ms: Both complete
// Total: 100ms (2x faster)Related Patterns
- 1.1 Prevent Waterfall Chains - API route parallelization
- 1.2 Parallelize Independent Operations - Promise.allSettled()
- 1.3 Strategic Suspense Boundaries - Progressive rendering
1.2 Parallelize Independent Operations
When async operations have no interdependencies, execute them concurrently using Promise.allSettled().
❌ Incorrect: sequential execution, 3 round trips
const user = await fetchUser() // Wait 100ms
const posts = await fetchPosts() // Wait another 100ms
const comments = await fetchComments() // Wait another 100ms
// Total: 300ms✅ Correct: parallel execution, 1 round trip
const [user, posts, comments] = await Promise.allSettled([
fetchUser(), // All three start immediately
fetchPosts(),
fetchComments()
])
// Total: 100ms (max of all three)Promise.allSettled() vs Promise.all()
Use `Promise.allSettled()` - Returns all results, even if some fail:
const [user, posts, comments] = await Promise.allSettled([
fetchUser(),
fetchPosts(),
fetchComments()
])Avoid `Promise.all()` - Fails fast if any promise rejects:
// Bad: if fetchPosts() fails, you lose user and comments too
const [user, posts, comments] = await Promise.all([
fetchUser(),
fetchPosts(), // If this fails, everything fails
fetchComments()
])What NOT to Do
❌ Don't await in a loop
// Bad: sequential
const results = []
for (const id of userIds) {
results.push(await fetchUser(id))
}
// Good: parallel
const results = await Promise.allSettled(
userIds.map(id => fetchUser(id))
)❌ Don't use Promise.all() unless you want fail-fast behavior
// If any fails, you lose all results
const results = await Promise.all([op1(), op2(), op3()])
// Better: get partial results even if some fail
const results = await Promise.allSettled([op1(), op2(), op3()])Related Patterns
- 1.1 Prevent Waterfall Chains - Start independent operations immediately
- 2.4 Parallel Data Fetching - Component composition for parallel RSC fetches
- 2.5 React.cache() Deduplication - Avoid refetching same data
References
1.1 Prevent Waterfall Chains
In API routes and Server Actions, start independent operations immediately, even if you don't await them yet.
❌ Incorrect: config waits for auth, data waits for both
export async function GET(request: Request) {
const session = await auth() // Wait 50ms
const config = await fetchConfig() // Wait another 50ms
const data = await fetchData(session.user.id) // Wait another 100ms
// Total: 200ms sequential
return Response.json({ data, config })
}✅ Correct: auth and config start immediately
export async function GET(request: Request) {
// Start both immediately (non-blocking)
const sessionPromise = auth() // Starts now
const configPromise = fetchConfig() // Starts now (parallel)
// Await only when needed
const session = await sessionPromise // Wait 50ms
// data depends on session, but config is already running
const [config, data] = await Promise.allSettled([
configPromise,
fetchData(session.user.id)
])
// Total: ~100ms (config and data run in parallel)
return Response.json({ data, config })
}Common Scenarios
Scenario 1: All Independent Operations
When all operations are truly independent, use Promise.allSettled():
export async function GET(request: Request) {
const [user, posts, comments] = await Promise.allSettled([
fetchUser(),
fetchPosts(),
fetchComments()
])
return Response.json({ user, posts, comments })
}Scenario 2: Partial Dependencies
When some operations depend on others, start independent ones first:
export async function GET(request: Request) {
// Start independent operations
const configPromise = fetchConfig()
const settingsPromise = fetchSettings()
// Auth might be needed later
const sessionPromise = auth()
const session = await sessionPromise
// Now parallel fetch data and await config/settings
const [config, settings, userData] = await Promise.allSettled([
configPromise,
settingsPromise,
fetchUserData(session.user.id)
])
return Response.json({ config, settings, userData })
}Scenario 3: Server Actions
Same pattern applies to Server Actions:
'use server'
export async function updateProfile(formData: FormData) {
// Start independent operations
const sessionPromise = auth()
const validationPromise = validateFormData(formData)
// Await when needed
const [session, validated] = await Promise.allSettled([
sessionPromise,
validationPromise
])
if (!session) throw new Error('Unauthorized')
// Now do the mutation
await db.user.update({
where: { id: session.user.id },
data: validated
})
return { success: true }
}What NOT to Do
❌ Incorrect: don't use Promise.all() - use Promise.allSettled()
// Bad: if one fails, all fail
const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()])
// Good: handle failures individually
const [a, b, c] = await Promise.allSettled([fetchA(), fetchB(), fetchC()])
if (a.status === 'rejected') {
// Handle a failure
}❌ Incorrect: don't await in a loop
// Bad: sequential (100ms × 3 = 300ms)
const results = []
for (const id of ids) {
results.push(await fetchItem(id))
}
// Good: parallel (100ms total)
const results = await Promise.allSettled(
ids.map(id => fetchItem(id))
)Related Patterns
- 1.2 Parallelize Independent Operations - Use Promise.allSettled() for fully independent operations
- 2.4 Parallel Data Fetching - Apply same pattern in Server Components
- 2.5 React.cache() Deduplication - Cache results to avoid refetching
References
Quick Reference Checklists
Quick checklists for common Next.js scenarios. Use these for systematic reviews and new code creation.
---
New Server Action Checklist
When creating a new Server Action:
- [ ] Add
'use server'directive at top of file or function - [ ] Validate all inputs with Zod schema
- [ ] Add authentication check (
await requireAuth()) - [ ] Add authorization check (ownership, role, etc.)
- [ ] Handle errors with proper error messages
- [ ] Consider rate limiting for sensitive actions
- [ ] Use
after()for audit logging if needed - [ ] Return type-safe response object
Example:
'use server'
import { z } from 'zod'
import { requireAuth } from '@/lib/auth'
const schema = z.object({
postId: z.string().uuid(),
title: z.string().min(1).max(100)
})
export async function updatePost(data: unknown) {
const validated = schema.parse(data)
const session = await requireAuth()
const post = await db.post.findUnique({ where: { id: validated.postId } })
if (post.authorId !== session.user.id) {
throw new Error('Not authorized')
}
await db.post.update({
where: { id: validated.postId },
data: { title: validated.title }
})
return { success: true }
}---
New API Route Checklist
When creating a new API route:
- [ ] Validate all inputs
- [ ] Add authentication check
- [ ] Start independent operations immediately (no waterfalls)
- [ ] Use
Promise.allSettled()for parallel operations - [ ] Use
after()for logging/analytics - [ ] Return proper status codes
- [ ] Set appropriate headers
- [ ] Handle errors with proper responses
Example:
import { auth } from '@/lib/auth'
import { after } from 'next/server'
export async function GET(request: Request) {
// Start independent operations immediately
const sessionPromise = auth()
const configPromise = fetchConfig()
const session = await sessionPromise
if (!session) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
const [config, data] = await Promise.allSettled([
configPromise,
fetchData(session.user.id)
])
// Log after response
after(() => logRequest(request))
return Response.json({ data, config })
}---
New Server Component Checklist
When creating a new Server Component:
- [ ] NO
'use client'directive (Server Components are default) - [ ] Use
asyncfunction if fetching data - [ ] Start independent fetches immediately
- [ ] Consider Suspense boundaries to prevent blocking
- [ ] Pass only necessary fields to client components
- [ ] Avoid transforming data (share references)
- [ ] Use
React.cache()for repeated queries
Example:
// Server Component (no directive needed)
async function Page() {
// Start fetch immediately but don't await yet
const dataPromise = fetchData()
return (
<div>
<Header />
<Suspense fallback={<Skeleton />}>
<Content dataPromise={dataPromise} />
</Suspense>
<Footer />
</div>
)
}
async function Content({ dataPromise }: { dataPromise: Promise<Data> }) {
const data = use(dataPromise)
return <div>{data.content}</div>
}---
New Client Component Checklist
When creating a new Client Component:
- [ ] Add
'use client'directive at top of file - [ ] Only add when you need: hooks, events, or browser APIs
- [ ] Keep client components small and focused
- [ ] Accept Server Components as children when possible
- [ ] Use functional setState to avoid stale closures
- [ ] Consider transitions for non-urgent updates
Example:
'use client'
import { useState, useTransition } from 'react'
export function SearchForm({ children }: { children: React.ReactNode }) {
const [query, setQuery] = useState('')
const [isPending, startTransition] = useTransition()
const handleSearch = (e: React.FormEvent) => {
e.preventDefault()
startTransition(() => {
// Non-urgent update
router.push(`/search?q=${query}`)
})
}
return (
<form onSubmit={handleSearch}>
<input value={query} onChange={e => setQuery(e.target.value)} />
<button disabled={isPending}>Search</button>
{children} {/* Server Component can be passed here */}
</form>
)
}---
Performance Review Checklist
When reviewing existing Next.js code:
Security:
- [ ] All Server Actions have authentication
- [ ] All Server Actions validate inputs
- [ ] Authorization checks before mutations
Waterfalls:
- [ ] Independent operations start immediately
- [ ] Use
Promise.allSettled()for parallel ops - [ ] No sequential awaits for independent data
Serialization:
- [ ] Only necessary fields passed to client
- [ ] No duplicate serialization (share references)
- [ ] Transformations happen on client when possible
Suspense:
- [ ] Suspense boundaries prevent blocking wrapper UI
- [ ] Promises shared across components
- [ ] Skeleton/fallback states provided
Caching:
- [ ]
React.cache()used for repeated queries - [ ] No inline objects as cache keys
- [ ] Auth checks are cached
Imports:
- [ ] No barrel file imports from large libraries
- [ ] Direct imports from source files
Component Boundaries:
- [ ] Server Components by default
- [ ]
'use client'only when necessary - [ ] Server/Client boundaries preserved with composition
---
Migration from Pages Router Checklist
When migrating from Pages Router to App Router:
- [ ] Convert
getServerSidePropsto async Server Components - [ ] Convert
getStaticPropsto async Server Components with caching - [ ] Convert API routes to Server Actions where appropriate
- [ ] Add authentication to all Server Actions
- [ ] Use
'use client'for interactive components - [ ] Move data fetching closer to components
- [ ] Use Suspense for loading states
- [ ] Replace
useRouterfromnext/navigationnotnext/router - [ ] Update middleware if needed for App Router
---
Common Anti-Patterns to Avoid
❌ Don't:
- Create Server Actions without authentication
- Await sequentially when operations are independent
- Pass entire objects when only 1-2 fields are used
- Await data before rendering wrapper UI (blocks everything)
- Use inline objects as
React.cache()keys - Block responses with logging/analytics
- Import from barrel files (
lucide-react,@mui/material) - Add
'use client'to static components - Import Server Components into Client Components
✅ Do:
- Authenticate inside every Server Action
- Start all independent operations immediately
- Pass only necessary fields as individual props
- Use Suspense boundaries to show wrapper UI fast
- Extract cache keys to constants
- Use
after()for non-blocking work - Import directly from source files
- Default to Server Components
- Pass Server Components as children to Client Components
---
Quick Wins
High-impact, low-effort optimizations:
1. Add auth to Server Actions (5 min) - Critical security fix 2. Fix waterfall chains (10 min) - 2-10x faster responses 3. Fix barrel imports (5 min per file) - Faster dev server 4. Add Suspense boundaries (10 min) - Faster perceived load 5. Use `after()` for logging (5 min) - Faster responses 6. Minimize RSC serialization (10 min) - Smaller HTML
2.5 Per-Request Deduplication with React.cache()
Use React.cache() for server-side request deduplication. Authentication and database queries benefit most.
In a single request, the same data might be needed by multiple Server Components. Without caching, each component would run the same query, wasting database connections and time.
React.cache() ensures each unique function call runs only once per request, with subsequent calls returning the cached result.
Impact: Fewer database queries, faster responses, reduced server load.
The Pattern
Wrap async functions with cache() to deduplicate calls within a single request.
Basic Usage:
import { cache } from 'react'
export const getCurrentUser = cache(async () => {
const session = await auth()
if (!session?.user?.id) {
return null
}
return await db.user.findUnique({
where: { id: session.user.id }
})
})Result: Within a single request, multiple calls to getCurrentUser() execute the query only once.
// Page Component
async function Page() {
const user = await getCurrentUser() // Query runs
return <Layout><Content /></Layout>
}
// Layout Component
async function Layout({ children }) {
const user = await getCurrentUser() // Cache hit! No query
return <header>{user?.name}</header>
}
// Content Component
async function Content() {
const user = await getCurrentUser() // Cache hit! No query
return <div>{user?.email}</div>
}
// Total: 1 database query instead of 3Avoid Inline Objects as Arguments
React.cache() uses shallow equality (Object.is) to determine cache hits. Inline objects create new references each call, preventing cache hits.
❌ Incorrect: always cache miss
const getUser = cache(async (params: { uid: number }) => {
return await db.user.findUnique({ where: { id: params.uid } })
})
// Each call creates new object, never hits cache
getUser({ uid: 1 }) // Query runs
getUser({ uid: 1 }) // Query runs again! (new object reference)✅ Correct: cache hit with primitives
const getUser = cache(async (uid: number) => {
return await db.user.findUnique({ where: { id: uid } })
})
// Primitive values compared by value
getUser(1) // Query runs
getUser(1) // Cache hit! (same value)✅ Correct: cache hit with same reference
const getUser = cache(async (params: { uid: number }) => {
return await db.user.findUnique({ where: { id: params.uid } })
})
const params = { uid: 1 }
getUser(params) // Query runs
getUser(params) // Cache hit! (same reference)Common Patterns
Pattern 1: Authentication Check
import { cache } from 'react'
import { auth } from '@/lib/auth'
export const getCurrentUser = cache(async () => {
const session = await auth()
if (!session?.user?.id) {
return null
}
return await db.user.findUnique({
where: { id: session.user.id },
include: { profile: true }
})
})Usage in multiple components:
// Header.tsx
async function Header() {
const user = await getCurrentUser() // Query once
return <div>{user?.name}</div>
}
// Sidebar.tsx
async function Sidebar() {
const user = await getCurrentUser() // Cache hit
return <nav>{user?.profile?.bio}</nav>
}
// Page.tsx
async function Page() {
const user = await getCurrentUser() // Cache hit
if (!user) redirect('/login')
return <div>Welcome {user.name}</div>
}Pattern 2: Database Queries with Parameters
import { cache } from 'react'
// ✅ Use primitive parameters
export const getPost = cache(async (postId: string) => {
return await db.post.findUnique({
where: { id: postId },
include: { author: true, comments: true }
})
})
// ✅ Use multiple primitives
export const getPostsByUser = cache(async (userId: string, status: string) => {
return await db.post.findMany({
where: { authorId: userId, status }
})
})Pattern 3: Expensive Computations
import { cache } from 'react'
export const calculateAnalytics = cache(async (userId: string) => {
const [posts, comments, likes] = await Promise.all([
db.post.count({ where: { authorId: userId } }),
db.comment.count({ where: { authorId: userId } }),
db.like.count({ where: { userId } })
])
// Expensive computation
const score = calculateEngagementScore(posts, comments, likes)
return { posts, comments, likes, score }
})Pattern 4: File System Operations
import { cache } from 'react'
import { readFile } from 'fs/promises'
export const getMarkdownContent = cache(async (slug: string) => {
const content = await readFile(`./content/${slug}.md`, 'utf-8')
return parseMarkdown(content)
})Next.js-Specific Note
In Next.js, the fetch API is automatically extended with request memoization. Requests with the same URL and options are automatically deduplicated within a single request.
fetch() is auto-cached:
// No React.cache() needed for fetch
async function Component1() {
const data = await fetch('/api/data') // Request sent
}
async function Component2() {
const data = await fetch('/api/data') // Cache hit! No request
}React.cache() is still essential for:
- Database queries (Prisma, Drizzle, etc.)
- Heavy computations
- Authentication checks
- File system operations
- Any non-fetch async work
Debugging Cache Hits/Misses
Add logging to see cache behavior:
import { cache } from 'react'
export const getUser = cache(async (id: string) => {
console.log(`[CACHE MISS] Fetching user ${id}`)
const user = await db.user.findUnique({ where: { id } })
return user
})
// Call multiple times
await getUser('123') // Logs: [CACHE MISS] Fetching user 123
await getUser('123') // No log (cache hit)
await getUser('456') // Logs: [CACHE MISS] Fetching user 456Cache Scope
Important: React.cache() is per-request only:
- ✅ Dedupe within single request/render
- ❌ Does NOT cache across requests
- ❌ Does NOT persist between renders
- ❌ Does NOT work like SWR/React Query
// Request 1
await getUser(1) // Query runs
await getUser(1) // Cache hit
// Request 2 (new user visits page)
await getUser(1) // Query runs again (new request)Related Patterns
- 2.1 Authenticate Server Actions - Cache auth checks
- 1.2 Parallelize Independent Operations - Combine with Promise.allSettled()
- 2.4 Parallel Data Fetching - Composition patterns
References
Authenticate Server Actions Like API Routes
Server Actions (functions with "use server") are exposed as public endpoints, just like API routes. Always verify authentication and authorization inside each Server Action—do not rely solely on middleware, layout guards, or page-level checks, as Server Actions can be invoked directly.
Next.js documentation explicitly states: "Treat Server Actions with the same security considerations as public-facing API endpoints, and verify if the user is allowed to perform a mutation."
Server Actions can be called:
- Directly from client components
- Via POST requests to the action endpoint
- From browser DevTools or custom scripts
- Bypassing page-level guards and middleware
The Pattern
❌ Incorrect: no authentication check
'use server'
export async function deleteUser(userId: string) {
// Anyone can call this! No auth check
await db.user.delete({ where: { id: userId } })
return { success: true }
}✅ Correct: authentication inside the action
'use server'
import { verifySession } from '@/lib/auth'
import { unauthorized } from '@/lib/errors'
export async function deleteUser(userId: string) {
// Always check auth inside the action
const session = await verifySession()
if (!session) {
throw unauthorized('Must be logged in')
}
// Check authorization too
if (session.user.role !== 'admin' && session.user.id !== userId) {
throw unauthorized('Cannot delete other users')
}
await db.user.delete({ where: { id: userId } })
return { success: true }
}✅ Correct: with input validation
'use server'
import { verifySession } from '@/lib/auth'
import { z } from 'zod'
const updateProfileSchema = z.object({
userId: z.string().uuid(),
name: z.string().min(1).max(100),
email: z.string().email()
})
export async function updateProfile(data: unknown) {
// Validate input first
const validated = updateProfileSchema.parse(data)
// Then authenticate
const session = await verifySession()
if (!session) {
throw new Error('Unauthorized')
}
// Then authorize
if (session.user.id !== validated.userId) {
throw new Error('Can only update own profile')
}
// Finally perform the mutation
await db.user.update({
where: { id: validated.userId },
data: {
name: validated.name,
email: validated.email
}
})
return { success: true }
}Security Checklist
Every Server Action should have:
1. Input validation - Use Zod or similar to validate all inputs 2. Authentication - Verify the user is logged in 3. Authorization - Check if the user is allowed to perform this action 4. Rate limiting - Consider adding rate limits for sensitive actions 5. Audit logging - Log security-relevant actions (use after() to avoid blocking)
Common Patterns
Pattern 1: Extract auth helper
// lib/auth.ts
export async function requireAuth() {
const session = await verifySession()
if (!session) {
throw new Error('Unauthorized')
}
return session
}
// Server Action
'use server'
import { requireAuth } from '@/lib/auth'
export async function updatePost(postId: string, content: string) {
const session = await requireAuth()
const post = await db.post.findUnique({ where: { id: postId } })
if (post.authorId !== session.user.id) {
throw new Error('Not authorized')
}
await db.post.update({ where: { id: postId }, data: { content } })
return { success: true }
}Pattern 2: Role-based access control
// lib/auth.ts
export async function requireRole(role: 'admin' | 'user') {
const session = await verifySession()
if (!session) {
throw new Error('Unauthorized')
}
if (session.user.role !== role && session.user.role !== 'admin') {
throw new Error('Insufficient permissions')
}
return session
}
// Server Action
'use server'
import { requireRole } from '@/lib/auth'
export async function banUser(userId: string) {
await requireRole('admin') // Only admins can ban
await db.user.update({ where: { id: userId }, data: { banned: true } })
return { success: true }
}Pattern 3: Resource ownership check
'use server'
import { requireAuth } from '@/lib/auth'
export async function deletePost(postId: string) {
const session = await requireAuth()
const post = await db.post.findUnique({ where: { id: postId } })
if (!post) {
throw new Error('Post not found')
}
// Check ownership or admin
if (post.authorId !== session.user.id && session.user.role !== 'admin') {
throw new Error('Not authorized to delete this post')
}
await db.post.delete({ where: { id: postId } })
return { success: true }
}References
3.2 Server vs Client Component
Server Components are the DEFAULT in Next.js App Router. All components are Server Components by default unless you add the 'use client' directive. DO NOT add 'use client' unless you specifically need client-side features.
Server Components provide significant benefits:
- Zero client-side JavaScript by default - reduces bundle size
- Direct database/API access - no need for API routes
- Secure handling of secrets - API keys never reach the client
- Automatic code splitting - only client components are bundled
- Better initial page load - HTML is rendered on the server
- SEO benefits - content is available immediately
Client Components increase bundle size, require hydration, and add runtime overhead. Only use them when you need client-side interactivity.
The Pattern
❌ Incorrect: unnecessary 'use client' directive
'use client'; // Unnecessary!
export function Header() {
return <header><h1>My App</h1></header>;
}✅ Correct: no directive needed
export function Header() {
return <header><h1>My App</h1></header>;
}Why: Only use 'use client' when you actually need client-side features. Static components should remain Server Components to reduce bundle size.
❌ Incorrect: server component in client component
'use client';
import { ServerComponent } from './server'; // This makes it a Client Component
export function ClientComponent() {
return <div><ServerComponent /></div>;
}✅ Correct: composition to preserve boundaries
// client.tsx
'use client'
export function ClientComponent({ children }) {
return <div>{children}</div>;
}
// page.tsx (Server Component)
import ClientComponent from './ClientComponent';
import ServerComponent from './ServerComponent';
export default function Page() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
);
}Why: Importing a Server Component into a Client Component converts it to a Client Component. Pass it as children or props instead.
Decision Tree
Need interactivity? (onClick, onChange, etc.)
├─ Yes → Client Component ('use client')
└─ No → Continue...
Need React hooks? (useState, useEffect, etc.)
├─ Yes → Client Component ('use client')
└─ No → Continue...
Need browser APIs? (window, localStorage, etc.)
├─ Yes → Client Component ('use client')
└─ No → Continue...
Need to fetch data?
├─ Yes → Server Component (default)
└─ No → Continue...
Need cookies/headers/searchParams?
├─ Yes → Server Component (default)
└─ No → Server Component (default, unless specific need)Common Mistakes
Mistake 1: Adding 'use client' unnecessarily
// ❌ Bad: static component with 'use client'
'use client'
export function Logo() {
return <img src="/logo.png" alt="Logo" />
}
// ✅ Good: Server Component by default
export function Logo() {
return <img src="/logo.png" alt="Logo" />
}Mistake 2: Making entire page client-side for one interactive element
// ❌ Bad: entire page is client-side
'use client'
export default function Page() {
const [count, setCount] = useState(0)
return (
<div>
<StaticHeader /> {/* Becomes client-side */}
<StaticContent /> {/* Becomes client-side */}
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
<StaticFooter /> {/* Becomes client-side */}
</div>
)
}
// ✅ Good: only interactive part is client-side
export default function Page() {
return (
<div>
<StaticHeader /> {/* Server Component */}
<StaticContent /> {/* Server Component */}
<Counter /> {/* Client Component */}
<StaticFooter /> {/* Server Component */}
</div>
)
}
// Counter.tsx
'use client'
function Counter() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
)
}Mistake 3: Importing Server Component into Client Component
// ❌ Bad: converts Server Component to Client Component
'use client'
import { DatabaseInfo } from './DatabaseInfo' // Server Component becomes Client
export function Dashboard() {
return <div><DatabaseInfo /></div>
}
// ✅ Good: pass as children
export default function Page() {
return (
<Dashboard>
<DatabaseInfo /> {/* Stays Server Component */}
</Dashboard>
)
}
'use client'
export function Dashboard({ children }) {
return <div>{children}</div>
}Related Patterns
- 2.3 Minimize Serialization at RSC Boundaries (pass only needed props to Client Components)
- 1.3 Strategic Suspense Boundaries (use with Server Components for streaming)
- 3.1 Avoid Barrel File Imports (especially important for Client Components)
References
1.3 Strategic Suspense Boundaries
Instead of awaiting data in async components before returning JSX, use Suspense boundaries to show the wrapper UI faster while data loads.
When you await data at the top level of a Server Component, the entire component tree waits before rendering. This blocks static content (headers, sidebars, footers) that could render immediately.
Suspense boundaries allow static UI to render while dynamic data loads, improving perceived performance.
Impact: Faster Time to First Byte (TTFB) and better user experience with progressive rendering.
❌ Incorrect: wrapper blocked by data fetching
async function Page() {
const data = await fetchData() // Blocks entire page
return (
<div>
<div>Sidebar</div>
<div>Header</div>
<div>
<DataDisplay data={data} />
</div>
<div>Footer</div>
</div>
)
}Problem: The entire layout (Sidebar, Header, Footer) waits for fetchData() even though only the middle section needs it.
✅ Correct: wrapper shows immediately, data streams in
function Page() {
return (
<div>
<div>Sidebar</div>
<div>Header</div>
<div>
<Suspense fallback={<Skeleton />}>
<DataDisplay />
</Suspense>
</div>
<div>Footer</div>
</div>
)
}
async function DataDisplay() {
const data = await fetchData() // Only blocks this component
return <div>{data.content}</div>
}Result: Sidebar, Header, and Footer render immediately. Only DataDisplay shows a skeleton while waiting for data.
Sharing Promises Across Components
When multiple components need the same data, start the fetch once and pass the promise to both:
✅ Correct: share promise across components
function Page() {
// Start fetch immediately, but don't await
const dataPromise = fetchData()
return (
<div>
<div>Sidebar</div>
<div>Header</div>
<Suspense fallback={<Skeleton />}>
<DataDisplay dataPromise={dataPromise} />
<DataSummary dataPromise={dataPromise} />
</Suspense>
<div>Footer</div>
</div>
)
}
function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
const data = use(dataPromise) // Unwraps the promise
return <div>{data.content}</div>
}
function DataSummary({ dataPromise }: { dataPromise: Promise<Data> }) {
const data = use(dataPromise) // Reuses the same promise
return <div>{data.summary}</div>
}Benefits:
- Only one fetch occurs (shared promise)
- Layout renders immediately
- Both components wait together in the same Suspense boundary
- Single skeleton for both components
Common Patterns
Pattern 1: Multiple Suspense Boundaries
function Page() {
return (
<div>
<Header />
<Suspense fallback={<PostsSkeleton />}>
<Posts />
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<Comments />
</Suspense>
<Footer />
</div>
)
}
async function Posts() {
const posts = await fetchPosts()
return <PostsList posts={posts} />
}
async function Comments() {
const comments = await fetchComments()
return <CommentsList comments={comments} />
}Result: Header and Footer render immediately, Posts and Comments stream in independently.
Pattern 2: Nested Suspense
function Page() {
return (
<Suspense fallback={<PageSkeleton />}>
<Dashboard />
</Suspense>
)
}
async function Dashboard() {
const user = await fetchUser()
return (
<div>
<UserHeader user={user} />
<Suspense fallback={<AnalyticsSkeleton />}>
<Analytics userId={user.id} />
</Suspense>
<Suspense fallback={<ActivitySkeleton />}>
<Activity userId={user.id} />
</Suspense>
</div>
)
}
async function Analytics({ userId }: { userId: string }) {
const analytics = await fetchAnalytics(userId)
return <AnalyticsDisplay data={analytics} />
}
async function Activity({ userId }: { userId: string }) {
const activity = await fetchActivity(userId)
return <ActivityDisplay data={activity} />
}Result: 1. Page skeleton shows immediately 2. After user loads, header shows with analytics/activity skeletons 3. Analytics and activity stream in independently
Pattern 3: Promise Passing for Parallel Data
function Page() {
// Start all fetches immediately
const userPromise = fetchUser()
const postsPromise = fetchPosts()
const commentsPromise = fetchComments()
return (
<div>
<Suspense fallback={<UserSkeleton />}>
<UserProfile userPromise={userPromise} />
</Suspense>
<Suspense fallback={<PostsSkeleton />}>
<PostsList postsPromise={postsPromise} />
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<CommentsList commentsPromise={commentsPromise} />
</Suspense>
</div>
)
}
async function UserProfile({ userPromise }: { userPromise: Promise<User> }) {
const user = use(userPromise)
return <div>{user.name}</div>
}
async function PostsList({ postsPromise }: { postsPromise: Promise<Post[]> }) {
const posts = use(postsPromise)
return <div>{posts.map(renderPost)}</div>
}
async function CommentsList({ commentsPromise }: { commentsPromise: Promise<Comment[]> }) {
const comments = use(commentsPromise)
return <div>{comments.map(renderComment)}</div>
}Benefits:
- All three fetches start immediately in parallel
- Each section streams in as its data becomes available
- No waterfall chains
The use() Hook
The use() hook unwraps promises in React components (React 19+):
import { use } from 'react'
function DataDisplay({ dataPromise }: { dataPromise: Promise<Data> }) {
const data = use(dataPromise)
return <div>{data.content}</div>
}Benefits:
- Suspends the component until the promise resolves
- Can be used conditionally (unlike hooks)
- Integrates with Suspense boundaries
Related Patterns
- 1.1 Prevent Waterfall Chains - Start fetches immediately
- 1.2 Parallelize Independent Operations - Use Promise.allSettled()
- 2.4 Parallel Data Fetching - Component composition
References
2.6 Use after() for Non-Blocking Operations
Use Next.js's after() to schedule work that should execute after a response is sent. This prevents logging, analytics, and other side effects from blocking the response.
❌ Incorrect: logging blocks the response
import { logUserAction } from '@/app/utils'
export async function POST(request: Request) {
// Perform mutation
await updateDatabase(request)
// Logging blocks the response
const userAgent = request.headers.get('user-agent') || 'unknown'
await logUserAction({ userAgent })
return new Response(JSON.stringify({ status: 'success' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
}✅ Correct: logging happens after response is sent
import { after } from 'next/server'
import { headers, cookies } from 'next/headers'
import { logUserAction } from '@/app/utils'
export async function POST(request: Request) {
// Perform mutation
await updateDatabase(request)
// Log after response is sent
after(async () => {
const userAgent = (await headers()).get('user-agent') || 'unknown'
const sessionCookie = (await cookies()).get('session-id')?.value || 'anonymous'
logUserAction({ sessionCookie, userAgent })
})
return new Response(JSON.stringify({ status: 'success' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
}The response is sent immediately while logging happens in the background.
Important Notes
after()runs even if the response fails or redirects- Works in Server Actions, Route Handlers, and Server Components
- Background tasks have access to request context (headers, cookies)
- Errors in
after()callbacks don't affect the response - Background tasks are subject to serverless function timeouts
Use Cases
Perfect for:
- Analytics tracking
- Audit logging
- Sending notifications (email, SMS, push)
- Cache invalidation
- Cleanup tasks
- Third-party API calls that don't affect the response
- Webhook deliveries
- Image processing and optimization
- Search index updates
NOT suitable for:
- Operations that affect the response data
- Critical error handling
- Operations that must complete before responding
- Data that the client needs immediately
Related Patterns
- 2.1 Authenticate Server Actions (use
after()for audit logging) - 1.1 Prevent Waterfall Chains (critical operations should still be parallelized)
- 2.5 Per-Request Deduplication (dedupe operations before scheduling them in after)
References
#!/bin/bash
# Check for Server Actions without authentication checks
set -Eeuo pipefail
DIR="${1:-.}"
echo "🔍 Checking for Server Actions without authentication..."
echo
# Find all files with 'use server'
mapfile -t FILES < <(grep -r -l "'use server'\|\"use server\"" "$DIR" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" 2>/dev/null || true)
if [[ ${#FILES[@]} -eq 0 ]]; then
echo "✅ No Server Actions found"
exit 0
fi
HAS_ISSUES=0
for file in "${FILES[@]}"; do
# Extract Server Action functions
while IFS= read -r line; do
# Check if function has auth check keywords
if ! grep -q -E "auth\(\)|verifySession\(\)|requireAuth\(\)|getCurrentUser\(\)|getSession\(\)" "$file"; then
echo "⚠️ $file"
echo " Missing authentication check"
echo
HAS_ISSUES=1
fi
done < <(grep -n "export.*async function" "$file")
done
if [ $HAS_ISSUES -eq 0 ]; then
echo "✅ All Server Actions have authentication checks"
else
echo
echo "❌ Found Server Actions without authentication"
echo
echo "Fix: Add authentication check inside each Server Action:"
echo " const user = await getCurrentUser()"
echo " if (!user) throw new Error('Unauthorized')"
exit 1
fi
#!/bin/bash
# Detect barrel file imports from large libraries
set -Eeuo pipefail
DIR="${1:-.}"
echo "🔍 Detecting barrel file imports..."
echo
# Common libraries with barrel file issues
LIBRARIES=(
"lucide-react"
"@mui/material"
"@mui/icons-material"
"react-icons"
"antd"
"@ant-design/icons"
)
HAS_ISSUES=0
for lib in "${LIBRARIES[@]}"; do
# Find imports from barrel files (not sub-paths)
MATCHES=$(grep -rn "from ['\"]$lib['\"]" "$DIR" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" 2>/dev/null || true)
if [ -n "$MATCHES" ]; then
echo "⚠️ Found barrel imports from $lib:"
echo "$MATCHES" | head -5
echo
HAS_ISSUES=1
fi
done
if [ $HAS_ISSUES -eq 0 ]; then
echo "✅ No barrel file imports detected"
else
echo
echo "❌ Found barrel file imports"
echo
echo "Fix examples:"
echo " ❌ import { Check } from 'lucide-react'"
echo " ✅ import Check from 'lucide-react/dist/esm/icons/check'"
echo
echo " ❌ import { Button } from '@mui/material'"
echo " ✅ import Button from '@mui/material/Button'"
echo
echo "Or use Next.js optimizePackageImports in next.config.js"
exit 1
fi
#!/bin/bash
# Find potential waterfall chains (sequential awaits)
set -Eeuo pipefail
DIR="${1:-.}"
echo "🔍 Finding potential waterfall chains..."
echo
# Find files with multiple awaits in sequence
mapfile -t FILES < <(find "$DIR" -type f \( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \) 2>/dev/null || true)
HAS_ISSUES=0
for file in "${FILES[@]}"; do
# Look for multiple awaits within 5 lines of each other
if grep -Pzo '(?s)await[^\n]*\n([^\n]*\n){0,5}await' "$file" > /dev/null 2>&1; then
echo "⚠️ $file"
echo " Multiple awaits in sequence (potential waterfall)"
# Show the problematic lines
grep -n "await" "$file" | head -10
echo
HAS_ISSUES=1
fi
done
if [ $HAS_ISSUES -eq 0 ]; then
echo "✅ No obvious waterfall chains detected"
else
echo
echo "❌ Found potential waterfall chains"
echo
echo "Fix: Start independent operations immediately:"
echo " ❌ const a = await fetchA()"
echo " const b = await fetchB()"
echo
echo " ✅ const aPromise = fetchA()"
echo " const bPromise = fetchB()"
echo " const [a, b] = await Promise.allSettled([aPromise, bPromise])"
exit 1
fi
Next.js Best Practices - Automation Scripts
This directory contains helper scripts to automatically detect common Next.js anti-patterns and optimization opportunities.
Available Scripts
check-server-actions-auth.sh
Validates that Server Actions include authentication checks.
Usage:
./scripts/check-server-actions-auth.sh [directory]What it checks:
- ❌ Server Actions without auth checks
- ✅ Server Actions with
auth(),getCurrentUser(),requireAuth(), etc.
Related pattern: 2.1 Authenticate Server Actions
Example:
# Check current directory
./scripts/check-server-actions-auth.sh
# Check specific directory
./scripts/check-server-actions-auth.sh app/actions
# Use in CI
./scripts/check-server-actions-auth.sh app && echo "Auth checks OK"---
detect-barrel-imports.sh
Finds barrel file imports from large libraries that cause slow dev server startup.
Usage:
./scripts/detect-barrel-imports.sh [directory]What it checks:
- ❌
import { Check } from 'lucide-react'(barrel import) - ❌
import { Button } from '@mui/material'(barrel import) - ✅
import Check from 'lucide-react/dist/esm/icons/check'(direct import) - ✅
import Button from '@mui/material/Button'(direct import)
Related pattern: 3.1 Avoid Barrel File Imports
Example:
# Check for barrel imports
./scripts/detect-barrel-imports.sh app
# Check before build
./scripts/detect-barrel-imports.sh . && npm run build---
find-waterfall-chains.sh
Detects potential waterfall chains where independent operations are awaited sequentially.
Usage:
./scripts/find-waterfall-chains.sh [directory]What it detects:
- Multiple
awaitstatements in sequence - Potential parallelization opportunities
Related pattern: 1.1 Prevent Waterfall Chains
Example:
# Find potential waterfalls
./scripts/find-waterfall-chains.sh app/api
# Review findings
./scripts/find-waterfall-chains.sh . | less---
Running All Checks
You can run all checks together:
#!/bin/bash
echo "Running Next.js best practices checks..."
./scripts/check-server-actions-auth.sh app
./scripts/detect-barrel-imports.sh app
./scripts/find-waterfall-chains.sh app
echo "All checks complete!"---
CI/CD Integration
GitHub Actions
name: Next.js Best Practices
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Check Server Actions authentication
run: ./scripts/check-server-actions-auth.sh app
- name: Check for barrel imports
run: ./scripts/detect-barrel-imports.sh app
- name: Check for waterfall chains
run: ./scripts/find-waterfall-chains.sh appPre-commit Hook
Add to .git/hooks/pre-commit:
#!/bin/bash
echo "Running Next.js best practices checks..."
if ! ./scripts/check-server-actions-auth.sh app; then
echo "❌ Server Actions auth check failed"
exit 1
fi
if ! ./scripts/detect-barrel-imports.sh app; then
echo "⚠️ Found barrel imports (consider fixing)"
# Don't fail commit, just warn
fi
echo "✅ Pre-commit checks passed"---
Limitations
These scripts use simple pattern matching and may produce:
- False positives - Code that looks like an anti-pattern but is actually fine
- False negatives - Missed issues due to complex code patterns
- Context-unaware - Scripts don't understand code semantics
Always manually review findings before making changes.
---
Adding New Scripts
When adding new automation scripts:
1. Name clearly - Use verb-noun format (e.g., check-auth.sh) 2. Add help text - Include usage and examples in script header 3. Use colors - Make output easy to scan (red=error, yellow=warning, green=success) 4. Exit codes - Return 0 for success, 1 for failures 5. Document here - Add to this README with usage examples
---
Related Resources
- Quick Checklist - Manual code review checklists
- AGENTS.md - Complete pattern reference
- Compound Patterns - Real-world examples
---
Notes
- These scripts are designed for Next.js App Router (13+)
- Adjust patterns if using Pages Router
- Scripts require
grep,find, and basic Unix tools - Works on macOS, Linux, and WSL
---
Last Updated: 2026-01-26 Next.js Version: App Router (13+)
Related skills
How it compares
Pick accelint-nextjs-best-practices for App Router server patterns; pair with accelint-react-best-practices for client-side React hook optimization.
FAQ
What Next.js versions does accelint-nextjs-best-practices cover?
accelint-nextjs-best-practices primarily covers the Next.js App Router from Next.js 13 onward, including Server Components, Server Actions, React.cache(), Suspense streaming, and Next-specific headers and cookies APIs.
When should agents activate accelint-nextjs-best-practices?
accelint-nextjs-best-practices activates when writing Server Components or Actions, optimizing data fetching, debugging RSC serialization issues, reviewing Next.js security, or choosing Server versus Client Component boundaries.
How does accelint-nextjs-best-practices differ from accelint-react-best-practices?
accelint-nextjs-best-practices focuses on Next.js-specific App Router optimizations like waterfalls, RSC serialization, and Server Actions auth, while accelint-react-best-practices covers general React hooks and memoization patterns.