Next.js 性能:当你拥有 200,000 条数据库记录

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

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

如果页面加载缓慢,请检查:

  1. 数据库查询是否慢?(Prisma 日志 / pg_stat_statements
  2. 是否缺少缓存?(Redis 命中率)
  3. 是否发送了过多的 JavaScript?(Next.js 包分析器)

通常是第 1 条。

实际上推动关键的因素

以下是产生最大性能提升的因素:

  • Database indexes – 将查询时间从秒级降低到毫秒级
  • Redis caching – 数据库访问减少了 80 %
  • Server Components – 客户端 JavaScript 更少,首次渲染更快
  • Image optimization – 页面重量降低了 5×

其余的影响微乎其微。专注这四点,你就没问题。

结论

大数据集会打破你在教程中学到的模式。解决方案并不复杂,但你必须以不同的方式思考数据流。

  • 数据库优先
  • 积极缓存
  • 减少 JavaScript 交付

就这样。

构建了类似的东西,需要帮助扩展吗? 👉 morley.media

最初发布于 kira.morley.media

0 浏览
Back to Blog

相关文章

阅读更多 »