☰
FrameworkBenchmarks 实战解析:Next.js 15 基准测试的路由实现与 Kysely 数据层设计
2026/10/16 2:03:54 网站建设 项目流程
  • 性能测试
  • 测试
  • 后端
  • 开发者工具

【免费下载链接】FrameworkBenchmarks

Source for the TechEmpower Framework Benchmarks project

项目地址:https://gitcode.com/gh_mirrors/fr/FrameworkBenchmarks
点击查看免费下载

本篇文章以 frameworks/TypeScript/nextjs/README.md 为核心骨架,结合该目录下的全部源码与配置,系统讲解 TechEmpower FrameworkBenchmarks 项目中 Next.js 基准测试的完整实现:七个测试端点分别对应哪条路由、如何使用 Kysely + pg 访问 PostgreSQL、queries参数如何被解析与限幅,以及如何通过standalone输出模式与 Dockerfile 完成生产级构建部署。读完本文,你可以掌握这套基准测试的调用关系、数据层原理与部署细节,并能在本地或容器环境中复现验证。

从 README 出发:Next.js 基准测试的全景

nextjs/README.md是这份实现的官方入口文档,它用一张表格将 TechEmpower 的 7 类基准测试与具体的路由源码一一对应。所有源码均位于app/目录下,采用 Next.js App Router 的约定式文件路由:每个route.ts文件声明一个 HTTP 端点,每个page.tsx文件声明一个页面。

测试源码URL
JSON Serializationframeworks/TypeScript/nextjs/app/json/route.tshttp://localhost:3000/json
Single Database Queryframeworks/TypeScript/nextjs/app/db/route.tshttp://localhost:3000/db
Multiple Database Queriesframeworks/TypeScript/nextjs/app/queries/route.tshttp://localhost:3000/queries?queries=
Fortunesframeworks/TypeScript/nextjs/app/fortunes/page.tsxhttp://localhost:3000/fortunes
Database Updatesframeworks/TypeScript/nextjs/app/updates/route.tshttp://localhost:3000/updates?queries=
Plaintextframeworks/TypeScript/nextjs/app/plaintext/route.tshttp://localhost:3000/plaintext
Cachingframeworks/TypeScript/nextjs/app/cached-queries/route.tshttp://localhost:3000/cached-queries?queries=

这里有两个值得注意的细节:

  1. 除 Fortunes 外全部采用 Route Handler:json、db、queries、updates、plaintext、cached-queries都以route.ts形式返回裸响应,避免页面渲染开销;只有fortunes是一个真实渲染的 React 页面(page.tsx),这正是 Fortunes 测试要求"渲染 HTML 表格"的体现。
  2. queries参数为必填入口:Multi Query、Updates、Cached Queries 三类测试都以?queries=作为查询数量参数,服务端代码负责解析与限幅(详见下文)。

这些测试类型在 Toolset 测试框架中分别对应 toolset/test_types/ 下的json、db、query、fortune、update、plaintext、cached-query模块,benchmarker 会按 benchmark_config.json 中声明的 URL 向服务发起压测请求。

项目结构与技术栈

frameworks/TypeScript/nextjs/ ├── app/ │ ├── cached-queries/route.ts # Caching 端点 │ ├── db/route.ts # 单条数据库查询 │ ├── fortunes/page.tsx # Fortunes 页面 │ ├── json/route.ts # JSON 序列化 │ ├── plaintext/route.ts # Plaintext 端点 │ ├── queries/route.ts # 多条数据库查询 │ ├── updates/route.ts # 数据库更新 │ ├── layout.tsx # 根布局 │ └── page.tsx # 首页(空实现) ├── lib/ │ ├── db.ts # Kysely 数据访问层 │ └── schema.ts # 表结构类型定义 ├── benchmark_config.json # 基准测试声明 ├── next.config.ts # Next.js 构建配置 ├── nextjs.dockerfile # Docker 构建镜像 ├── package.json / package-lock.json └── tsconfig.json

