你的 Next.js 应用在每次页面加载时执行相同的数据库查询 5 次

发布: (2026年3月24日 GMT+8 07:17)
10 分钟阅读
原文: Dev.to

Source: Dev.to

Your Next.js 应用每次页面加载进行相同的数据库查询 5 次的封面图

Aditya Kushwah

Source:

问题描述

打开你的 Next.js 应用。导航到任意包含多个组件的页面。现在统计一下下面这条 SQL 语句

SELECT * FROM users WHERE id = ?

执行了多少次。

你可能不知道。几乎没有人会去数。因为每个请求都会返回 200,每个组件都渲染正常,应用“工作”了。

但在这页正常渲染的背后,同样的查询可能已经执行了 5 次

  • 导航栏一次。
  • 侧边栏一次。
  • 主内容一次。
  • 设置面板一次。
  • 再一次,因为 React 严格模式让你的 effect 执行了两遍。

这就是 5 次相同的往返 到数据库,查询的数据在最近的 200 ms 内根本没有变化。

这并非假设

下面是一个典型的 Next.js API 路由:

// app/api/user/route.ts
export async function GET() {
  const user = await prisma.user.findUnique({
    where: { id: session.userId }
  });
  return NextResponse.json(user);
}

简洁、清晰、易于代码审查。

现在这个端点在多个地方被调用:

组件为什么会请求 /api/user
Layout 导航栏显示用户的名字
仪表盘页面展示用户统计信息
设置页面预填表单
个人资料侧边栏显示头像
通知铃铛检查偏好设置

每个组件各自调用 fetch('/api/user')。每一次调用都会命中 API 路由,进而执行 prisma.user.findUnique()。每个查询都会去 PostgreSQL,返回相同的用户记录。

五个组件 → 五次请求 → 五次查询 → 同一条用户记录。

为什么会发生这种情况

这并不是糟糕的代码;而是组件化架构的自然结果。

  • 在 React/Next.js 中,组件是自包含的。
    • fetch 自己获取数据。
    • 组件独立检查权限。

这是一种 良好 的软件设计,但它意味着每个需要用户数据的组件都会自行发起请求。重复的请求往往不被注意,因为:

  • DevTools 只显示一个平面列表。 你会看到 5 次对 /api/user 的请求,混杂在其他 20 条请求中。必须手动去发现它们是相同的。
  • 每个请求都成功。 200 OK,数据正确,速度足够快。没有错误,也没有警告。
  • 不同的开发者编写了不同的组件。 开发者 A 构建了导航栏,开发者 B 构建了侧边栏。彼此都不知道对方也在请求 /api/user
  • 代码审查没有捕捉到。 每个组件的代码在单独审视时都是正确的。重复只在所有组件同页面渲染时的运行时出现。

真实成本

“那又怎样?查询很快。”

让我们算一算:

指标
每页加载的重复查询5
每次查询耗时(热数据库)30 ms
每日活跃用户1,000
每次会话的页面加载次数10

每日不必要的查询50,000
每页浪费的延迟150 ms

如果你的数据库负载较高,那些“快速”的 30 ms 查询可能会变成 200 ms,导致应用在没有明显原因的情况下显得迟缓。

每一次重复查询还意味着一次额外的 HTTP 请求、一次额外的 API 路由调用、一次额外的连接池连接以及额外的内存分配——所有这些都是为你已经拥有的数据而产生的。

React 严格模式让情况更糟

React 严格模式(在 Next.js 中默认启用)在开发环境下会让每个 useEffect 运行两次。效果中的每个 fetch() 都会被触发两次。

你的 5 次重复请求会变成 10 次。你的 5 次数据库查询会变成 10 次。

令人困惑的地方在于:你会看到对 /api/user 的 10 次请求,进而觉得 “我的应用坏了”。 但其中有 5 次是严格模式的“幽灵”,在生产环境中根本不会发生,另外 5 次才是真正的重复请求。你在 DevTools 中无法分辨哪一个是哪一个。

常见建议方案

方案优点缺点
使用 React Query / SWR在客户端去重。表现良好。需要重构每个组件以使用相同的查询键。对 Server Components 没有帮助。
将获取提升到父组件通过 props 向下传递数据。打破组件封装。
使用共享布局加载器layout.tsx 中获取数据,通过 Context 共享。这是个好模式,但需要提前进行架构规划。
添加缓存(Redis、内存等)减少数据库负载。带来缓存失效问题;需要自行决定 TTL。

所有方案都有效,但它们要求你已经知道哪些查询是重复的——这才是难点。看不见的问题是无法解决的。

看到问题

在 API 级别的可观测性至关重要。既不是应用层日志(太冗长),也不是基础设施监控(层次太高)。你需要将 API 视为一个相互关联的系统——哪些端点被一起调用,哪些查询被共享,以及它们如何与用户实际的操作关联

Brakit 是一个开源开发工具,正是为此而生。只需在你的 Next.js 应用中添加一行代码,它就会捕获每个 HTTP 请求、数据库查询和外部 fetch——并按用户操作进行分组。

与在 DevTools 中看到的 10 条请求的平铺列表不同,你将看到所有内容以嵌套、计时和关联的方式呈现:

Brakit 仪表盘显示按用户操作分组的嵌套请求和查询

React/Next.js 中的重复查询

重复查询示意图

重复项会自动标记。跨多个端点运行的 SELECT users 被高亮显示。React Strict Mode 的重复项会单独标记 — “React Strict Mode duplicate — does not happen in production” — 这样你就能知道哪些需要修复,哪些可以忽略。

如何处理

一旦看到重复的请求,解决办法就很直接:

1. 在请求层面去重

使用 React QuerySWR,让多个组件共享同一个请求:

// hooks/useUser.ts
export function useUser() {
  return useQuery({
    queryKey: ['user'],
    queryFn: () => fetch('/api/user').then(r => r.json()),
    staleTime: 30_000,
  });
}

每个调用 useUser() 的组件现在都会共享同一个请求和缓存。

2. 在查询层面去重

将共享的查询抽取到数据访问层:

// lib/data/user.ts
import { cache } from 'react';

export const getUser = cache(async (userId: string) => {
  return prisma.user.findUnique({ where: { id: userId } });
});

React 的 cache() 会在单次服务器渲染中去重。若需要跨请求缓存,可使用 unstable_cache 或专门的缓存层。

3. 将共享数据移动到布局

一次获取后通过 context 共享:

// app/layout.tsx
export default async function Layout({ children }) {
  const user = await getUser(session.userId);
  return (
    <UserContext.Provider value={user}>
      {children}
    </UserContext.Provider>
  );
}

更广泛的模式

重复查询是基于组件的架构中最常见的后端性能问题。它们在代码审查中不可见,在单元测试中无法检测,只有在真实组件在真实页面上组合时才会在运行时显现。

解决办法并不复杂——难点在于看到问题本身。

如果你想了解你的 Next.js 应用在每次页面加载背后到底做了什么——哪些查询被执行,哪些被重复,以及是哪 个组件导致的——请尝试 Brakit。只需一条命令,零配置,你的重复查询将一目了然。

  • 开源,在本地运行,生产环境不做任何操作。
  • 你的数据永不离开你的机器。

2 分钟快速入门 →

0 浏览
Back to Blog

相关文章

阅读更多 »

不,Windows Start 并未使用 React

2026年3月23日 — Pat Hartl Windows 再次成为新闻焦点。这一次,Microsoft 发布了一份标准的企业《Our commitment to Windows quality》https://blogs.windo...