Skip to content

Commit ce36b84

Browse files
Merge pull request #9506 from jonathanhefner/benchmark-nextjs
Benchmark Next.js
2 parents 51288cb + b92239a commit ce36b84

23 files changed

+1576
-0
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
*.dockerfile
2+
.dockerignore
3+
node_modules
4+
npm-debug.log
5+
README.md
6+
.next
7+
.git
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.*
7+
.yarn/*
8+
!.yarn/patches
9+
!.yarn/plugins
10+
!.yarn/releases
11+
!.yarn/versions
12+
13+
# testing
14+
/coverage
15+
16+
# next.js
17+
/.next/
18+
/out/
19+
20+
# production
21+
/build
22+
23+
# misc
24+
.DS_Store
25+
*.pem
26+
27+
# debug
28+
npm-debug.log*
29+
yarn-debug.log*
30+
yarn-error.log*
31+
.pnpm-debug.log*
32+
33+
# env files (can opt-in for committing if needed)
34+
.env*
35+
36+
# vercel
37+
.vercel
38+
39+
# typescript
40+
*.tsbuildinfo
41+
next-env.d.ts
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Next.js Benchmarking Test
2+
3+
## Test source files and URLs
4+
5+
| Test | Source Code | URL |
6+
| --- | --- | --- |
7+
| [JSON Serialization][] | [`app/json/route.ts`][] | http://localhost:3000/json |
8+
| [Single Database Query][] | [`app/db/route.ts`][] | http://localhost:3000/db |
9+
| [Multiple Database Queries][] | [`app/queries/route.ts`][] | http://localhost:3000/queries?queries= |
10+
| [Fortunes][] | [`app/fortunes/page.tsx`][] | http://localhost:3000/fortunes |
11+
| [Database Updates][] | [`app/updates/route.ts`][] | http://localhost:3000/updates?queries= |
12+
| [Plaintext][] | [`app/plaintext/route.ts`][] | http://localhost:3000/plaintext |
13+
| [Caching][] | [`app/cached-queries/route.ts`][] | http://localhost:3000/cached-queries?queries= |
14+
15+
[JSON Serialization]: https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview#json-serialization
16+
[Single Database Query]: https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview#single-database-query
17+
[Multiple Database Queries]: https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview#multiple-database-queries
18+
[Fortunes]: https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview#fortunes
19+
[Database Updates]: https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview#database-updates
20+
[Plaintext]: https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview#plaintext
21+
[Caching]: https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview#caching
22+
23+
[`app/json/route.ts`]: ./app/json/route.ts
24+
[`app/db/route.ts`]: ./app/db/route.ts
25+
[`app/queries/route.ts`]: ./app/queries/route.ts
26+
[`app/fortunes/page.tsx`]: ./app/fortunes/page.tsx
27+
[`app/updates/route.ts`]: ./app/updates/route.ts
28+
[`app/plaintext/route.ts`]: ./app/plaintext/route.ts
29+
[`app/cached-queries/route.ts`]: ./app/cached-queries/route.ts
30+
31+
## TODO
32+
33+
The Fortunes test is currently disabled because the benchmark expects exact HTML output — see [TechEmpower/FrameworkBenchmarks#9505](https://github.com/TechEmpower/FrameworkBenchmarks/pull/9505). After that issue is resolved, the Fortunes test can be re-enabled by applying the following diff:
34+
35+
```diff
36+
--- a/frameworks/TypeScript/nextjs/benchmark_config.json
37+
+++ b/frameworks/TypeScript/nextjs/benchmark_config.json
38+
@@ -20,7 +20,7 @@
39+
"json_url": "/json",
40+
"db_url": "/db",
41+
"query_url": "/queries?queries=",
42+
- "TEMPORARILY DISABLED fortune_url": "/fortunes",
43+
+ "fortune_url": "/fortunes",
44+
"update_url": "/updates?queries=",
45+
"plaintext_url": "/plaintext",
46+
"cached_query_url": "/cached-queries?queries="
47+
```
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { findWorld as uncached_findWorld, World } from "@/lib/db"
2+
import { unstable_cache } from "next/cache"
3+
import { NextRequest } from "next/server"
4+
5+
const findWorld = unstable_cache(uncached_findWorld)
6+
7+
export async function GET(request: NextRequest) {
8+
const queriesParam = request.nextUrl.searchParams.get("queries")
9+
const queriesCount = Math.min(Math.max(Number(queriesParam) || 1, 1), 500)
10+
const results = Array<World | undefined>(queriesCount)
11+
12+
for (let i = 0; i < queriesCount; i += 1) {
13+
const id = 1 + Math.floor(Math.random() * 10000)
14+
results[i] = await findWorld(id)
15+
}
16+
17+
return Response.json(results)
18+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { findWorld } from "@/lib/db"
2+
3+
export async function GET() {
4+
const id = 1 + Math.floor(Math.random() * 10000)
5+
return Response.json(await findWorld(id))
6+
}
25.3 KB
Binary file not shown.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { db } from "@/lib/db"
2+
3+
// Prevent database queries during build phase.
4+
export const dynamic = "force-dynamic"
5+
6+
export default async function Page() {
7+
const fortunes = await db.selectFrom("Fortune").selectAll().execute()
8+
fortunes.push({ id: 0, message: "Additional fortune added at request time." })
9+
fortunes.sort((a, b) => a.message.localeCompare(b.message))
10+
11+
return <>
12+
<title>Fortunes</title>
13+
14+
<table>
15+
<thead>
16+
<tr>
17+
<th>id</th>
18+
<th>message</th>
19+
</tr>
20+
</thead>
21+
<tbody>
22+
{fortunes.map(fortune =>
23+
<tr key={fortune.id}>
24+
<td>{fortune.id}</td>
25+
<td>{fortune.message}</td>
26+
</tr>
27+
)}
28+
</tbody>
29+
</table>
30+
</>
31+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export async function GET() {
2+
return Response.json({ message: "Hello, World!" })
3+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export default function RootLayout({
2+
children,
3+
}: Readonly<{
4+
children: React.ReactNode;
5+
}>) {
6+
return (
7+
<html lang="en">
8+
<body>
9+
{children}
10+
</body>
11+
</html>
12+
);
13+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Home() {
2+
return
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function GET() {
2+
return new Response("Hello, World!")
3+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { findWorld, World } from "@/lib/db"
2+
import { NextRequest } from "next/server"
3+
4+
export async function GET(request: NextRequest) {
5+
const queriesParam = request.nextUrl.searchParams.get("queries")
6+
const queriesCount = Math.min(Math.max(Number(queriesParam) || 1, 1), 500)
7+
const promises = Array<Promise<World | undefined>>(queriesCount)
8+
9+
for (let i = 0; i < queriesCount; i += 1) {
10+
const id = 1 + Math.floor(Math.random() * 10000)
11+
promises[i] = findWorld(id)
12+
}
13+
14+
return Response.json(await Promise.all(promises))
15+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { db, findWorld, upsertWorlds, World } from "@/lib/db"
2+
import { NextRequest } from "next/server"
3+
4+
export async function GET(request: NextRequest) {
5+
const queriesParam = request.nextUrl.searchParams.get("queries")
6+
const queriesCount = Math.min(Math.max(Number(queriesParam) || 1, 1), 500)
7+
8+
const ids = new Set<number>()
9+
while (ids.size < queriesCount) {
10+
ids.add(1 + Math.floor(Math.random() * 10000))
11+
}
12+
13+
const promises = new Array<Promise<World | undefined>>()
14+
for (const id of ids) {
15+
promises.push(findWorld(id))
16+
}
17+
18+
const results = await Promise.all(promises) as World[]
19+
for (const result of results) {
20+
result.randomNumber = 1 + Math.floor(Math.random() * 10000)
21+
}
22+
23+
await upsertWorlds(results)
24+
25+
return Response.json(results)
26+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"framework": "nextjs",
3+
"tests": [
4+
{
5+
"default": {
6+
"display_name": "Next.js",
7+
"versus": "nodejs",
8+
"classification": "Platform",
9+
"language": "TypeScript",
10+
"platform": "nodejs",
11+
"framework": "nextjs",
12+
"os": "Linux",
13+
"webserver": "None",
14+
"database": "postgres",
15+
"database_os": "Linux",
16+
"orm": "Micro",
17+
"approach": "Realistic",
18+
"notes": "",
19+
"port": 3000,
20+
"json_url": "/json",
21+
"db_url": "/db",
22+
"query_url": "/queries?queries=",
23+
"TEMPORARILY DISABLED fortune_url": "/fortunes",
24+
"update_url": "/updates?queries=",
25+
"plaintext_url": "/plaintext",
26+
"cached_query_url": "/cached-queries?queries="
27+
}
28+
}
29+
]
30+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Kysely, PostgresDialect } from "kysely"
2+
import { Pool } from "pg"
3+
import { Database, WorldRow } from "./schema.js"
4+
5+
export const db = new Kysely<Database>({
6+
dialect: new PostgresDialect({
7+
pool: new Pool({ connectionString: process.env.DATABASE_URL }),
8+
}),
9+
})
10+
11+
export type World = {
12+
[key in keyof WorldRow as key extends "randomnumber" ? "randomNumber" : key]: WorldRow[key]
13+
}
14+
15+
export async function findWorld(id: number): Promise<World | undefined> {
16+
return db.selectFrom("World").
17+
where("id", "=", id).
18+
select(["id", "randomnumber as randomNumber"]).
19+
executeTakeFirst()
20+
}
21+
22+
export async function upsertWorlds(worlds: World[]) {
23+
const values = worlds.map(world => ({ id: world.id, randomnumber: world.randomNumber }))
24+
return db.insertInto("World").values(values).onConflict(oc =>
25+
oc.column("id").doUpdateSet({ randomnumber: eb => eb.ref("excluded.randomnumber") })
26+
).execute()
27+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Generated, Insertable, Selectable, Updateable } from "kysely"
2+
3+
export interface Database {
4+
World: WorldTable
5+
Fortune: FortuneTable
6+
}
7+
8+
export interface WorldTable {
9+
id: Generated<number>
10+
randomnumber: number
11+
}
12+
13+
export type WorldRow = Selectable<WorldTable>
14+
export type NewWorld = Insertable<WorldTable>
15+
export type WorldUpdate = Updateable<WorldTable>
16+
17+
export interface FortuneTable {
18+
id: Generated<number>
19+
message: string
20+
}
21+
22+
export type Fortune = Selectable<FortuneTable>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { NextRequest, NextResponse } from "next/server"
2+
3+
export function middleware(request: NextRequest) {
4+
const response = NextResponse.next()
5+
response.headers.set("Server", "Next.js")
6+
return response
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { NextConfig } from "next";
2+
3+
const nextConfig: NextConfig = {
4+
output: "standalone",
5+
};
6+
7+
export default nextConfig;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
FROM node:22-slim
2+
3+
ENV NEXT_TELEMETRY_DISABLED="1"
4+
ENV DATABASE_URL="postgres://benchmarkdbuser:benchmarkdbpass@tfb-database/hello_world"
5+
6+
EXPOSE 3000
7+
8+
WORKDIR /nextjs
9+
10+
COPY package.json package-lock.json ./
11+
RUN npm ci
12+
13+
COPY ./ ./
14+
RUN npm run build \
15+
&& cp -r public .next/standalone/ \
16+
&& cp -r .next/static .next/standalone/.next/
17+
18+
ENV NODE_ENV="production"
19+
20+
CMD ["node", ".next/standalone/server.js"]

0 commit comments

Comments
 (0)