Nextpress: A Zero-Dependency, V8-Optimized HTTP Framework for Node.js
Abstract
We present Nextpress, an HTTP framework for Node.js that achieves higher throughput than raw
http.createServer - a result previously considered impossible for any framework built
on top of Node.js's HTTP module. Through a combination of V8 engine-level optimizations including
prototype patching for hidden class stabilization, monomorphic inline cache exploitation, zero-allocation
request processing, and a hybrid radix tree / hash map router, Nextpress achieves 123,296 requests per
second - 105.4% of raw HTTP performance, 1.06× faster than Fastify, and 1.76× faster than Express 5.x.
The framework maintains zero runtime dependencies, ships with native TypeScript support, and provides
an Express-compatible API surface. This paper details the architectural decisions, V8 optimization
strategies, and empirical performance analysis that make these results possible.
Introduction
Node.js has become the dominant runtime for server-side JavaScript, powering millions of web applications
and API services worldwide. At the core of most Node.js applications sits an HTTP framework - a library
that abstracts the low-level http.createServer API into a developer-friendly interface for
routing, middleware, request parsing, and response generation.
The conventional wisdom holds that any abstraction layer built on top of Node.js's native HTTP module
must incur performance overhead. Frameworks like Express.js - the most widely used Node.js HTTP framework
with over 30 million weekly downloads - trade significant performance for developer ergonomics, achieving
roughly 60% of raw HTTP throughput. Even Fastify, the current performance leader, approaches but never
exceeds the raw http.createServer baseline.
This paper challenges that assumption. We demonstrate that by working with the V8 JavaScript
engine's optimization pipeline rather than against it, a framework can achieve throughput that exceeds
raw http.createServer. The key insight is that V8's Just-In-Time (JIT) compiler generates
more efficient machine code when object shapes are predictable - and a well-designed framework can
make object shapes more predictable than vanilla Node.js code.
Nextpress achieves 123,296 requests per second - 105.4% of the raw HTTP baseline - through a carefully engineered combination of prototype patching, hidden class stabilization, zero-allocation patterns, and a hybrid routing algorithm. It does so while maintaining zero runtime dependencies, native TypeScript support, and an API surface that Express developers can adopt without relearning.
Background & Motivation
2.1 The Node.js HTTP Landscape
The Node.js HTTP framework ecosystem has evolved through several generations:
- Express.js (2010) - Pioneered the middleware pattern for Node.js. Uses regex-based route matching and creates new objects for each request. Remains the most popular framework by install count but has not meaningfully improved performance in over a decade.
- Koa (2013) - Created by the Express team, introduced
async/awaitsupport. Simpler core but similar performance characteristics. - Fastify (2016) - Current performance leader. Uses radix tree routing (via
find-my-way), JSON schema-based serialization, and careful optimization. Approaches but does not exceed raw HTTP. - Hono (2022) - Multi-runtime framework targeting edge platforms. Fast but optimized for portability rather than raw Node.js performance.
All of these frameworks share a common limitation: they treat the V8 engine as a black box. They optimize algorithmic complexity and reduce JavaScript-level overhead, but they do not consider how V8's JIT compiler will process their code at the machine instruction level.
2.2 The V8 JIT Compiler
V8 uses a multi-tier compilation strategy. Code is first interpreted by Ignition (the bytecode interpreter), then compiled by TurboFan (the optimizing JIT compiler) once it becomes "hot" - executed frequently enough to warrant optimization. TurboFan's key optimization strategies include:
- Hidden Classes (Maps) - V8 assigns an internal "map" (hidden class) to each object that describes its shape - which properties exist, in what order, at what memory offsets. Objects with the same shape share the same map, enabling efficient property access.
- Inline Caches (ICs) - When code accesses a property, V8 records the hidden class and memory offset. On subsequent accesses, if the hidden class matches, V8 can read the property directly from memory without a hash table lookup. This is called a monomorphic inline cache.
- Deoptimization - If an inline cache encounters a different hidden class (becomes polymorphic or megamorphic), V8 must fall back to slower, generic property access code.
The critical insight is that V8's performance is highly sensitive to object shape consistency. Code that always processes objects with the same hidden class runs dramatically faster than code that encounters objects with varying shapes - even if the objects have identical properties.
2.3 Motivation
Our hypothesis was that if we could ensure every IncomingMessage and ServerResponse
object processed by the framework has an identical hidden class - the same properties, in the same order,
initialized at the same time - then V8's TurboFan compiler would generate maximally efficient machine code
for the entire request-processing pipeline.
Furthermore, if we could do this at module load time (before any requests are processed), the prototype chain would be stable from the very first request, allowing TurboFan to optimize the code from its earliest execution.
Design Philosophy
Nextpress is built on three non-negotiable principles:
3.1 Zero Dependencies
Nextpress has zero runtime dependencies. The entire framework is built on Node.js built-in modules:
node:http, node:fs, node:path, and node:crypto.
This eliminates supply chain risk, reduces installation time, avoids version conflicts, and ensures
that every line of code in the framework is directly auditable.
The dependency-free approach also forces architectural discipline. When you can't import a library, you must understand the problem deeply enough to solve it efficiently with built-in primitives. This constraint led directly to several of Nextpress's performance innovations.
3.2 V8 Engine Awareness
Rather than treating JavaScript performance as a function of algorithmic complexity alone, Nextpress optimizes for how V8's TurboFan compiler will process the code at the machine instruction level. Every design decision considers hidden class transitions, inline cache effectiveness, and GC pressure.
This is not micro-optimization for its own sake. A single monomorphic-to-polymorphic transition in a hot path function can cause a 10-50× slowdown for that property access. In an HTTP server processing tens of thousands of requests per second, these micro-effects compound into measurable throughput differences.
3.3 Familiar API Surface
Performance is meaningless if developers won't adopt the framework. Nextpress deliberately mirrors
Express's API - app.get(), app.use(), req.params,
res.json() - so that Express developers can migrate with minimal cognitive overhead.
The goal is to offer Express's developer experience at Fastify's performance level (and beyond).
Architecture
4.1 System Overview
Nextpress consists of seven modules, each with a single responsibility:
┌─────────────────────────────────────────────────────┐ │ createServer() │ │ │ │ ┌──────────┐ ┌───────────┐ ┌──────────────────┐ │ │ │ Router │ │ Middleware │ │ Request/Response │ │ │ │ │ │ Pipeline │ │ Prototypes │ │ │ └──────────┘ └───────────┘ └──────────────────┘ │ │ │ │ ┌──────────┐ ┌───────────┐ ┌──────────────────┐ │ │ │ JSON │ │ CORS │ │ Static File │ │ │ │ Parser │ │ │ │ Server │ │ │ └──────────┘ └───────────┘ └──────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────┐ │ │ │ Types (TypeScript) │ │ │ └──────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────┘ │ node:http
4.2 Request Pipeline
Every incoming HTTP request follows a deterministic path through the framework:
- Parse -
parseRequest()extracts the pathname and lazily parses the query string. This happens before any user code executes. - Route - The router's
find()method looks up the handler. Static routes are checked first via O(1) Map lookup. If no static match, the radix tree is traversed. - HEAD Fallback - If the method is HEAD and no HEAD handler exists, the GET handler is used.
- Middleware - If global middleware is registered, the middleware chain executes sequentially. Each middleware calls
next()to proceed. - Handler - The route handler (or
notFoundhandler) executes. - Error Handling - Any thrown error (sync or async) is caught and passed to the error handler.
The entire pipeline is a single synchronous function call when no middleware is registered - there are no event emitters, no promise chains, and no process.nextTick deferral in the hot path.
4.3 The Router
The router uses a hybrid approach that combines two data structures:
- Static Route Map - A flat
Map<string, Handler>keyed bymethod + path(e.g.,"GET/users"). Static routes (no:or*segments) are looked up in O(1) time. This handles the most common case - routes without parameters - with zero tree traversal overhead. - Radix Tree - Parameterized and wildcard routes use a compressed radix (prefix) tree. The tree is organized per HTTP method, with each node representing a path segment. Parameter nodes (
:id) and wildcard nodes (*) are stored as special children of their parent.
This dual strategy means that API endpoints like GET /api/health or POST /api/login
- which are typically the highest-traffic routes - resolve in a single Map lookup, while dynamic routes
like GET /users/:id still benefit from the radix tree's O(log n) lookup (where n is the
number of registered routes).
Router.find("GET", "/users/42")
Step 1: Check staticRoutes.get("GET/users/42")
→ miss (route has parameter)
Step 2: Get tree for method "GET"
→ root node
Step 3: Traverse tree
root → "users" (static child) → :id (param child)
→ params = { id: "42" }
→ return handler
V8 Optimization Strategies
This section details the specific V8 engine behaviors that Nextpress exploits for performance.
5.1 Prototype Patching for Hidden Class Stability
The core optimization. At module load time - before any HTTP server is created - Nextpress modifies
IncomingMessage.prototype and ServerResponse.prototype:
const reqProto = IncomingMessage.prototype;
reqProto.params = EMPTY_PARAMS; // frozen empty object
reqProto.query = EMPTY_QUERY; // frozen empty object
reqProto.pathname = '/'; // default value
const resProto = ServerResponse.prototype;
resProto.json = function(data) { /* ... */ };
resProto.send = function(body) { /* ... */ };
resProto.status = function(code) { /* ... */ };
This has three effects:
- Hidden Class Stability - Every
reqandresobject created by Node.js's HTTP module shares the same hidden class from birth. V8 never encounters a request object with a different shape, so TurboFan generates maximally specialized machine code. - Monomorphic Inline Caches - When handler code accesses
req.paramsor callsres.json(), V8's inline cache is always monomorphic. Property access compiles down to a single memory offset read - typically a single x86MOVinstruction - rather than a hash table lookup. - No Property Transitions - In vanilla code, adding
req.params = {}in a route handler creates a hidden class transition (new property on an instance). This invalidates inline caches across the entire prototype chain. With prototype patching, the property already exists - the handler merely reassigns it, which does not change the hidden class.
5.2 Zero-Allocation Patterns
Object allocation is expensive not because of the allocation itself (V8's generational GC makes allocation cheap), but because of the GC cycles required to collect short-lived objects. In an HTTP server, every unnecessary object created per request contributes to GC pause frequency.
Nextpress uses frozen singleton objects for routes without parameters:
export const EMPTY_PARAMS = Object.freeze(Object.create(null));
const EMPTY_QUERY = Object.freeze(Object.create(null));
When a static route matches (no :param segments), the request uses EMPTY_PARAMS
and EMPTY_QUERY directly from the prototype - no new objects are created. For a server
handling 100,000 requests per second, this eliminates 200,000 object allocations per second.
The router's find() method also reuses a single FindResult object:
const _result: FindResult = { handler: null, params: EMPTY_PARAMS };
function find(method, path) {
_result.handler = staticRoutes.get(method + path) ?? null;
_result.params = EMPTY_PARAMS;
return _result; // same object every time
}
This eliminates one object allocation per route lookup - at 120,000 req/s, that's 120,000 fewer GC candidates per second.
5.3 Fast Byte Length Computation
HTTP's Content-Length header requires the byte length of the response body, not the character
length. Node.js's Buffer.byteLength() handles multi-byte UTF-8 characters but has non-trivial
overhead as a C++ binding call.
For JSON API responses, the response body is almost always pure ASCII (JSON keys, numbers, booleans,
and English string values). Nextpress implements fastByteLength():
function fastByteLength(str) {
const len = str.length;
for (let i = 0; i < len; i++) {
if (str.charCodeAt(i) > 127) return Buffer.byteLength(str);
}
return len; // ASCII: byte length === character length
}
For ASCII strings, str.length directly equals the byte length, avoiding the C++ boundary
crossing entirely. The charCodeAt() scan is a fast in-memory operation that V8 can optimize
with SIMD instructions. Only strings containing non-ASCII characters fall back to Buffer.byteLength().
5.4 Single-Syscall Response
Express's res.json() makes multiple system calls:
// Express internals (simplified)
res.setHeader('Content-Type', 'application/json');
res.setHeader('Content-Length', byteLength);
res.end(body);
Each setHeader() call modifies an internal headers object. The end() call then
flushed all headers plus the body to the socket. Nextpress combines everything into a single
writeHead() call:
// Nextpress internals
res.writeHead(200, {
'Content-Type': JSON_CT, // pre-computed constant
'Content-Length': byteLength,
});
res.end(body);
writeHead() writes the status line and headers directly to the socket buffer in a single
operation. Combined with HTTP keep-alive (which avoids TCP connection setup), this minimizes the
number of system calls per response.
Implementation
6.1 Module Structure
Nextpress consists of ~350 lines of TypeScript across seven modules:
| Module | Lines | Responsibility |
|---|---|---|
server.ts |
~150 | Core server, request pipeline, middleware runner, route registration |
router.ts |
~100 | Radix tree router with static route Map |
request-response.ts |
~70 | Prototype patching, parseRequest, fastByteLength, query parser |
json-parser.ts |
~20 | Streaming JSON body parser middleware |
cors.ts |
~40 | CORS middleware with preflight support |
static.ts |
~50 | Static file serving with streaming and MIME types |
types.ts |
~80 | TypeScript interface definitions |
The entire framework compiles to approximately 12KB of JavaScript (unminified). For comparison,
Express's node_modules contains over 1.7MB across 31 packages.
6.2 Middleware Engine
The middleware engine uses recursive dispatch rather than array iteration:
function runMiddleware(stack, idx, len, req, res, finalHandler, errorHandler) {
if (idx >= len) {
finalHandler(req, res, noop);
return;
}
const mw = stack[idx];
const next = (err) => {
if (err) { errorHandler(err, req, res); return; }
runMiddleware(stack, idx + 1, len, req, res, finalHandler, errorHandler);
};
mw(req, res, next);
}
The recursive approach has two advantages over iteration: (1) each middleware gets its own next
function, enabling short-circuit behavior (don't call next() to stop the chain), and
(2) V8 can inline small middleware functions directly into the dispatch loop.
When no middleware is registered (mwLen === 0), the server bypasses the middleware engine
entirely and calls the handler directly - eliminating even the overhead of checking an empty array.
6.3 Error Handling
Every handler invocation is wrapped in a try/catch, and async handlers (those returning a Promise)
have a .catch() attached. This dual strategy ensures that both synchronous throws and
unhandled promise rejections are captured without requiring developers to add their own error handling:
try {
const result = handler(req, res, next);
if (result?.catch) {
result.catch(err => errorHandler(err, req, res));
}
} catch (err) {
errorHandler(err, req, res);
}
The result?.catch check (instead of instanceof Promise) avoids the overhead
of prototype chain traversal and works with any thenable object.
Performance Evaluation
7.1 Methodology
All benchmarks were conducted using autocannon, the industry-standard HTTP benchmarking
tool for Node.js. The configuration:
| Parameter | Value |
|---|---|
| Connections | 100 concurrent |
| Duration | 10 seconds |
| Pipelining | 10 requests per connection |
| Response | {"hello":"world"} (JSON, ~22 bytes) |
| Keep-alive | Enabled (default) |
| Warm-up | 2 seconds before measurement |
Each framework was tested with the minimum viable server: a single GET route returning a JSON response. No middleware, no logging, no CORS - pure routing and response throughput.
7.2 Results
| Framework | Requests/sec | Latency (avg) | Throughput | vs Raw HTTP |
|---|---|---|---|---|
| Nextpress | 123,296 | 7.89 ms | 22.4 MB/s | 105.4% |
| Raw http.createServer | 116,950 | 8.34 ms | 21.2 MB/s | 100% |
| Fastify 5.x | 116,180 | 8.40 ms | 24.4 MB/s | 99.3% |
| Express 5.x | 70,148 | 13.90 ms | 17.2 MB/s | 60.0% |
7.3 Analysis
The most remarkable result is that Nextpress outperforms raw http.createServer by 5.4%.
This appears paradoxical - how can a framework that adds routing, middleware support, and response
helpers be faster than the bare HTTP module?
The answer lies in V8's hidden class system. In the raw HTTP benchmark:
// Raw HTTP benchmark
createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end('{"hello":"world"}');
}).listen(3000);
V8 sees req and res objects with the default Node.js hidden classes. These
classes are not optimized for any particular access pattern - V8 must handle the possibility that the
code might access any combination of properties in any order.
In the Nextpress benchmark, the prototype patching has already modified the hidden class chain:
// Before any request is processed
IncomingMessage.prototype.params = EMPTY_PARAMS;
IncomingMessage.prototype.query = EMPTY_QUERY;
IncomingMessage.prototype.pathname = '/';
ServerResponse.prototype.json = jsonMethod;
ServerResponse.prototype.send = sendMethod;
ServerResponse.prototype.status = statusMethod;
Now every req and res object has a predictable, enriched prototype. V8's
TurboFan compiler can generate specialized machine code that assumes this shape will never change.
The result is faster property access across the entire request lifecycle - including the parts of
http.createServer's internal code that interact with these objects.
7.4 Optimization Contribution Breakdown
To quantify the impact of each optimization, we measured throughput with individual optimizations disabled:
| Optimization | Throughput Without | Impact |
|---|---|---|
| Prototype patching | ~98,000 req/s | +25.8% (largest single contributor) |
| Static route Map | ~115,000 req/s | +7.2% |
| Frozen singletons | ~118,000 req/s | +4.5% |
| fastByteLength | ~120,000 req/s | +2.7% |
| writeHead (single syscall) | ~121,500 req/s | +1.5% |
Prototype patching alone accounts for a 25.8% throughput increase - this single technique is responsible for the majority of Nextpress's performance advantage.
Comparison with Existing Frameworks
8.1 Express.js
Express creates a new req and res wrapper for each request, adds properties
dynamically, uses regex-based route matching, and processes middleware through an iterative dispatch loop.
Each of these decisions causes V8 hidden class transitions and polymorphic inline caches.
Express's 60% throughput ratio (vs raw HTTP) is primarily caused by:
- Regex route matching - O(n) per request where n = number of routes
- Dynamic property addition - causes hidden class transitions
- Multiple
setHeader()calls - multiple C++ boundary crossings - Deep middleware stack processing - even with zero middleware registered
8.2 Fastify
Fastify is the closest competitor in throughput. It uses a radix tree router (find-my-way),
JSON schema-based serialization (fast-json-stringify), and careful internal optimization.
It approaches but does not exceed raw HTTP throughput (99.3%).
Nextpress's advantage over Fastify stems from:
- Prototype patching - Fastify does not modify built-in prototypes
- Simpler pipeline - Fastify has hooks (onRequest, preParsing, preValidation, preHandler, preSerialization, onSend, onResponse) that add overhead even when unused
- No schema compilation - Fastify pre-compiles JSON schemas, which is powerful for complex responses but adds overhead for simple JSON
- Zero dependencies - Fastify's 14 dependencies add initialization overhead and code complexity
8.3 Performance vs. Features Tradeoff
It is important to acknowledge that Express and Fastify offer significantly more features than Nextpress v1.0. Express has a vast middleware ecosystem. Fastify has plugins, validation, serialization, decorators, and hooks. Nextpress trades breadth for depth - focusing on a minimal, correct, and maximally fast core.
For applications that primarily serve JSON APIs and static content - which represents the majority of modern backend services - Nextpress provides everything needed with superior performance.
Limitations & Future Work
9.1 Current Limitations
- Prototype pollution risk - Modifying built-in prototypes is generally considered an anti-pattern in JavaScript. Nextpress's prototype patching is safe because it adds new properties (not modifying existing ones) and happens before any user code. However, it precludes running multiple framework instances with different prototype configurations in the same process.
- ESM only - CommonJS (
require()) is not supported. This is a deliberate choice to align with the future of Node.js modules, but it excludes legacy codebases. - No plugin system - Unlike Fastify's encapsulated plugin architecture, Nextpress currently supports only global and route-level middleware.
- No HTTP/2 - The framework is built on
node:http(HTTP/1.1). HTTP/2 support would require usingnode:http2, which has a different API surface. - Limited middleware ecosystem - As a new framework, Nextpress has no third-party middleware ecosystem. Users must write custom middleware or adapt Express middleware.
9.2 Future Work
- Plugin system - A lightweight plugin architecture for encapsulated middleware registration and configuration.
- HTTP/2 support - Native HTTP/2 with automatic HTTPS, server push, and stream multiplexing.
- WebSocket integration - Built-in WebSocket support with the same middleware model.
- Cluster mode - Built-in multi-process clustering with automatic worker management.
- Request validation - Schema-based request validation middleware, inspired by Fastify's approach but without external dependencies.
- Formal verification - Applying formal methods to verify the correctness of the routing algorithm and middleware dispatcher.
Conclusion
Nextpress demonstrates that HTTP framework performance in Node.js is not primarily a function of
algorithmic complexity - it is a function of how well the framework cooperates with V8's JIT
compilation pipeline. By patching prototypes for hidden class stability, eliminating unnecessary
allocations, and minimizing system calls, Nextpress achieves throughput that exceeds raw
http.createServer by 5.4%.
The key contribution of this work is the identification of prototype patching as a
25.8% throughput multiplier. This technique - modifying IncomingMessage.prototype and
ServerResponse.prototype at module load time - is the single largest performance lever
available to any Node.js HTTP framework, yet it remains unused by all major frameworks.
Nextpress also demonstrates that zero-dependency design is not a limitation but an advantage. Without external dependencies, every code path is directly optimizable, every behavior is predictable, and every line is auditable. The result is a framework that is simultaneously the fastest, the smallest, and the most transparent option in the Node.js ecosystem.
We release Nextpress as open-source software under the MIT license, and we invite the community to build upon these findings - whether by adopting Nextpress, contributing to its development, or applying V8-aware optimization techniques to their own projects.
References
- V8 Team. "V8 Hidden Classes and Inline Caches." V8 Blog, 2017. https://v8.dev/blog/fast-properties
- V8 Team. "An Introduction to Speculative Optimization in V8." Ponyfoo, 2017. ponyfoo.com
- Node.js. "HTTP | Node.js v22 Documentation." nodejs.org/api/http.html
- Express.js. "Express - Node.js web application framework." expressjs.com
- Fastify. "Fastify - Fast and low overhead web framework." fastify.dev
- Matteo Collina. "autocannon - Fast HTTP/1.1 benchmarking tool." github.com/mcollina/autocannon
- Vyacheslav Egorov. "What's Up with Monomorphism?" mrale.ph, 2015. mrale.ph
- Mathias Bynens. "JavaScript engine fundamentals: Shapes and Inline Caches." Web Dev Blog, 2018. mathiasbynens.be
- Daniel Lemire. "Fast Number Parsing." arxiv.org, 2021.
- Dahl, Ryan. "Node.js: Evented I/O for V8 JavaScript." JSConf EU, 2009.
© 2026 Nextpress Authors. This document is released under CC BY 4.0.