N

Next.js 可以处理 5000 个页面吗?

2025-06-08

Next.js 可以处理 5000 个页面吗?

我想分享一个实验,它将Next.js 的 13 个SSR 和 SSG 功能发挥到极致。我构建了一个包含 5000 个 SSR 页面的网站,以测试 Next.js 在本地和生产环境中的性能。我在 AWS Amplify 服务团队工作,我想使用我们的托管服务来构建和部署这些页面,并确保它能够处理大量的页面和图片。使用静态参数和动态参数有什么区别?如果您需要快速构建时间,哪一个是最佳选择?如果您不关心构建时间,但想要一个极速的网站,哪一个是最佳选择?让我们来一探究竟。

在本文结束时,您将看到我如何生成 5000 个测试图像和记录,将它们上传以使用AWS Amplify传送到Amazon S3Dynamo DB,构建一个 Next.js 应用程序来获取数据和图像,将它们呈现在屏幕上,部署到 Amplify Hosting 并记录性能影响和差异。

获取并上传 5000 条数据

TL;DR:如果您不想遵循本节概述的流程,您可以点击这些链接访问我的5000 条记录5000 张图像

如何找到 5000 张图片?
我面临的第一个挑战是为每个页面找到 5000 张图片。我没有依赖Kaggle等平台的数据集,而是采用了一种更直接的方法。我从Unsplash下载了 10 张图片,并将它们保存在名为 的本地文件夹中5k_src

如果你用过 Unsplash,你应该会记得这些原始图片通常分辨率高,体积也很大,经常超过 1MB。我对每张图片都进行了优化,以减小其文件大小。请确保你选择的文件夹中有 10 张优化后的图片。

积累了 5000 张图片后,我编写了一个脚本来复制这些图片,直到总数达到 5000 张。对我来说,每张图片是否唯一并不重要,所以我决定使用 10 张不同的图片并进行复制,直到达到 5000 张。要使用这个脚本,我需要为这 10 张图片创建一个 src 文件夹,为剩下的 5000 张图片创建一个目标文件夹。创建一个名为 的文件夹demo,并向其中添加两个文件夹:5k_dest5k_src

切换到demo文件夹并在终端中运行此 CLI 脚本来复制图像:

dest="5K_dest"
src="5K_src"
for i in {1..500}; do cp "$src/b.jpg" "$dest/b$i.jpg"; done
Enter fullscreen mode Exit fullscreen mode

该脚本设置了一个名为 的目标文件夹,5K_dest用于存储所有复制的图像。它还指定了一个名为 的源文件夹5K_src,用于存储原始图像。然后,它会循环运行 500 次。

每次迭代都会从源文件夹复制名为 b.jpg 的图像,并以唯一名称将副本保存到目标文件夹中。新名称包含一个数字(i 的值),该数字会随着每次迭代而递增,最终生成的图像名称依次为“b1.jpg”、“b2.jpg”等,直至“b500.jpg”。

我使用这种方法来确保每张图片都有唯一的文件名。通过对每张图片运行此脚本,我总共得到了 5000 张图片。要获取这 5000 张重复的图片,您可以对5k_src文件夹中的每张图片运行该脚本。

如何找到 5k 记录?

接下来,我需要生成一个随机记录列表。我使用了一个名为Mocaroo的工具,它允许免费账户一次最多生成 1000 行记录。

访问Mocaroo后,我清除了所有默认数据并添加了新字段,如下图所示。

添加新字段后,我将“行”字段设置为 1000,这是 Mockaroo 可以生成的最大值。设置完成后,我点击了 5 次“生成数据”,生成了 5 个 csv 文件,每个文件包含 5000 条记录。

现在,为了合并这 5 个 csv 文件,我不得不求助于 Google 表格。如果您正在跟着操作,可以按照以下步骤操作:

  1. 打开 Google 表格。
  2. 创建一个新文件。
  3. 点击左上角的“文件”。
  4. 点击“导入”并选择“上传”并选择第一个csv文件。
  5. 第一个 csv 文件上传完成后,重复相同的步骤,但当出现合并的弹出选项时,将其余文件导入同一张表并附加到表的末尾。

