Skip to content

Performance

The fastest path is a reused instance, compiled hot-path expressions, and sync execution when your extensions do not need promises.

NeedBest choice
One quick sync evaluationevaluateSync()
Repeated hot-path evaluationcompile() once, then compiled.evaluateSync()
Async plugin or function callevaluate() or compiled.evaluate()
Small bundle surfaceImport only the stdlib modules you actually use
PracticeWhy it helps
Create one instance and reuse itKeeps the per-instance caches warm and avoids repeated setup work.
Compile repeated expressionsAvoids repeated parse/compile work and gives you an explicit reusable handle.
Prefer evaluateSync()Avoids promise overhead when your transforms/functions are synchronous.
Import only the stdlib modules you needKeeps bundle size and startup overhead down.
Use clearCache() sparinglyClearing caches is useful for churny workloads, but it throws away warm-state performance.

Benchmarks

Measured with vitest bench on Apple Silicon (M-series). Treat these as point-in-time guidance, not part of the API contract.

ExpressionExampleops/secµs/op
Simple literal42~30,000,0000.033
Arithmetic1 + 2 * 3~30,000,0000.033
Property accessuser.name~21,000,0000.048
Comparison + logicuser.age >= 18 && user.verified~11,000,0000.091
Transform pipelineuser.name |> upper~12,000,0000.083
Array operationsitems |> sum~17,000,0000.059

vs Jexl

ScenarioSpeedup
Default usage (parse + evaluate)88x faster
Pre-compiled literal3.2x faster
Pre-compiled comparison5.6x faster

Run benchmarks yourself: bunx vitest bench

Why it's fast

  • Per-instance caches - repeated expressions reuse parsed ASTs and compiled expression objects.
  • Compiler optimizations - Constant folding and dead branch elimination at compile time.
  • Zero dependencies - no runtime dependency graph to initialize or ship.
  • Hand-written parser - Recursive descent, no parser generators or regex-heavy tokenizers.

Optimization tips

Use compile() for repeated expressions. If the same expression runs in a loop or on every request, compile it once and reuse the compiled object. This skips parsing and optimization entirely.

ts
// Good: compile once, evaluate many
const rule = expr.compile('order.total >= freeShippingThreshold && order.country == "GB"')
for (const order of orders) {
  if (rule.evaluateSync({ order, freeShippingThreshold: 100 })) { /* ... */ }
}

Use evaluateSync over evaluate. The sync path avoids Promise overhead. Only use evaluate when you have async transforms.

Choose an appropriate cache size. The default (256) works well for most applications. If you have thousands of unique expression strings, increase it. If memory is tight, decrease it.

ts
// High-volume: larger cache
const expr = bonsai({ cacheSize: 1024 })

// Memory-constrained: smaller cache
const expr = bonsai({ cacheSize: 64 })

Most apps do not need benchmark chasing

The biggest wins come from reusing one instance, compiling repeated expressions, and avoiding async evaluation unless you genuinely need it.