Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading