Featured image of post HTTP Kernel v0.2.0 – Von dynamischem Dispatch zur vorkompilierten Pipeline

HTTP Kernel v0.2.0 – Von dynamischem Dispatch zur vorkompilierten Pipeline

Wie ich meinen Deno-basierten HTTP Kernel von einer rekursiven Dispatch-Schleife auf eine vorkompilierte Middleware-Pipeline umgestellt habe – inklusive Benchmarks mit bis zu 60 % Performance-Gewinn.

Mit v0.2.0 hat mein Deno‑basierter HTTP Kernel einen entscheidenden Schritt gemacht: Statt jede Middleware bei jedem Request dynamisch zu verkettet, wird die gesamte Kette jetzt einmalig kompiliert. Das reduziert Overhead, vereinfacht die Fehlerbehandlung – und macht das Routing spürbar schneller.

Ausgangslage

Die erste Generation des Kernels setzte auf einen rekursiven dispatch()‑Mechanismus:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
private async executePipeline(
    ctx: TContext,
    middleware: Middleware<TContext>[],
    handler: Handler<TContext>,
): Promise<Response> {
    const handleInternalError = (ctx: TContext, err?: unknown) =>
        this.cfg.httpErrorHandlers[HTTP_500_INTERNAL_SERVER_ERROR](
            ctx,
            normalizeError(err),
        );

    let lastIndex = -1;

    const dispatch = async (currentIndex: number): Promise<Response> => {
        if (currentIndex <= lastIndex) {
            throw new Error('Middleware called `next()` multiple times');
        }
        lastIndex = currentIndex;

        const isWithinMiddleware = currentIndex < middleware.length;
        const fn = isWithinMiddleware ? middleware[currentIndex] : handler;

        if (isWithinMiddleware) {
            if (!isMiddleware(fn)) {
                throw new Error('Expected middleware function, but received invalid value');
            }
            return await fn(ctx, () => dispatch(currentIndex + 1));
        }

        if (!isHandler(fn)) {
            throw new Error('Expected request handler, but received invalid value');
        }

        return await fn(ctx);
    };

    try {
        const response = await dispatch(0);
        return this.cfg.decorateResponse(response, ctx);
    } catch (e) {
        return handleInternalError(ctx, e);
    }
}

Dieser Ansatz war leicht zu verstehen, erzeugte aber pro Request mehrere Closures, Index‑Prüfungen und Typ‑Checks – alles Dinge, die bei hoher Parallelität unnötig Zeit kosten.

Der neue Ansatz

Beim Registrieren einer Route wird nun eine statisch verlinkte Funktionskette erzeugt. Ergebnis ist eine einzige Funktion runRoute(ctx), die alle Middlewares und den Handler inline ausführt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private compile(
    route: Omit<IInternalRoute<TContext>, 'runRoute' | 'matcher' | 'method'>,
): (ctx: TContext) => Promise<Response> {
    if (!isHandler<TContext>(route.handler)) {
        throw new TypeError('Route handler must be a function returning a Promise<Response>.');
    }
    let composed = route.handler;

    for (let i = route.middlewares.length - 1; i >= 0; i--) {
        if (!isMiddleware<TContext>(route.middlewares[i])) {
            throw new TypeError(`Middleware at index ${i} is not a valid function.`);
        }

        const current = route.middlewares[i];
        const next = composed;

        composed = async (ctx: TContext): Promise<Response> => {
            let called = false;
            return await current(ctx, async () => {
                if (called) {
                    throw new Error(`next() called multiple times in middleware at index ${i}`);
                }
                called = true;
                return await next(ctx);
            });
        };
    }

    return composed;
}

Wichtig: Jede Middleware bekommt weiterhin ihr next() – jedoch kann dieses dank called‑Flag nur noch einmal aufgerufen werden, was Mehrfach‑Ausführungen zuverlässig verhindert.

Benchmarks

Gemessen mit 10 000 parallelen Requests auf einem Ryzen 7 8845HS:

Benchmark (parallel)v0.1.0 (dynamisch)v0.2.0 (vorkompiliert)Gewinn
Simple Route5.4 ms2.1 ms≈ 61 %
Complex Route11.2 ms7.9 ms≈ 29 %

Bei Einzeln‑Requests liegen die Werte nahezu gleichauf – der Geschwindigkeitsvorteil zeigt sich vor allem unter hoher Last.

Fazit

  • Statisches Routing reduziert Overhead und steigert Durchsatz signifikant.
  • next()‑Fehler werden jetzt sofort erkannt.
  • API blieb kompatibel – Upgrade von 0.1.x auf 0.2.0 ist ohne Code‑Änderungen möglich.

Der Kernel läuft bereits performant und stabil in meinem build‑cache‑server – einem Deno‑basierten Build‑Cache für act; kompatiibel mit der Github Actions Cache API.


👉 Quellcode & Release‑Notes: https://github.com/0xMax42/http-kernel

Erstellt mit Hugo
Theme Stack gestaltet von Jimmy