Skip to content

Commit

Permalink
Merge pull request #9506 from jonathanhefner/benchmark-nextjs
Browse files Browse the repository at this point in the history
Benchmark Next.js
  • Loading branch information
msmith-techempower authored Jan 10, 2025
2 parents 51288cb + b92239a commit ce36b84
Show file tree
Hide file tree
Showing 23 changed files with 1,576 additions and 0 deletions.
7 changes: 7 additions & 0 deletions frameworks/TypeScript/nextjs/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
*.dockerfile
.dockerignore
node_modules
npm-debug.log
README.md
.next
.git
41 changes: 41 additions & 0 deletions frameworks/TypeScript/nextjs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
47 changes: 47 additions & 0 deletions frameworks/TypeScript/nextjs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Next.js Benchmarking Test

## Test source files and URLs

| Test | Source Code | URL |
| --- | --- | --- |
| [JSON Serialization][] | [`app/json/route.ts`][] | http://localhost:3000/json |
| [Single Database Query][] | [`app/db/route.ts`][] | http://localhost:3000/db |
| [Multiple Database Queries][] | [`app/queries/route.ts`][] | http://localhost:3000/queries?queries= |
| [Fortunes][] | [`app/fortunes/page.tsx`][] | http://localhost:3000/fortunes |
| [Database Updates][] | [`app/updates/route.ts`][] | http://localhost:3000/updates?queries= |
| [Plaintext][] | [`app/plaintext/route.ts`][] | http://localhost:3000/plaintext |
| [Caching][] | [`app/cached-queries/route.ts`][] | http://localhost:3000/cached-queries?queries= |

[JSON Serialization]: https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview#json-serialization
[Single Database Query]: https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview#single-database-query
[Multiple Database Queries]: https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview#multiple-database-queries
[Fortunes]: https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview#fortunes
[Database Updates]: https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview#database-updates
[Plaintext]: https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview#plaintext
[Caching]: https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Framework-Tests-Overview#caching

[`app/json/route.ts`]: ./app/json/route.ts
[`app/db/route.ts`]: ./app/db/route.ts
[`app/queries/route.ts`]: ./app/queries/route.ts
[`app/fortunes/page.tsx`]: ./app/fortunes/page.tsx
[`app/updates/route.ts`]: ./app/updates/route.ts
[`app/plaintext/route.ts`]: ./app/plaintext/route.ts
[`app/cached-queries/route.ts`]: ./app/cached-queries/route.ts

## TODO

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:

```diff
--- a/frameworks/TypeScript/nextjs/benchmark_config.json
+++ b/frameworks/TypeScript/nextjs/benchmark_config.json
@@ -20,7 +20,7 @@
"json_url": "/json",
"db_url": "/db",
"query_url": "/queries?queries=",
- "TEMPORARILY DISABLED fortune_url": "/fortunes",
+ "fortune_url": "/fortunes",
"update_url": "/updates?queries=",
"plaintext_url": "/plaintext",
"cached_query_url": "/cached-queries?queries="
```
18 changes: 18 additions & 0 deletions frameworks/TypeScript/nextjs/app/cached-queries/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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)
}
6 changes: 6 additions & 0 deletions frameworks/TypeScript/nextjs/app/db/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { findWorld } from "@/lib/db"

export async function GET() {
const id = 1 + Math.floor(Math.random() * 10000)
return Response.json(await findWorld(id))
}
Binary file added frameworks/TypeScript/nextjs/app/favicon.ico
Binary file not shown.
31 changes: 31 additions & 0 deletions frameworks/TypeScript/nextjs/app/fortunes/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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>
</>
}
3 changes: 3 additions & 0 deletions frameworks/TypeScript/nextjs/app/json/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export async function GET() {
return Response.json({ message: "Hello, World!" })
}
13 changes: 13 additions & 0 deletions frameworks/TypeScript/nextjs/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
{children}
</body>
</html>
);
}
3 changes: 3 additions & 0 deletions frameworks/TypeScript/nextjs/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function Home() {
return
}
3 changes: 3 additions & 0 deletions frameworks/TypeScript/nextjs/app/plaintext/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function GET() {
return new Response("Hello, World!")
}
15 changes: 15 additions & 0 deletions frameworks/TypeScript/nextjs/app/queries/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
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))
}
26 changes: 26 additions & 0 deletions frameworks/TypeScript/nextjs/app/updates/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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)
}
30 changes: 30 additions & 0 deletions frameworks/TypeScript/nextjs/benchmark_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"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=",
"TEMPORARILY DISABLED fortune_url": "/fortunes",
"update_url": "/updates?queries=",
"plaintext_url": "/plaintext",
"cached_query_url": "/cached-queries?queries="
}
}
]
}
27 changes: 27 additions & 0 deletions frameworks/TypeScript/nextjs/lib/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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()
}
22 changes: 22 additions & 0 deletions frameworks/TypeScript/nextjs/lib/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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
}

export type Fortune = Selectable<FortuneTable>
7 changes: 7 additions & 0 deletions frameworks/TypeScript/nextjs/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { NextRequest, NextResponse } from "next/server"

export function middleware(request: NextRequest) {
const response = NextResponse.next()
response.headers.set("Server", "Next.js")
return response
}
7 changes: 7 additions & 0 deletions frameworks/TypeScript/nextjs/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
output: "standalone",
};

export default nextConfig;
20 changes: 20 additions & 0 deletions frameworks/TypeScript/nextjs/nextjs.dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
FROM node:22-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"]
Loading

0 comments on commit ce36b84

Please sign in to comment.