Next.js 性能:当你拥有 200,000 条数据库记录
Source: Dev.to
大多数 Next.js 教程只教你如何用 10 篇文章来构建一个博客。真实的应用往往拥有数十万条记录。当你的数据库规模不再微小时,真正重要的因素就在这里。
问题
我最近在一个拥有 超过 200,000 条商品列表 的市场工作。教程和演示中的标准模式在这种规模下很快就会失效,因此下面的大部分内容都是我们在实践中摸索出来的,以保持系统的响应性。
数据库查询比 React 更重要
这听起来显而易见,但我经常看到它被忽视:你的数据库是瓶颈,而不是 React。
如果你的 Postgres 查询需要 3 秒,任何 React 优化都无济于事。先修复查询。
索引不是可选的
每个你用来过滤或排序的列都必须有索引。就这么简单。
// schema.prisma – add index for search
model Product {
id Int @id @default(autoincrement())
name String
@@index([name])
}
对于文本搜索,我们使用 Prisma 的过滤功能,底层是 PostgreSQL GIN trigram 索引。索引在迁移中创建,查询层由 Prisma 处理。
- 没有索引时:按名称搜索 20 万 行约需 4 秒。
- 有索引时:约 45 毫秒。
不要 在未建立索引的列上使用
contains,除非你喜欢看进度旋转器。
分页,而非无限滚动(通常情况下)
无限滚动很流行——但也是个陷阱。每次用户滚动时,你都会获取更多数据,保存在内存中,并重新渲染列表。大约 500 条数据后,浏览器会变慢,内存使用会激增。
改用 基于游标的分页:
// Get 20 products after this cursor
const products = await db.product.findMany({
take: 20,
skip: 1,
cursor: { id: lastProductId },
orderBy: { createdAt: 'desc' },
});
- 用户一次获取 20 条项目,可以向前或向后翻页,浏览器也不会因为持有 10 000 个 DOM 节点而崩溃。
服务器组件是你的朋友
我们发现最有效的模式是:
- 服务器组件 用于页面布局、标题、元数据、过滤器标签以及任何在每次请求中不变的静态内容。
- 客户端组件 用于产品网格,该网格会根据搜索词、过滤器、分页和排序而变化。
// app/products/page.tsx – Server Component
export default function ProductsPage() {
return (
Browse Cards
超过 200,000 张来自 Pokémon、MTG、Yu‑Gi‑Oh 等的卡牌。
// components/Page.tsx – Server Component
import ProductBrowser from '@/components/ProductBrowser';
export default function Page() {
return (
{/* Static content above renders server‑side immediately */}
{/* Client Component handles all dynamic stuff */}
);
}
// components/ProductBrowser.tsx – Client Component
'use client';
import { useState } from 'react';
import { useProducts } from '@/hooks/useProducts';
import { SearchBar } from '@/components/SearchBar';
import { FilterSidebar } from '@/components/FilterSidebar';
import { ProductGrid } from '@/components/ProductGrid';
import { Pagination } from '@/components/Pagination';
export function ProductBrowser() {
const [filters, setFilters] = useState(defaultFilters);
const { data, isLoading } = useProducts(filters);
return (
setFilters(f => ({ ...f, query: q }))} />
);
}
用户可以立即看到页面结构和静态内容,而产品列表则在加载中。此拆分还意味着服务器组件的输出可以被积极缓存(对每位访客都是相同的),只有客户端组件执行每次请求的工作。
避免 N+1 查询
经典错误:
// Bad: N+1 query
const products = await db.product.findMany();
for (const product of products) {
product.seller = await db.user.findUnique({
where: { id: product.sellerId },
});
}
你刚刚为产品执行了 1 次查询,然后为卖家执行了 N 次查询。对于 100 个产品来说,总共是 101 次往返。
更好:使用 include(或连接):
// Good: 1 query
const products = await db.product.findMany({
include: { seller: true },
});
使用 Prisma 时,include 在内部会执行一次连接——只需一次查询,速度快得多。
缓存策略
对于不经常变化的数据,进行缓存。项目使用 Redis 来:
- 搜索结果 – 缓存 5 分钟
- 卖家资料 – 缓存 1 小时
- 分类列表 – 缓存 1 天
import Redis from 'ioredis';
const redis = new Redis();
export async function getCachedProducts(category: string) {
const cacheKey = `products:${category}`;
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
const products = await db.product.findMany({
where: { category },
take: 20,
});
await redis.setex(cacheKey, 300, JSON.stringify(products)); // 5 min TTL
return products;
}
这将使重复访客的数据库负载降低 80 %+。
图像优化
拥有超过 20 万张产品图片时,即使单个文件很小,直接提供全分辨率 PNG 也会消耗大量带宽。Next.js 的 Image 组件会自动处理这些问题:
import Image from 'next/image';
Next.js 将会:
- 提供合适尺寸的 WebP/AVIF 变体。
- 对屏幕外的图片进行懒加载。
- 缓存并通过 CDN 优化资源。
TL;DR
- 对所有过滤/排序的字段建立索引。
- 使用分页,而非无限滚动。
- 利用 Server Components 生成静态标记。
- 通过
include/join 防止 N+1 查询。 - 缓存高读取频率的数据(Redis 表现优秀)。
- 让 Next.js 负责图像优化。
遵循这些模式,即使是 20 万行的数据集,也能像 10 篇博客文章一样响应迅速。
在支持的情况下提供 WebP/AVIF
- 将图像调整为适合显示尺寸
- 对折叠以下的图像进行懒加载
- 缓存已优化的版本
仅通过在所有地方使用
next/image,页面重量就从 2 MB 降至 400 KB。
对慢查询进行流式处理
有时查询本身就很慢(复杂的连接、聚合等)。与其阻塞整个页面,不如对慢的部分使用流式加载。
import { Suspense } from 'react';
export default function Page() {
return (
}>
);
}
async function SlowProductList() {
const products = await someSlowQuery();
return ;
}
页眉和页脚会立即渲染。产品列表在准备好后再流式显示,这样用户就能快速看到内容,而不是盯着空白页。
测量一切
不要猜测。测量。
我们是自托管的,所以使用:
- Prisma 查询日志 来捕获慢查询(应该更一致地使用此功能)
- Redis 监控 来跟踪缓存命中率(这也是我们应该正确设置的内容)
对于自托管的 Next.js 应用,你还可以:
- 使用 Prisma 的
log: ['query']选项来暴露所有慢查询 - 查看 Redis
INFO统计以获取命中/未命中比例 - 服务器端性能监控(New Relic、Datadog,或简单的 Express 中间件日志)
- 在部署流水线中加入 Lighthouse CI
如果页面加载缓慢,请检查:
- 数据库查询是否慢?(Prisma 日志 /
pg_stat_statements) - 是否缺少缓存?(Redis 命中率)
- 是否发送了过多的 JavaScript?(Next.js 包分析器)
通常是第 1 条。
实际上推动关键的因素
以下是产生最大性能提升的因素:
- Database indexes – 将查询时间从秒级降低到毫秒级
- Redis caching – 数据库访问减少了 80 %
- Server Components – 客户端 JavaScript 更少,首次渲染更快
- Image optimization – 页面重量降低了 5×
其余的影响微乎其微。专注这四点,你就没问题。
结论
大数据集会打破你在教程中学到的模式。解决方案并不复杂,但你必须以不同的方式思考数据流。
- 数据库优先
- 积极缓存
- 减少 JavaScript 交付
就这样。
构建了类似的东西,需要帮助扩展吗? 👉 morley.media
最初发布于 kira.morley.media