导入完成后,我使用“自动填充”功能为每条记录添加了图片名称。由于图片是连续的,所以这样做很方便。

如何将 5k 图像上传到云端?

接下来我需要将这 5000 张图片上传到云端,具体来说是 Amazon Simple Storage Service (Amazon S3),并且我必须先创建一个 AWS Amplify 项目。为此,请按照以下步骤操作:

  • 导航到您的 AWS 控制台并搜索AWS Amplify
  • 选择AWS Amplify以打开 Amplify 控制台。
  • 在右上角,选择“新应用程序” ,然后从下拉菜单中选择“构建应用程序” 。

为应用程序命名(我将它命名为 5kpages)并单击“确认部署”进行部署。

部署完成后,单击启动工作室按钮打开 Amplify 工作室。

接下来我需要创建一个存储实例来存储图片。然而,在此之前,我需要设置身份验证。

要继续设置身份验证,请单击“设置”按钮。您可以保留所有默认选择,因为我们不会对此应用使用身份验证;它仅在使用存储时才需要。继续,然后单击“部署”按钮,确认警告,然后选择“确认部署”

身份验证部署过程大约需要一两分钟。完成后,您将看到一条确认消息,表明身份验证已成功部署。

设置身份验证后,我设置了存储并创建了一个新的 S3 存储桶。为此,请在屏幕左侧的设置菜单中选择“存储”选项。

在授权设置中,确保登录用户拥有上传、查看和删除文件的权限,而访客用户只能查看和删除文件。最后,点击创建存储桶按钮。

要查看存储桶,您可以返回 Amplify 控制台。搜索 s3 并选择它。

我把我的存储桶命名为5kpages。选择你的存储桶来打开它。

要将图像上传到存储桶,请单击“上传”按钮。

要将 5k 张图像上传到 S3 存储桶,我将文件夹拖放5k_dest到页面上,然后单击“上传”按钮,如下所示。

上传所有图像需要一些时间。

上传完成后,返回 Amplify Studio,选择侧面菜单中的“文件浏览器”选项。在公共文件夹中,您将找到5k_dest包含全部 5,000 张图片的文件夹。您可以浏览页面来查看这些图片。

如何将 5000 条记录上传到云端?
上传完图片后,下一步就是将这 5000 条记录上传到 Amazon DynamoDB。具体步骤如下:

  1. 返回您的 Amplify 控制台。
  2. 从侧面菜单中选择数据。
  3. 点击“ +添加模型”按钮。
  4. 填写下图所示的字段。
  5. 填写字段后,单击“保存并部署”按钮。

创建模型后,转到 Amplify 控制台并搜索 DynamoDB。单击侧面菜单上的“表”,您将找到一个产品数量为 0 的产品表。复制表名,然后转到终端创建一个 Node 应用程序。

在创建 Node 应用之前,我需要5kproducts.csv从 Google 表格下载记录文件。
要下载文件,请按照以下步骤操作:

  1. 打开包含 5000 条记录的 Google 表格。
  2. 点击左上角的“文件”。
  3. 选择“下载”,然后选择“下载为 CSV”。

下载文件后,创建一个新文件夹,并为其命名5kdyno或任何其他您喜欢的名称。将下载的 CSV 文件放入此文件夹中。接下来,package.json在该文件夹中创建一个文件,并向其中添加以下内容:

{
  "name": "5kdyno",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@aws-sdk/client-dynamodb": "^3.423.0",
    "@aws-sdk/lib-dynamodb": "^3.423.0",
    "aws-sdk": "^2.1468.0",
    "csv-parse": "^5.5.0",
    "csv-reader": "^1.0.12",
    "uuid": "^9.0.1"
  },
  "type": "module"
}
Enter fullscreen mode Exit fullscreen mode

在终端中运行以下命令来安装这些依赖项:

npm install
Enter fullscreen mode Exit fullscreen mode

在文件夹中创建一个index.js文件并向其中添加以下内容:

import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { PutCommand, DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
import { v4 } from "uuid";

import { parse } from "csv-parse";
import { createReadStream } from "fs";

const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);

