Next.js 高频开发技巧 Top 30
这些技巧来自 Next.js 15+ 项目的真实开发经验——不是文档搬运,而是那些"知道了能省半小时"的实用知识。按使用频率排序,每个技巧直接给出代码片段和适用场景。
1. 路由与渲染技巧
Tip 1:Route Group 组织布局
Route Group(括号目录)不影响 URL,只用来共享布局:
app/
(marketing)/ ← URL 不含 (marketing)
layout.tsx ← 公共营销布局(导航 + Footer)
page.tsx ← /
pricing/page.tsx ← /pricing
(dashboard)/ ← URL 不含 (dashboard)
layout.tsx ← 后台布局(Sidebar + Header)
projects/page.tsx ← /projects
settings/page.tsx ← /settings
(admin)/
layout.tsx ← 管理员布局
admin/page.tsx ← /admin
一个项目可以有多套完全不同的布局,URL 干净整洁。
Tip 2:Parallel Routes 实现 Modal
用 @slot 实现在不离开当前页面的情况下打开 Modal:
// app/(dashboard)/layout.tsx
export default function Layout({
children,
modal,
}: {
children: React.ReactNode
modal: React.ReactNode
}) {
return (
<>
{children}
{modal}
</>
)
}
// app/(dashboard)/@modal/create-project/page.tsx
export default function CreateProjectModal() {
return (
<Dialog defaultOpen>
<DialogContent>
<CreateProjectForm />
</DialogContent>
</Dialog>
)
}
// app/(dashboard)/@modal/default.tsx
export default function Default() {
return null // 不展示 modal 时返回 null
}URL 变为 /create-project,但页面不刷新,背景保留。
Tip 3:动态 Metadata
每个页面/布局都可以导出 generateMetadata:
// app/(dashboard)/projects/[id]/page.tsx
import type { Metadata } from 'next'
export async function generateMetadata({
params,
}: {
params: Promise<{ id: string }>
}): Promise<Metadata> {
const { id } = await params
const project = await getProject(id)
return {
title: `${project.name} | Dashboard`,
description: project.description,
openGraph: { title: project.name, images: [project.coverImage] },
}
}SEO 和社交分享自动搞定,不需要额外库。
Tip 4:Catch-all 路由
// app/docs/[...slug]/page.tsx ← 匹配 /docs/a, /docs/a/b, /docs/a/b/c
export default async function DocPage({
params,
}: {
params: Promise<{ slug: string[] }>
}) {
const { slug } = await params
// slug = ['a', 'b', 'c'] for /docs/a/b/c
const doc = await getDoc(slug.join('/'))
return <article>{doc.content}</article>
}Tip 5:路由拦截(Intercepting Routes)
app/
feed/
page.tsx ← 动态列表页
(..)photo/[id]/page.tsx ← 拦截:在 feed 页打开 photo modal
photo/[id]/
page.tsx ← 直接访问:完整 photo 页
从 feed 点击图片 → modal 预览;直接访问 /photo/123 → 完整页面。Instagram 模式。
Tip 6:notFound() 和 redirect()
import { notFound, redirect } from 'next/navigation'
export default async function ProjectPage({ params }: Props) {
const { id } = await params
const project = await getProject(id)
if (!project) notFound() // 自动渲染 not-found.tsx
if (project.isArchived) redirect('/projects') // 302 跳转
return <div>{project.name}</div>
}不用手动返回 404 响应或 <Navigate>。
Tip 7:loading.tsx 自动 Suspense
// app/(dashboard)/projects/loading.tsx
export default function Loading() {
return (
<div className="space-y-4">
<Skeleton className="h-8 w-48" />
<div className="grid grid-cols-3 gap-4">
{Array.from({ length: 6 }).map((_, i) => (
<Skeleton key={i} className="h-32" />
))}
</div>
</div>
)
}放在路由目录下,Next.js 自动用 <Suspense> 包裹 page.tsx。
2. 数据获取技巧
Tip 8:Server Component 直接查数据库
// app/(dashboard)/projects/page.tsx — Server Component,无 'use client'
import { db } from '@/lib/db'
import { projects } from '@/lib/db/schema'
export default async function ProjectsPage() {
const data = await db.select().from(projects).limit(20)
return <ProjectList projects={data} />
}不需要 API Route、不需要 useEffect、不需要 loading state——数据在服务端获取,HTML 直出。
Tip 9:并行数据获取
// ❌ 串行——总耗时 = 所有查询之和
const projects = await getProjects()
const stats = await getStats()
const members = await getMembers()
// ✅ 并行——总耗时 = 最慢的一个查询
const [projects, stats, members] = await Promise.all([
getProjects(),
getStats(),
getMembers(),
])简单改动,页面加载速度可能提升 2-3 倍。
Tip 10:Server Action 替代 API Route
// lib/actions/project.ts
'use server'
import { z } from 'zod'
import { revalidatePath } from 'next/cache'
const schema = z.object({ name: z.string().min(1).max(100) })
export async function createProject(formData: FormData) {
const parsed = schema.safeParse({ name: formData.get('name') })
if (!parsed.success) return { error: parsed.error.flatten().fieldErrors }
await db.insert(projects).values({ name: parsed.data.name, tenantId })
revalidatePath('/projects')
}// components/create-project-form.tsx
'use client'
export function CreateProjectForm() {
return (
<form action={createProject}>
<input name="name" required />
<button type="submit">Create</button>
</form>
)
}表单提交不需要 fetch、不需要 API Route、不需要 useState。
Tip 11:useActionState 管理表单状态
'use client'
import { useActionState } from 'react'
import { createProject } from '@/lib/actions/project'
export function CreateProjectForm() {
const [state, action, isPending] = useActionState(createProject, null)
return (
<form action={action}>
<input name="name" required disabled={isPending} />
{state?.error && <p className="text-red-500">{state.error}</p>}
<button disabled={isPending}>
{isPending ? 'Creating...' : 'Create'}
</button>
</form>
)
}Tip 12:revalidatePath vs revalidateTag
// 按路径重新验证——简单直接
revalidatePath('/projects') // 重新生成 /projects 页面
revalidatePath('/projects/[id]', 'page') // 重新生成特定页面
// 按标签重新验证——精细控制
// 获取数据时打标签
const projects = await fetch('/api/projects', { next: { tags: ['projects'] } })
// 变更时按标签失效
revalidateTag('projects') // 所有标记了 'projects' 的缓存都失效Tip 13:searchParams 驱动查询
// app/(dashboard)/projects/page.tsx
export default async function ProjectsPage({
searchParams,
}: {
searchParams: Promise<{ q?: string; page?: string; sort?: string }>
}) {
const { q, page = '1', sort = 'updated' } = await searchParams
const data = await getProjects({
search: q,
page: parseInt(page),
sort: sort as 'updated' | 'created' | 'name',
})
return (
<>
<SearchBar defaultValue={q} />
<ProjectList projects={data.items} />
<Pagination total={data.total} page={parseInt(page)} />
</>
)
}搜索、排序、分页全部用 URL 参数——可分享、可收藏、SSR 友好。
Tip 14:unstable_cache 缓存数据库查询
import { unstable_cache } from 'next/cache'
const getCachedStats = unstable_cache(
async (tenantId: string) => {
return db.select().from(stats).where(eq(stats.tenantId, tenantId))
},
['tenant-stats'], // 缓存 key
{ revalidate: 300, tags: ['stats'] } // 5 分钟过期
)3. 性能优化技巧
Tip 15:dynamic import 拆分大组件
import dynamic from 'next/dynamic'
const HeavyEditor = dynamic(() => import('@/components/rich-editor'), {
loading: () => <Skeleton className="h-64" />,
ssr: false, // 不在服务端渲染(编辑器通常依赖 DOM)
})编辑器、图表库、代码高亮器——这些大组件不需要首屏加载。
Tip 16:Image 组件自动优化
import Image from 'next/image'
<Image
src="/hero.png"
width={1200}
height={630}
alt="Hero"
priority // 首屏图片加 priority,预加载
placeholder="blur" // 模糊占位
blurDataURL="..." // base64 模糊图
/>
// 远程图片需要在 next.config.ts 配置域名
// images: { remotePatterns: [{ hostname: 'cdn.example.com' }] }自动生成 WebP/AVIF、自动响应式 srcset、自动懒加载。
Tip 17:部分预渲染(PPR)
// next.config.ts
export default { experimental: { ppr: true } }
// app/(dashboard)/page.tsx
import { Suspense } from 'react'
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1> {/* 静态 shell,预渲染 */}
<Suspense fallback={<Skeleton />}>
<DynamicStats /> {/* 动态部分,流式加载 */}
</Suspense>
</div>
)
}页面的静态外壳(导航、标题、布局)立即返回,动态数据流式填充。TTFB 极快。
Tip 18:useOptimistic 乐观更新
'use client'
import { useOptimistic } from 'react'
export function TodoList({ todos }: { todos: Todo[] }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo: Todo) => [...state, newTodo]
)
async function handleAdd(formData: FormData) {
const title = formData.get('title') as string
addOptimisticTodo({ id: 'temp', title, done: false }) // 立即在 UI 更新
await createTodo(formData) // 后台提交
}
return (
<form action={handleAdd}>
<input name="title" />
<ul>
{optimisticTodos.map(t => <li key={t.id}>{t.title}</li>)}
</ul>
</form>
)
}Tip 19:Streaming 和 Suspense 嵌套
export default function DashboardPage() {
return (
<div className="grid grid-cols-3 gap-4">
<Suspense fallback={<CardSkeleton />}>
<RevenueCard /> {/* 独立流式 */}
</Suspense>
<Suspense fallback={<CardSkeleton />}>
<UsersCard /> {/* 独立流式 */}
</Suspense>
<Suspense fallback={<CardSkeleton />}>
<ActivityCard /> {/* 独立流式 */}
</Suspense>
</div>
)
}三个卡片独立加载——谁先好谁先显示,不互相阻塞。
Tip 20:Link 预取
import Link from 'next/link'
// 默认预取(视口内自动预取)
<Link href="/projects">Projects</Link>
// 禁用预取(节省带宽)
<Link href="/heavy-page" prefetch={false}>Heavy Page</Link>4. TypeScript 与工具技巧
Tip 21:类型安全的 env 验证
// lib/env.ts
import { z } from 'zod'
const envSchema = z.object({
DATABASE_URL: z.string().url(),
CLERK_SECRET_KEY: z.string().min(1),
STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
NEXT_PUBLIC_APP_URL: z.string().url(),
})
export const env = envSchema.parse(process.env)
// 启动时如果缺少环境变量直接报错,不等到运行时Tip 22:类型安全的路由
// 在 next.config.ts 启用
export default { experimental: { typedRoutes: true } }
// 使用
import Link from 'next/link'
<Link href="/projects">OK</Link> // ✅
<Link href="/nonexistent">Bad</Link> // ❌ TypeScript 报错Tip 23:cn() 工具函数
// lib/utils.ts
import { clsx, type ClassValue } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
// 使用
<div className={cn(
'rounded-lg border p-4',
isActive && 'border-blue-500 bg-blue-50',
className // 允许外部覆盖
)} />Tip 24:Drizzle 类型推导
import { type InferSelectModel, type InferInsertModel } from 'drizzle-orm'
import { projects } from './schema'
type Project = InferSelectModel<typeof projects> // SELECT 结果类型
type NewProject = InferInsertModel<typeof projects> // INSERT 输入类型
// 在 Server Action 中使用
export async function createProject(data: NewProject) {
return db.insert(projects).values(data).returning()
}Tip 25:全局错误边界
// app/error.tsx
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<h2>Something went wrong</h2>
<p className="text-muted-foreground">{error.message}</p>
<button onClick={reset}>Try again</button>
</div>
)
}每个路由层级都可以有 error.tsx,错误不会让整个应用崩溃。
5. 调试与开发体验
Tip 26:Turbopack 开发加速
# next.config.ts 中无需额外配置,Next.js 15 默认使用 Turbopack dev
next dev --turbopack # 显式启用
# 效果:
# - 冷启动:从 10s+ 降至 1-2s
# - HMR:从 500ms+ 降至 <100msTip 27:Server Action 调试
'use server'
export async function createProject(formData: FormData) {
// 在终端打印(Server Action 是服务端代码)
console.log('FormData:', Object.fromEntries(formData))
// 模拟延迟测试 loading 状态
await new Promise(r => setTimeout(r, 2000))
// ...
}Server Action 的 console.log 输出在 终端(不是浏览器 Console)。
Tip 28:条件渲染 DevTools
// components/dev-only.tsx
export function DevOnly({ children }: { children: React.ReactNode }) {
if (process.env.NODE_ENV !== 'development') return null
return <>{children}</>
}
// 使用
<DevOnly>
<DebugPanel data={debugInfo} />
</DevOnly>生产构建时 DevOnly 内的内容会被 tree-shaking 移除。
Tip 29:中间件调试
// middleware.ts
import { NextResponse } from 'next/server'
export function middleware(request: NextRequest) {
// 中间件 console.log 输出在终端
console.log(`[MW] ${request.method} ${request.nextUrl.pathname}`)
// 添加自定义 header 帮助调试
const response = NextResponse.next()
response.headers.set('x-middleware-time', Date.now().toString())
return response
}Tip 30:next.config.ts 实用配置
// next.config.ts
import type { NextConfig } from 'next'
const config: NextConfig = {
// 严格模式(捕获 React 问题)
reactStrictMode: true,
// 输出独立文件(Docker 部署)
output: 'standalone',
// 忽略 lint 错误(CI 中单独跑 lint)
eslint: { ignoreDuringBuilds: true },
// 忽略 TS 错误(CI 中单独跑 tsc)
typescript: { ignoreBuildErrors: false },
// 重定向
async redirects() {
return [
{ source: '/old-path', destination: '/new-path', permanent: true },
]
},
// 自定义 headers
async headers() {
return [
{
source: '/(.*)',
headers: [
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
],
},
]
},
}
export default config本章小结
30 个技巧按类别:
| 类别 | 技巧 | 核心价值 |
|---|---|---|
| 路由 | Route Group, Parallel Routes, Intercepting, notFound/redirect, loading.tsx, Metadata, Catch-all | 灵活布局 + 自动化 |
| 数据 | SC 直查 DB, Promise.all, Server Action, useActionState, revalidate, searchParams, unstable_cache | 极简数据流 |
| 性能 | dynamic import, Image, PPR, useOptimistic, Streaming Suspense, Link prefetch | 秒开体验 |
| TS/工具 | env 验证, typedRoutes, cn(), Drizzle 类型, error.tsx | 类型安全 |
| 调试 | Turbopack, Action 调试, DevOnly, 中间件调试, next.config | 开发效率 |