从 package.json 可以看到其技术栈版本:

  • Next.js 15.3.3+React 19.2.3(服务端组件默认模式)
  • Kysely 0.28.14:类型安全的 SQL 查询构建器(ORM 分类为 "Micro")
  • pg 8.16.3:PostgreSQL 原生驱动,提供连接池
  • TypeScript 5.x,构建脚本使用 Turbopack(next build --turbopack、next dev --turbopack)

tsconfig.json配置了@/*路径别名映射到项目根目录,因此所有源码中@/lib/db的导入都解析到 lib/db.ts。

数据访问层:Kysely + pg 连接 PostgreSQL

表结构类型定义

lib/schema.ts 用 Kysely 的泛型工具类型声明了两张表的完整类型体系:

import { Generated, Insertable, Selectable, Updateable } from "kysely" export interface Database { World: WorldTable Fortune: FortuneTable } export interface WorldTable { id: Generated<number> randomnumber: number } export type WorldRow = Selectable<WorldTable> export type NewWorld = Insertable<WorldTable> export type WorldUpdate = Updateable<WorldTable> export interface FortuneTable { id: Generated<number> message: string }
  • Generated<number>表示id由数据库生成;
  • Selectable / Insertable / Updateable分别派生出行、插入、更新三种形态的类型,供查询构建器在编译期做类型检查。

这里需要注意的是,World表的数值列在数据库中名为randomnumber(见 toolset/databases/postgres/create-postgres.sql 中的CREATE TABLE World (id integer NOT NULL, randomNumber integer NOT NULL default 0, ...),PostgreSQL 会将其折叠为小写)。

连接池与查询封装

lib/db.ts 是全部路由共享的数据访问核心:

import { Kysely, PostgresDialect } from "kysely" import { Pool } from "pg" import { Database, WorldRow } from "./schema.js" export const db = new Kysely<Database>({ dialect: new PostgresDialect({ pool: new Pool({ connectionString: process.env.DATABASE_URL }), }), }) export type World = { [key in keyof WorldRow as key extends "randomnumber" ? "randomNumber" : key]: WorldRow[key] } export async function findWorld(id: number): Promise<World | undefined> { return db.selectFrom("World"). where("id", "=", id). select(["id", "randomnumber as randomNumber"]). executeTakeFirst() } export async function upsertWorlds(worlds: World[]) { const values = worlds.map(world => ({ id: world.id, randomnumber: world.randomNumber })) return db.insertInto("World").values(values).onConflict(oc => oc.column("id").doUpdateSet({ randomnumber: eb => eb.ref("excluded.randomnumber") }) ).execute() }

三个关键设计:

  1. 连接池全局共享:db单例基于pg.Pool创建,连接串来自环境变量DATABASE_URL(Dockerfile 中注入,见下文)。所有路由共用该实例,避免压测时反复建连。
  2. 字段名映射:World类型通过映射类型把数据库列randomnumber映射为 JSON 输出所需的randomNumber,配合SELECT "randomnumber" as "randomNumber"完成"数据库蛇形列名 → API 驼峰字段名"的转换。
  3. UPSERT 实现:upsertWorlds使用 PostgreSQL 的ON CONFLICT (id) DO UPDATE SET randomnumber = excluded.randomnumber语法,先按 id 冲突则原地更新randomnumber,这是 Updates 测试的写路径核心。

底层数据表

基准数据库由 toolset/databases/postgres/create-postgres.sql 初始化:World表预置 10000 行(id1–10000,randomnumber随机 1–10000),Fortune表预置 12 条经典格言,其中包含一条 XSS 测试数据<script>alert(...)</script>与一条日文数据,用于验证页面渲染时的转义与多字节处理。该 SQL 同时创建了不带引号(大小写折叠)与带引号(保留大小写)两套表,适配不同 ORM 的命名习惯。

七个测试端点的实现详解

1. JSON Serialization:纯 JSON 序列化

app/json/route.ts 是最简单的端点,只返回固定 JSON,不访问数据库:

export async function GET() { return Response.json({ message: "Hello, World!" }) }

Response.json由运行时内置,避免引入额外序列化库;该测试衡量框架本身的 JSON 序列化吞吐能力。

2. Single Database Query:单条随机查询

app/db/route.ts 在 1–10000 中随机取一个 id,查询对应World行并返回 JSON:

import { findWorld } from "@/lib/db" export async function GET() { const id = 1 + Math.floor(Math.random() * 10000) return Response.json(await findWorld(id)) }

随机 id 的生成方式与其余端点保持一致(1 + Math.floor(Math.random() * 10000),落在闭区间 [1, 10000]),保证每次请求命中World表中不同行,从而测出真实的主键查询性能。

3. Multiple Database Queries:多查询并发

app/queries/route.ts 通过queries查询参数控制查询条数:

import { findWorld, World } from "@/lib/db" import { NextRequest } from "next/server" export async function GET(request: NextRequest) { const queriesParam = request.nextUrl.searchParams.get("queries") const queriesCount = Math.min(Math.max(Number(queriesParam) || 1, 1), 500) const promises = Array<Promise<World | undefined>>(queriesCount) for (let i = 0; i < queriesCount; i += 1) { const id = 1 + Math.floor(Math.random() * 10000) promises[i] = findWorld(id) } return Response.json(await Promise.all(promises)) }

实现要点:

  • 参数限幅:Math.min(Math.max(Number(queriesParam) || 1, 1), 500)将合法值钳制在 1–500 之间;非数字、缺省值统一回落为 1。这与 Toolset 测试端(toolset/test_types/query/query.py)声明的查询范围约定一致。
  • 并发查询:先构造queriesCount个 Promise,再用Promise.all并发执行,让 N 条主键查询在连接池上并行,而不是串行等待。

4. Fortunes:服务端渲染 HTML 表格

app/fortunes/page.tsx 是唯一一个页面端点,完整实现了 Fortunes 测试的四个步骤——读取全部格言、追加一条新数据、按消息排序、渲染表格:

import { db } from "@/lib/db" // Prevent database queries during build phase. export const dynamic = "force-dynamic" export default async function Page() { const fortunes = await db.selectFrom("Fortune").selectAll().execute() fortunes.push({ id: 0, message: "Additional fortune added at request time." }) fortunes.sort((a, b) => a.message.localeCompare(b.message)) return <> <title>Fortunes</title> <table> <thead> <tr> <th>id</th> <th>message</th> </tr> </thead> <tbody> {fortunes.map(fortune => <tr key={fortune.id}> <td>{fortune.id}</td> <td>{fortune.message}</td> </tr> )} </tbody> </table> </> }

要点说明:

  • export const dynamic = "force-dynamic":这是本项目的一个重要细节。它强制该路由在请求时动态渲染,避免 Next.js 在构建阶段(next build)预取执行db.selectFrom("Fortune"),否则构建期无数据库连接会导致构建失败或产生被缓存死的页面。
  • 排序规则:使用String.prototype.localeCompare按消息文本排序(追加条目id: 0,排在常规数据之后参与比较)。
  • XSS 与多语言:数据库中预置的<script>alert(...)与日文格言(见create-postgres.sql第 11–12 行)由 React 服务端渲染自动转义与正确编码,这也是该测试验证的要点之一。

5. Database Updates:读取 + 更新

app/updates/route.ts 综合了"随机查询多条 → 修改 randomNumber → UPSERT 写回 → 返回新值"的完整链路:

import { db, findWorld, upsertWorlds, World } from "@/lib/db" import { NextRequest } from "next/server" export async function GET(request: NextRequest) { const queriesParam = request.nextUrl.searchParams.get("queries") const queriesCount = Math.min(Math.max(Number(queriesParam) || 1, 1), 500) const ids = new Set<number>() while (ids.size < queriesCount) { ids.add(1 + Math.floor(Math.random() * 10000)) } const promises = new Array<Promise<World | undefined>>() for (const id of ids) { promises.push(findWorld(id)) } const results = await Promise.all(promises) as World[] for (const result of results) { result.randomNumber = 1 + Math.floor(Math.random() * 10000) } await upsertWorlds(results) return Response.json(results) }

与 queries 端点相比,Updates 有两个明显差异:

  • id 去重:先用Set<number>收集随机 id,保证同一请求内不会对同一行做重复查询/更新,这是 TechEmpower Updates 测试的既定约束;
  • 写回数据库:所有行更新randomNumber后一次性交给upsertWorlds,通过ON CONFLICT ... DO UPDATE SET批量写回(底层实现在 lib/db.ts)。

6. Plaintext:纯文本响应

app/plaintext/route.ts 返回text/plain的固定字符串,不经过数据库与序列化:

export function GET() { return new Response("Hello, World!") }

它衡量的是框架最底层"裸响应"的吞吐上限,因此这里刻意使用最轻量的new Response()而不调用Response.json。

7. Caching:基于 unstable_cache 的查询缓存

app/cached-queries/route.ts 是 Next.js 原生缓存能力在基准测试中的体现:

import { findWorld as uncached_findWorld, World } from "@/lib/db" import { unstable_cache } from "next/cache" import { NextRequest } from "next/server" const findWorld = unstable_cache(uncached_findWorld) export async function GET(request: NextRequest) { const queriesParam = request.nextUrl.searchParams.get("queries") const queriesCount = Math.min(Math.max(Number(queriesParam) || 1, 1), 500) const results = Array<World | undefined>(queriesCount) for (let i = 0; i < queriesCount; i += 1) { const id = 1 + Math.floor(Math.random() * 10000) results[i] = await findWorld(id) } return Response.json(results) }

实现方式是用next/cache提供的unstable_cache包装findWorld,使数据库查询结果被框架级缓存层拦截;由于同一请求内随机 id 可能重复(此处未去重),命中缓存的概率与缓存复用效果会直接影响该测试的成绩。注意这里与updates端点不同:Caching 测试只读不写,因此直接串行 await 缓存包装后的查询即可。

基准测试声明:benchmark_config.json 逐项解读

benchmark_config.json 是 Toolset 调度器读取的测试清单:

{ "framework": "nextjs", "tests": [ { "default": { "display_name": "Next.js", "versus": "nodejs", "classification": "Platform", "language": "TypeScript", "platform": "nodejs", "framework": "nextjs", "os": "Linux", "webserver": "None", "database": "postgres", "database_os": "Linux", "orm": "Micro", "approach": "Realistic", "notes": "", "port": 3000, "json_url": "/json", "db_url": "/db", "query_url": "/queries?queries=", "fortune_url": "/fortunes", "update_url": "/updates?queries=", "plaintext_url": "/plaintext", "cached_query_url": "/cached-queries?queries=" } } ] }

字段含义与实现事实对应如下:

  • classification: "Platform"、platform: "nodejs":Next.js 在此被归类为构建于 Node.js 之上的平台型框架,与versus: "nodejs"一起用于结果分组与横向对比;
  • webserver: "None":不套独立 Web 服务器,直接以 Next.js standalone 服务对外监听;
  • database: "postgres":目标数据库为 PostgreSQL,连接信息由环境变量DATABASE_URL提供;
  • orm: "Micro":对应 Kysely 这类轻量查询构建器,而非全量 ORM;
  • approach: "Realistic":表示实现尽量贴近真实生产用法(使用框架缓存、连接池、类型安全查询等);
  • port: 3000与七条*_url:声明了监听端口和全部测试路径,Toolset 会据此拼接完整 URL 发起压测,并通过 toolset/test_types/ 下各测试类型脚本校验响应(例如fortune.py要求fortune_url至少 9 个字符、verifications.py 中的verify_queries_count会核对queries参数请求返回的行数)。

构建与部署:standalone 输出与 Docker 镜像

输出模式与响应头

next.config.ts 做了两项关键配置:

import type { NextConfig } from "next"; const nextConfig: NextConfig = { output: "standalone", async headers() { return [ { source: "/(.*?)", headers: [ { key: "Server", value: "Next.js" }, ], }, ] }, }; export default nextConfig;
  • output: "standalone":启用 Next.js 独立输出模式。构建产物/.next/standalone/内会生成一个自包含的server.js,连同最小化的node_modules一起,无需next start即可直接以node server.js启动,这对容器部署与镜像瘦身非常关键;
  • 自定义Server响应头:为所有路由(/(.*?)通配)统一注入Server: Next.js头,满足基准测试对响应头的要求,也便于压测工具识别框架。

Docker 构建流程

nextjs.dockerfile 完整呈现了从依赖安装到产物装配的构建步骤:

FROM node:24-slim ENV NEXT_TELEMETRY_DISABLED="1" ENV DATABASE_URL="postgres://benchmarkdbuser:benchmarkdbpass@tfb-database/hello_world" EXPOSE 3000 WORKDIR /nextjs COPY package.json package-lock.json ./ RUN npm ci COPY ./ ./ RUN npm run build \ && cp -r public .next/standalone/ \ && cp -r .next/static .next/standalone/.next/ ENV NODE_ENV="production" CMD ["node", ".next/standalone/server.js"]

逐步解读:

  1. 基础镜像node:24-slim,关闭遥测(NEXT_TELEMETRY_DISABLED=1);
  2. 注入数据库连接串:DATABASE_URL指向tfb-database主机上的hello_world库(用户benchmarkdbuser),与 toolset/databases/postgres/create-postgres.sql 中GRANT ALL PRIVILEGES ON World to benchmarkdbuser的授权对象一致;该主机名由 Toolset 的 Docker 网络在压测时解析到数据库容器;
  3. 先装依赖再拷贝源码:先COPY package.json package-lock.json并npm ci(按 lockfile 精确安装,适合可复现构建),再COPY ./ ./,利用 Docker 层缓存加快迭代构建;
  4. 构建并装配 standalone 产物:npm run build(Turbopack 构建)后,把public/与.next/static复制进.next/standalone/,补齐独立服务所需的静态资源;
  5. 生产启动:NODE_ENV=production,CMD直接执行node .next/standalone/server.js,监听 3000 端口(与benchmark_config.json的port: 3000对应)。

本地运行方式

如需在本地复现该基准服务,可按 README 中的 URL 约定自行验证:

# 1. 安装依赖 npm ci # 2. 注入数据库连接(指向本地 PostgreSQL,表结构见 create-postgres.sql) export DATABASE_URL="postgres://benchmarkdbuser:benchmarkdbpass@localhost/hello_world" # 3. 构建并启动 npm run build npm start # 4. 访问测试端点 curl http://localhost:3000/json curl "http://localhost:3000/queries?queries=10" curl http://localhost:3000/fortunes

注意DATABASE_URL必须指向已按 create-postgres.sql 初始化过World/Fortune表的数据库,否则各数据库相关端点会直接报错;fortunes页面因声明了force-dynamic,无需担心构建期预取问题。

关键实现要点小结

从源码结构看,这套 Next.js 基准实现可以提炼出四个值得借鉴的设计:

  1. 单例数据层:lib/db.ts以模块级单例暴露 Kysely 实例与连接池,七个路由共享,避免压测下的连接风暴;
  2. 统一参数规约:所有带queries参数的端点使用同一套钳制表达式(Math.min(Math.max(Number(param) || 1, 1), 500)),保证不同测试类型的行为一致且符合 Toolset 预期;
  3. 区分动态与缓存:fortunes用force-dynamic强制请求时渲染,cached-queries用unstable_cache主动缓存查询,体现"该动态的动态、该缓存的缓存";
  4. 构建即部署:output: "standalone"+ Dockerfile 手动装配静态资源,让node .next/standalone/server.js成为唯一运行入口,为容器环境提供最小运行面。

如果你希望深入某个测试类型的校验逻辑,可以继续阅读 toolset/test_types/ 下对应模块与 toolset/test_types/verifications.py;若想了解 Next.js 之外的其他框架如何实现同一套测试,可对比 frameworks/TypeScript/ 目录下其他实现的目录结构。

  • 性能测试
  • 测试
  • 后端
  • 开发者工具

【免费下载链接】FrameworkBenchmarks

Source for the TechEmpower Framework Benchmarks project

项目地址:https://gitcode.com/gh_mirrors/fr/FrameworkBenchmarks
点击查看免费下载

相关推荐

上一篇:yuzu Switch 模拟器快速上手指南:免费在 PC 上运行 Switch 游戏的完整教程
下一篇:IBM Granite-4.0-H-Small:32B参数企业级大模型如何重塑AI应用范式

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询