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 响应或 &lt;Navigate&gt;

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 自动用 &lt;Suspense&gt; 包裹 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>
  )
}

三个卡片独立加载——谁先好谁先显示,不互相阻塞。

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+ 降至 <100ms

Tip 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开发效率