Silgi

Compiled Pipelines

How Silgi compiles your procedures into optimized pipelines at startup for maximum runtime performance.

When your server starts, Silgi doesn't just register your procedures — it compiles them. Each procedure is analyzed and transformed into the fastest possible handler function. This happens once at startup, so every request after that runs pre-optimized code.

Overview

No schemas Has schemas Yes s.router compileProcedure Has wraps? Sync fast path Semi-sync path Wrap path compileRouter rou3 radix tree

What gets compiled?

When you call s.router() or s.serve(), Silgi walks through every procedure and:

  1. Separates guards from wraps — guards and wraps have different execution models, so they're split into separate lists
  2. Selects an unrolled guard runner — based on how many guards a procedure has (0–4), a specialized function is chosen with no loop overhead
  3. Pre-computes the fail function — if the procedure defines typed errors, the error factory is created once and reused
  4. Builds a direct function chain — the final handler is a single function that calls guards → validates input → runs the resolver → validates output, with no per-request closures

The result is a CompiledHandler — a single function that takes a context object, raw input, and abort signal, and returns the result.

Guard unrolling

Most procedures have 0–4 guards. Instead of looping through them at runtime, Silgi generates specialized code paths for each count:

0 1 2 3 4 5+ selectGuardRunner guard count no-op 1 direct call 2 direct calls 3 direct calls 4 direct calls loop

This matters because V8's optimizing compiler (Maglev/TurboFan) can inline fixed call counts much better than dynamic loops. Each guard function becomes a direct reference — no property lookups at runtime.

Guards also have a sync fast path: if a guard returns a plain object (not a Promise), the result is applied immediately without awaiting. Only async guards trigger the Promise path.

No Yes guard.fn Returns Promise? Apply result immediately await Promise Next guard or resolve

Context pooling

Every request needs a context object. Instead of allocating a new one each time, Silgi maintains a pool of pre-allocated null-prototype objects:

No Yes Under 128 Full Request Pool empty? Pop from pool Object.create null Use ctx Wipe properties Pool full? Return to pool GC collect

The pool holds up to 128 context objects. This eliminates per-request garbage collection pressure — no objects are created or destroyed during normal operation.

Context objects use Object.create(null) — they have no prototype chain. This prevents prototype pollution attacks and avoids accidental collisions with Object.prototype methods.

Three execution paths

Based on what a procedure uses, Silgi picks one of three paths at compile time:

Yes No No Yes compileProcedure Has wraps? Wrap path — onion chain with next Has schemas? Sync fast path — zero closures, zero awaits Semi-sync — validates input/output CompiledHandler
PathWhenWhat it avoids
Sync fast pathNo wraps, no input/output schemasZero closures, zero awaits, zero validation
Semi-syncNo wraps, has schemasZero closures, validates input/output
Wrap pathHas wrapsBuilds onion chain for next() calls

The sync fast path is the most common case — a procedure with a few guards and a resolver. It runs without creating a single closure or Promise (unless a guard is async).

Sync fast path in detail

Acquire ctx Run guards Call resolver Return result Release ctx

Wrap path in detail

next next Acquire ctx Run guards Validate input wrap 1 wrap 2 resolver wrap 2 after wrap 1 after Validate output Release ctx

Router compilation

The procedure tree (nested objects from s.router()) is compiled into a radix tree powered by rou3 — the same router used by h3 and Nitro. The tree is built once at startup:

s.router Walk tree users.list users.get users.:id rou3 radix tree findRoute

Route matching is O(path length), not O(route count). Special-case routes defined with .$route({ path, method }) (e.g. auth passthrough wildcards) are also compiled into the same tree.

What this means for you

You don't need to think about any of this. Silgi handles compilation automatically. But it helps explain a few things:

  • Cold start is fast — compilation is cheap (just function composition, no code generation)
  • Warm requests are faster — no per-request setup, no dynamic dispatch, no middleware array iteration
  • Guard count doesn't matter (up to 4) — adding a second or third guard has near-zero overhead compared to one
  • Wraps are slightly slower than guards — they need the onion model (next() calls), so prefer guards when you only need to add context

On this page