export const main = async () => {
  const parser = createReadStream("5kproducts.csv").pipe(parse());
  for await (const row of parser) {
    console.log(row);
    const command = new PutCommand({
      TableName: "Product-f6gl4tj2gzffyfynoogu-staging",
      Item: {
        id: v4(),
        createdAt: new Date().toJSON(),
        desription: row[1],
        img: row[6],
        name: row[0],
        price: row[4],
        quantity: row[2],
        size: row[3],
        updatedAt: new Date().toJSON(),
        __typename: "Product",
      },
    });
    const response = await docClient.send(command);
    console.log(response);
  }
};
main();
Enter fullscreen mode Exit fullscreen mode

您可能注意到的第一件事是,我没有向 Dynamo DB 客户端传递任何凭证。您可以查看此AWS 文档,了解如何为您的计算机设置凭证。

接下来我创建了一个DynamoDBClient实例,并使用该实例创建了DynamoDBDocumentClient。该客户端提供了一个用于操作 DynamoDB 的更高级别的接口。

接下来,我声明了一个名为 的异步函数main,并设置了一个流来5kproducts.csv逐行读取文件,并将该流传输到 CSV 解析器。之后,我循环遍历 CSV 文件的每一行,并将每一行都记录到控制台。

PutCommand使用 csv 文件中的行创建对 DynamoDB 表的写入操作。最后,我使用客户端将写入操作发送到 Dynamo DB

不要忘记用您创建的表的名称替换 TableName。

现在,如果您打开终端并运行命令node index.js,您将看到它正在写入指定的表。

这需要几分钟才能完成,完成后,返回 DynamoDB 并刷新页面。点击“浏览表项”,您就能看到数据库中的项目。

现在,如果您返回 Amplify 并单击侧面菜单中的“内容”部分,您将能够看到传入的内容。

在浏览器上渲染数据

将图片和记录上传到云端后,接下来要做的就是在网站上显示产品列表。为此,我创建了一个 Next.js 应用。我没有从头开始,而是创建了一个包含 Tailwind 配置和其他设置的启动项目。你可以在终端中运行以下Degit命令来克隆它:

npx degit christiannwamba/5kpages#starter
Enter fullscreen mode Exit fullscreen mode

启动项目包含三个组件和一个ui/button组件。其中一个组件是ProductList,它接收items,循环遍历并渲染它们。在ProductList组件中,S3Image组件用于从 S3 获取图像。此外,该项目还包含一个Pagination管理分页的组件。

之后,运行以下命令安装依赖项:

npm install
Enter fullscreen mode Exit fullscreen mode

接下来,我配置了Amplify,以便访问我们上传的数据和图片。具体操作如下:返回 Amplify Studio,复制显示的 pull 命令。

从您的项目目录中,将复制的命令粘贴到终端,然后执行您复制的命令。

运行该命令后,您将被重定向到 Web 浏览器以授予 CLI 访问权限。到达那里后,点击“是”以向 Amplify Studio 进行身份验证。

之后,返回 CLI。在这里,您将被问到一系列问题,以收集有关项目配置的基本详细信息。接受下图中突出显示的默认值:

接下来,我配置了 Next.js,使其能够识别图像的源域。为此,请将以下内容添加到next.config.js文件中:

/** @type {import('next').NextConfig} */

const nextConfig = {
  images: {
    domains: [
      "5kproducts-storage-dd7c40fc142146-staging.s3.us-east-1.amazonaws.com",
    ],
  },
};

module.exports = nextConfig;
Enter fullscreen mode Exit fullscreen mode

不要忘记domains用从 s3 存储桶复制的那个来替换。

一旦我设置了一个 Amplify 项目,我就需要生成可用于与我们的数据交互的 GraphQL 操作的代码。

要生成这些操作,请在项目根目录下运行以下命令:

amplify add codegen
Enter fullscreen mode Exit fullscreen mode

接受下图中突出显示的默认值:

此命令将生成 GraphQL 操作并将其保存在graphql目录中src/graphql。生成的操作可以导入到您的组件中,以便与您的 API 无缝交互。

要获取并呈现产品列表,请将以下内容添加到您的app/page.js文件中:

