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
What gets compiled?
When you call s.router() or s.serve(), Silgi walks through every procedure and:
- Separates guards from wraps — guards and wraps have different execution models, so they're split into separate lists
- Selects an unrolled guard runner — based on how many guards a procedure has (0–4), a specialized function is chosen with no loop overhead
- Pre-computes the fail function — if the procedure defines typed errors, the error factory is created once and reused
- 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:
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.
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:
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:
| Path | When | What it avoids |
|---|---|---|
| Sync fast path | No wraps, no input/output schemas | Zero closures, zero awaits, zero validation |
| Semi-sync | No wraps, has schemas | Zero closures, validates input/output |
| Wrap path | Has wraps | Builds 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
Wrap path in detail
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:
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