From b883b7a4ba0bfdf1e9cd98ee80e98343c3314074 Mon Sep 17 00:00:00 2001 From: BennyFranciscus <268274351+BennyFranciscus@users.noreply.github.com> Date: Tue, 17 Mar 2026 07:59:01 +0000 Subject: [PATCH] Add Hono: ultrafast Web Standards framework for Node.js (~29k stars) --- frameworks/hono/Dockerfile | 9 ++ frameworks/hono/README.md | 16 +++ frameworks/hono/meta.json | 21 +++ frameworks/hono/package.json | 10 ++ frameworks/hono/server.js | 252 +++++++++++++++++++++++++++++++++++ 5 files changed, 308 insertions(+) create mode 100644 frameworks/hono/Dockerfile create mode 100644 frameworks/hono/README.md create mode 100644 frameworks/hono/meta.json create mode 100644 frameworks/hono/package.json create mode 100644 frameworks/hono/server.js diff --git a/frameworks/hono/Dockerfile b/frameworks/hono/Dockerfile new file mode 100644 index 00000000..aed25152 --- /dev/null +++ b/frameworks/hono/Dockerfile @@ -0,0 +1,9 @@ +FROM node:22-slim +RUN apt-get update && apt-get install -y --no-install-recommends python3 make g++ && rm -rf /var/lib/apt/lists/* +WORKDIR /app +COPY package.json . +RUN npm install --omit=dev +COPY server.js . +ENV NODE_ENV=production +EXPOSE 8080 +CMD ["node", "server.js"] diff --git a/frameworks/hono/README.md b/frameworks/hono/README.md new file mode 100644 index 00000000..a6671057 --- /dev/null +++ b/frameworks/hono/README.md @@ -0,0 +1,16 @@ +# Hono + +[Hono](https://github.com/honojs/hono) is an ultrafast, lightweight web framework built on Web Standards. It runs on any JavaScript runtime — Node.js, Bun, Deno, Cloudflare Workers, and more. + +## Implementation Notes + +- Uses `@hono/node-server` for the Node.js runtime adapter +- Cluster mode with one worker per CPU core +- `better-sqlite3` for the /db endpoint with mmap enabled +- Manual gzip compression (zlib level 1) for /compression +- HTTP/2 via native `http2` module on port 8443 +- Hono's RegExpRouter provides fast pattern matching + +## Why Hono? + +Hono is one of the fastest-growing JS frameworks (~29k stars). Its key differentiator is being built entirely on Web Standards (Request/Response), making it portable across runtimes. Running it on Node.js via `@hono/node-server` shows how the Web Standards abstraction layer performs compared to native Node.js APIs (Fastify, bare node) and other runtimes (Bun, Deno). diff --git a/frameworks/hono/meta.json b/frameworks/hono/meta.json new file mode 100644 index 00000000..393e68d0 --- /dev/null +++ b/frameworks/hono/meta.json @@ -0,0 +1,21 @@ +{ + "display_name": "Hono", + "language": "JS", + "type": "framework", + "engine": "V8", + "description": "Ultrafast web framework built on Web Standards, runs on any JS runtime.", + "repo": "https://github.com/honojs/hono", + "enabled": true, + "tests": [ + "baseline", + "pipelined", + "limited-conn", + "json", + "upload", + "compression", + "mixed", + "noisy", + "baseline-h2", + "static-h2" + ] +} diff --git a/frameworks/hono/package.json b/frameworks/hono/package.json new file mode 100644 index 00000000..ef95186a --- /dev/null +++ b/frameworks/hono/package.json @@ -0,0 +1,10 @@ +{ + "name": "httparena-hono", + "version": "1.0.0", + "private": true, + "dependencies": { + "hono": "^4.7.0", + "@hono/node-server": "^1.14.0", + "better-sqlite3": "^11.0.0" + } +} diff --git a/frameworks/hono/server.js b/frameworks/hono/server.js new file mode 100644 index 00000000..3adbe61c --- /dev/null +++ b/frameworks/hono/server.js @@ -0,0 +1,252 @@ +const cluster = require('cluster'); +const os = require('os'); +const fs = require('fs'); +const http2 = require('http2'); +const zlib = require('zlib'); + +const SERVER_NAME = 'hono'; + +let datasetItems; +let largeJsonBuf; +let dbStmt; +const staticFiles = {}; +const MIME_TYPES = { + '.css': 'text/css', '.js': 'application/javascript', '.html': 'text/html', + '.woff2': 'font/woff2', '.svg': 'image/svg+xml', '.webp': 'image/webp', '.json': 'application/json' +}; + +function loadStaticFiles() { + const dir = '/data/static'; + try { + for (const name of fs.readdirSync(dir)) { + const buf = fs.readFileSync(dir + '/' + name); + const ext = name.slice(name.lastIndexOf('.')); + staticFiles[name] = { buf, ct: MIME_TYPES[ext] || 'application/octet-stream' }; + } + } catch (e) {} +} + +function loadDataset() { + const path = process.env.DATASET_PATH || '/data/dataset.json'; + try { + datasetItems = JSON.parse(fs.readFileSync(path, 'utf8')); + } catch (e) {} +} + +function loadLargeDataset() { + try { + const raw = JSON.parse(fs.readFileSync('/data/dataset-large.json', 'utf8')); + const items = raw.map(d => ({ + id: d.id, name: d.name, category: d.category, + price: d.price, quantity: d.quantity, active: d.active, + tags: d.tags, rating: d.rating, + total: Math.round(d.price * d.quantity * 100) / 100 + })); + largeJsonBuf = Buffer.from(JSON.stringify({ items, count: items.length })); + } catch (e) {} +} + +function loadDatabase() { + try { + const Database = require('better-sqlite3'); + const db = new Database('/data/benchmark.db', { readonly: true }); + db.pragma('mmap_size=268435456'); + dbStmt = db.prepare('SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE price BETWEEN ? AND ? LIMIT 50'); + } catch (e) {} +} + +function sumQuery(query) { + let sum = 0; + for (const key in query) { + const n = parseInt(query[key], 10); + if (n === n) sum += n; + } + return sum; +} + +function parseQueryString(url) { + const q = url.indexOf('?'); + if (q === -1) return {}; + const result = {}; + const qs = url.slice(q + 1); + let i = 0; + while (i < qs.length) { + const eq = qs.indexOf('=', i); + if (eq === -1) break; + let amp = qs.indexOf('&', eq); + if (amp === -1) amp = qs.length; + result[decodeURIComponent(qs.slice(i, eq))] = decodeURIComponent(qs.slice(eq + 1, amp)); + i = amp + 1; + } + return result; +} + +function startWorker() { + loadDataset(); + loadLargeDataset(); + loadStaticFiles(); + loadDatabase(); + + const { Hono } = require('hono'); + const { serve } = require('@hono/node-server'); + + const app = new Hono(); + + // --- /pipeline --- + app.get('/pipeline', (c) => { + c.header('server', SERVER_NAME); + return c.text('ok'); + }); + + // --- /baseline11 GET & POST --- + app.get('/baseline11', (c) => { + const query = parseQueryString(c.req.raw.url); + const s = sumQuery(query); + c.header('server', SERVER_NAME); + return c.text(String(s)); + }); + + app.post('/baseline11', async (c) => { + const query = parseQueryString(c.req.raw.url); + const querySum = sumQuery(query); + const body = await c.req.text(); + let total = querySum; + const n = parseInt(body.trim(), 10); + if (n === n) total += n; + c.header('server', SERVER_NAME); + return c.text(String(total)); + }); + + // --- /baseline2 --- + app.get('/baseline2', (c) => { + const query = parseQueryString(c.req.raw.url); + const s = sumQuery(query); + c.header('server', SERVER_NAME); + return c.text(String(s)); + }); + + // --- /json --- + app.get('/json', (c) => { + if (!datasetItems) { + return c.text('No dataset', 500); + } + const items = datasetItems.map(d => ({ + id: d.id, name: d.name, category: d.category, + price: d.price, quantity: d.quantity, active: d.active, + tags: d.tags, rating: d.rating, + total: Math.round(d.price * d.quantity * 100) / 100 + })); + const buf = Buffer.from(JSON.stringify({ items, count: items.length })); + c.header('server', SERVER_NAME); + c.header('content-type', 'application/json'); + c.header('content-length', String(buf.length)); + return c.body(buf); + }); + + // --- /compression --- + app.get('/compression', (c) => { + if (!largeJsonBuf) { + return c.text('No dataset', 500); + } + const compressed = zlib.gzipSync(largeJsonBuf, { level: 1 }); + c.header('server', SERVER_NAME); + c.header('content-type', 'application/json'); + c.header('content-encoding', 'gzip'); + c.header('content-length', String(compressed.length)); + return c.body(compressed); + }); + + // --- /db --- + app.get('/db', (c) => { + if (!dbStmt) { + c.header('server', SERVER_NAME); + c.header('content-type', 'application/json'); + return c.body('{"items":[],"count":0}'); + } + const query = parseQueryString(c.req.raw.url); + let min = 10, max = 50; + if (query.min) min = parseFloat(query.min) || 10; + if (query.max) max = parseFloat(query.max) || 50; + const rows = dbStmt.all(min, max); + const items = rows.map(r => ({ + id: r.id, name: r.name, category: r.category, + price: r.price, quantity: r.quantity, active: r.active === 1, + tags: JSON.parse(r.tags), + rating: { score: r.rating_score, count: r.rating_count } + })); + const body = JSON.stringify({ items, count: items.length }); + c.header('server', SERVER_NAME); + c.header('content-type', 'application/json'); + c.header('content-length', String(Buffer.byteLength(body))); + return c.body(body); + }); + + // --- /upload --- + app.post('/upload', async (c) => { + const buf = Buffer.from(await c.req.arrayBuffer()); + c.header('server', SERVER_NAME); + return c.text(String(buf.length)); + }); + + // Start HTTP/1.1 via @hono/node-server + serve({ + fetch: app.fetch, + port: 8080, + hostname: '0.0.0.0', + }, () => { + startH2(); + }); +} + +function startH2() { + const certFile = process.env.TLS_CERT || '/certs/server.crt'; + const keyFile = process.env.TLS_KEY || '/certs/server.key'; + try { + const opts = { + cert: fs.readFileSync(certFile), + key: fs.readFileSync(keyFile), + allowHTTP1: false, + }; + const h2server = http2.createSecureServer(opts, (req, res) => { + const url = req.url; + const q = url.indexOf('?'); + const p = q === -1 ? url : url.slice(0, q); + if (p.startsWith('/static/')) { + const name = p.slice(8); + const sf = staticFiles[name]; + if (sf) { + res.writeHead(200, { 'content-type': sf.ct, 'content-length': sf.buf.length, 'server': SERVER_NAME }); + res.end(sf.buf); + } else { + res.writeHead(404); + res.end(); + } + } else { + let sum = 0; + if (q !== -1) { + const qs = url.slice(q + 1); + let i = 0; + while (i < qs.length) { + const eq = qs.indexOf('=', i); + if (eq === -1) break; + let amp = qs.indexOf('&', eq); + if (amp === -1) amp = qs.length; + const n = parseInt(qs.slice(eq + 1, amp), 10); + if (n === n) sum += n; + i = amp + 1; + } + } + res.writeHead(200, { 'content-type': 'text/plain', 'server': SERVER_NAME }); + res.end(String(sum)); + } + }); + h2server.listen(8443); + } catch (e) {} +} + +if (cluster.isPrimary) { + const numCPUs = os.availableParallelism ? os.availableParallelism() : os.cpus().length; + for (let i = 0; i < numCPUs; i++) cluster.fork(); +} else { + startWorker(); +}