import { API } from "aws-amplify";

import Pagination from "@/components/Pagination";
import ProductList from "@/components/ProductList";
import * as queries from "../src/graphql/queries";

async function fetchData(nextToken, prevToken, action) {
  const variables = {
    limit: 10,
  };
  if (action == "next" && nextToken) variables.nextToken = nextToken;
  if (action == "prev" && prevToken) variables.nextToken = prevToken;

  const allProducts = await API.graphql({
    query: queries.listProducts,
    variables,
  });

  return allProducts.data.listProducts;
}

async function Home({ searchParams }) {
  const nextToken = searchParams.nextToken;
  const prevToken = nextToken;
  const action = searchParams.action;

  const products = await fetchData(nextToken, prevToken, action);

  return (
    <div className="w-[800px] mx-auto py-24">
      <h1 className="text-2xl text-center pb-8">Products</h1>
      <ProductList items={products.items} />
      <Pagination nextToken={products.nextToken} prevToken={prevToken} />
    </div>
  );
}

export default Home;
Enter fullscreen mode Exit fullscreen mode

在此文件中,我导入了几个内容。首先,我从 AWS Amplify 库导入API 。这允许我与 GraphQL API 进行交互。我还导入了PaginationProductList组件,它们是 React 组件,分别用于显示产品列表和提供分页功能。此外,我还查询文件导入了查询../src/graphql/

接下来,我定义了一个名为 的异步函数,fetchData该函数根据提供的参数获取产品数据。在该函数中,我定义了一个名为 的对象,variableslimit值为 10,表示我希望一次获取 10 个产品。

然后,我根据提供的操作和令牌修改分页变量。如果操作是next并且nextToken提供了 ,variables.nextToken则将 的值设置为nextToken,这将获取下一组产品。如果操作是prev并且prevToken提供了 ,则使用prevToken来获取上一组产品。

queries.listProducts然后,该函数使用查询和变量进行 GraphQL 调用来获取产品数据。最后,它返回获取到的产品列表。

接下来,我定义了一个Home组件,它将作为在主页上显示产品列表的主要 React 组件。该组件是异步的,并从 中检索nextTokenprevToken为了 获取所需的产品列表,该组件使用这些参数调用函数。actionsearchParams
fetchData

返回的 JSX 包括页面标题、传递给组件的获取产品列表ProductList以及Pagination用于处理分页控制的组件。

现在,如果您打开浏览器,就会看到显示的产品。点击“下一步”按钮,您可以查看更多产品;点击“上一步”按钮,则会显示上一组产品。

使用动态参数渲染动态页面
我想探索动态参数和静态生成的页面,并比较性能差异。

对于动态参数,我们需要接收 id 作为参数。这样,​​我们就可以开始构建页面来呈现单个产品项了。

在文件夹中创建一个名为“ [id]in your folder”的新文件夹app。在该[id]文件夹中,创建一个page.js文件并向其中添加以下代码:

import { API } from "aws-amplify";
import * as queries from "@/src/graphql/queries";
import S3Image from "@/components/S3Image";
import Link from "next/link";
import { Button } from "@/components/ui/button";

async function Product({ params }) {
  console.log(params);
  const variables = {
    id: params.id,
  };
  const res = await API.graphql({
    query: queries.getProduct,
    variables,
  });

  const product = res.data.getProduct;
  return (
    <div className=" p-4 w-1/2">
      <div className="flex items-center mb-4">
        <div className="h-96 w-80 bg-slate-400 relative">
          <S3Image imageName={product.img} />
        </div>
        <div className="ml-4">
          <p className="text-lg font-semibold pb-2 text-slate-800">
            ${product.price}
          </p>
          <p className="text-xs">{product.quantity} left</p>
          <h3 className="text-lg">{product.name}</h3>
          <div>{product.desription}</div>

          <Link href={`/`} className="mt-8 block">
            <Button className="w-full">Go back</Button>
          </Link>
        </div>
      </div>
    </div>
  );
}

export default Product;
export const revalidate = 60 * 50;
Enter fullscreen mode Exit fullscreen mode

该文件以几个导入开始。API导入来自 AWS Amplify 库,用于与 GraphQL API 交互。查询导入用于获取产品详情。S3Image导入用于显示存储在 Amazon S3 中的图片。来自next/linkLink导入用于客户端路由之间的转换。最后,导入Button 组件用于 UI 功能

接下来,我定义一个名为 的异步组件Product。它首先定义一个变量对象,然后使用idfromparams来设置variables。这id用于查询特定产品。

queries.getProduct接下来,该函数使用查询和 进行GraphQL 调用variables。响应包含产品数据,该数据被提取并存储在product常量中。

最后,返回的 JSX 包含每个产品的以下内容:使用组件的产品图像S3Image、产品价格、库存数量、产品名称、产品描述以及单击后将用户导航回主页的按钮。

我还定义了一个名为的常量revalidate,用于指定 Next.js 每隔多久(50 分钟)重新检查页面上的新数据。重新验证 Next.js 非常重要,以确保您的响应不会被缓存太久。如果您的内容需要频繁更改,那么缓存可能会对保持内容的新鲜度带来挑战。

在此演示中,缓存的主要问题与使用 Amazon S3 存储图片有关。S3 不会直接提供公共 URL。即使它提供了公共 URL,它也使用会过期的令牌进行签名。一旦令牌过期,您将无法再使用相同的 URL 访问图片,必须请求新的令牌。

这意味着,如果 Next.js 缓存了你的响应并保存了 URL,它将无法知道该图片现已失效。当你尝试渲染该图片时,它将不会显示任何内容。除非你要求 Next.js 刷新其缓存并向存储发出新的 URL 请求。

为了解决这个问题,我将 S3 镜像的过期时间设置为 1 小时,并将 Next.js 配置为 50 分钟后重新验证镜像。这样,Next.js 会在 S3 镜像过期前发出获取请求,使缓存失效并显示更新后的页面。

使用静态参数渲染动态页面
到目前为止,我们已经了解了如何使用动态参数实现动态渲染。现在,让我们探索如何使用静态参数实现动态渲染。

为此,请转到代码编辑器并将所做的更改提交到主分支。要创建新分支,请在终端中运行以下命令:

git checkout -b static
Enter fullscreen mode Exit fullscreen mode

打开app/[id]/page.js并将以下代码添加到文件底部:

export async function generateStaticParams() {
  const variables = {
    limit: 5000,
  };

  const allProducts = await API.graphql({
    query: queries.listProducts,
    variables,
  });
  const items = allProducts.data.listProducts.items;

  return items.map((item) => ({
    id: item.id,
  }));
}
Enter fullscreen mode Exit fullscreen mode

此函数使用 GraphQL 查询获取所有产品(共 5000 个)的列表queries.listProducts。然后,它会映射产品列表并提取 ID。
返回的 ID 数组将在 Next.js 构建时用于静态生成,为每个产品 ID 创建一个预渲染页面。

如果您访问浏览器,刷新页面并随机点击项目,您会注意到加载速度显著提高,这是静态生成实施的结果。

本地建设

现在让我们总结一下迄今为止所做的工作,并比较动态参数和静态参数的构建时间。

要切换回主分支,请在终端中运行以下命令:

git checkout main
Enter fullscreen mode Exit fullscreen mode

在终端中运行以下命令来查看动态参数的构建时间:

npm run build
Enter fullscreen mode Exit fullscreen mode

如下图所示,构建过程仅生成了5个静态页面,耗时12秒完成。

我们对静态参数也做同样的操作。在终端中运行以下命令:

git checkout static
npm run build
Enter fullscreen mode Exit fullscreen mode

你应该注意到它正在迭代并尝试构建每个页面。在下图中,你可以看到构建时间耗时 3 分钟,相当于 180 秒。

生产中的构建

下一步是部署到生产环境并进行测试。返回代码编辑器并发布静态分支。完成后,也发布主分支。

前往您的 AWS 控制台,搜索AWS Amplify,并从服务列表中选择它。接下来,选择5kpages应用。

选择托管环境

在下一页上选择 GitHub 或您的 Git 提供商,然后单击“连接分支”按钮。

选择您打算托管的存储库,选择main动态参数的分支,然后单击下一步按钮继续。

在构建设置页面中,选择一个环境或创建一个新环境。如果您已有服务角色,请选择一个;或者,点击“创建新角色”按钮创建一个允许 Amplify Hosting 访问您资源的新角色。完成选择后,点击“下一步”按钮继续。

查看您的存储库详细信息应用程序设置,然后单击“保存并部署”。

开发和用户体验洞察

在构建和部署应用程序的静态和动态版本时,我一直在跟踪一些数字。我想看看在考虑以下问题时是否有一些有用的见解:

  1. 构建时间有多快?
  2. 访问者感知到的加载速度是多少?

开发经验

构建时间有多快?

测量构建时间很简单——iTerm(本地)和 AWS Amplify(生产环境)在构建过程中都有时间戳,我只需减去构建时间即可。下表表明,在构建时,动态参数比静态生成的页面更快。

构建时间(秒) 部署时间(秒)
动态参数 12 58
静态参数 180 240

(部署时间仅用于构建 Next.js 应用程序,不包括配置和后端构建时间。)

构建和部署时间差异的原因很明显——如果我希望静态生成,Next.js 必须构建 5000 个页面中的每一个。

动态开发,静态发布

generateStaticParams我偶然发现一个没人提过的策略:在非生产环境中测试时不要调用。等待 5000 个页面在临时服务器上构建完成,对开发者来说是一个痛苦的经历。

我的建议是仅在生产中启用,generateStaticParams因为您几乎不会频繁地进行生产。

用户体验

访问者感知到的加载速度是多少?

为了深入了解静态网站和动态网站的区别,我用PageSpeed Insights对网站进行了分析。以下是主页、动态参数页面和静态生成页面的服务器和缓存速度指数日志。

为了便于理解,我分析了Next.js Showcase中的 6 个网站,平均速度指数为 2.6 秒。

主页(秒) 动态参数(秒) 静态参数(秒)
速度指数(服务器) 3.7 2.9 2.4
速度指数(缓存) 3.1 2.1 1.6

(服务器速度指标为部署成功后立即采集,缓存速度指标为再次分析采集)

我的第一个观察是,主页比动态页面相对较慢,因为它们有更多的图像需要渲染。

正如预期的那样,静态生成的页面加载速度比动态参数加载速度快 1.2 倍。在实际场景中,页面会比我的演示包含更多文本、字体和图片。所以,对 1.2 倍的理解要谨慎一些,因为在这种情况下,差异肯定会更加显著。

其他用户体验指标

速度指数能提供全面的视角,但在衡量网站性能时,这还不够。一篇名为《什么是速度》的文章非常精彩,它能帮助您了解速度的原理以及需要考虑的因素。考虑到这一点,我决定将 PageSpeed Insights 中的所有图片都转储出来,以便您深入了解。

来自服务器的主页:

来自服务器的动态参数页面:

从服务器静态生成的页面:

来自缓存的主页:

来自缓存的动态参数页面:

来自缓存的静态参数页面:

清理

为确保您的 AWS 账户中没有任何未使用的资源,请运行以下命令删除此项目中创建的所有资源(如果您不打算保留它们)。

amplify delete
Enter fullscreen mode Exit fullscreen mode

我的观点

这对我来说是一次有见地的实验,它得出了一些有趣的结论,将指导我在接下来的几个月里使用 Next.js。

为了我的客户体验:除了动态页面内容频繁更改的情况外,我将始终默认在生产环境中使用静态生成的页面。此外,Amplify Hosting 完全符合我的预期,所有页面的构建都没有任何错误,也没有额外的延迟。

就我的开发经验而言:在处理动态页面时,我会忽略静态页面生成。由于 Next.js 的设计是generateStaticParams可插拔的,所以事后再考虑这一点也没问题。如果您想了解更多关于 Amplify 托管服务的信息,这里有一个入门指南。

鏂囩珷鏉ユ簮锛�https://dev.to/codebeast/can-nextjs-handle-5000-pages-1ejn
PREV
适用于 Git 和 GitHub 的 VS Code
NEXT
我如何克服写作恐惧