Compared with Other Libraries
Several JavaScript libraries evaluate rules or expressions written outside your codebase. They make different trade-offs, and for many projects one of them is a better fit than Bonsai. This page compares them on facts measured on one machine or taken from each library's own documentation, so you can choose.
Everything here was measured on 2026-10-14 with the versions listed under Methodology, except Bonsai's sizes, which were measured again for 1.3.1 on 2026-10-26. Libraries change; if a row is out of date, please open an issue.
At a glance
| Library | Rules are | Min / gzip (browser) | Static check against a schema | Explains results | Evaluation budget (as documented) |
|---|---|---|---|---|---|
| bonsai-js/runtime/core 1.3.1, rules checked on a server | text, JS-like | 86.6 KB / 29.0 KB | Yes, where rules are saved | Yes, where rules are saved | Yes, on by default |
| bonsai-js 1.3.1, checking in the browser | text, JS-like | 206.7 KB / 69.2 KB | Yes, with suggestions | Yes | Yes, on by default |
| @marcbachmann/cel-js 8.0.0 | text, CEL | 84.0 KB / 24.3 KB | Yes | No | Parse-time limits |
| @gorules/zen-engine 2.1.2 | text expressions, JSON decisions | Node native addon; WASM build below | Yes (see notes) | Partial | Graph depth and function-node timeout |
| json-rules-engine 7.3.1 | JSON | 83.5 KB / 24.3 KB | No | Yes | No |
| json-logic-js 2.0.5 | JSON | 5.8 KB / 2.0 KB | No | No | No |
| jsonata 2.2.2 | text, JSONata | 84.0 KB / 26.5 KB | No | No | No |
| jexl 2.3.0 | text, JS-like | 20.4 KB / 6.1 KB | No | No | No |
| filtrex 3.1.0 | text, own syntax | 26.5 KB / 7.8 KB | No | No | No |
| expr-eval 2.0.2 | text, math-like | 26.4 KB / 8.0 KB | No | No | No |
| expr-eval-fork 3.0.3 | text, math-like | 27.9 KB / 8.4 KB | No | No | No |
@gorules/zen-engine-wasm 0.23.1, the browser build of the ZEN expression engine, is 12.7 KB / 4.1 KB of JavaScript plus a 1,736,841-byte WebAssembly file (469,794 bytes gzipped) fetched at startup. Its README describes it as experimental and expressions-only.
Where each one is the better choice
- json-logic-js is the smallest by far (2.0 KB gzipped, about 7% of Bonsai's browser runtime) and its rules are plain JSON, which suits a visual rule builder that emits JSON and a backend that stores it.
- json-rules-engine is a rules engine rather than an expression language: rules with events and asynchronous facts, stored as JSON, and every condition's fact value and result is reported back. Choose it when the problem is "fire events when conditions hold" and rules are edited as structured forms.
- @marcbachmann/cel-js implements the Common Expression Language, a specification with implementations in other languages (its README links the Go implementation). Choose it when the same expressions must also run in Go, Java, or C++ services, or when CEL is already used in your stack (for example in policy systems). It type-checks against declared types, is about a third of Bonsai's browser size, and is fast.
- @gorules/zen-engine is a full business rules engine with decision tables, decision graphs, policy documents, a visual editor, and bindings for Node.js, Python, Go, Java, Kotlin, and .NET (per its README). Choose it when business users should edit decision tables, when the same rules must run on several platforms, or when you want a hosted rules platform. On Node.js it is a native addon (the darwin-arm64 binary is 21 MB on disk); in the browser it needs a 470 KB gzipped WebAssembly file.
- jsonata is a query and transformation language for JSON. Choose it when users reshape documents, not only test conditions. It is asynchronous and its date functions take UTC offsets rather than time zone names.
- filtrex compiles expressions to JavaScript functions and was the second fastest in the throughput test. It suits short filters in apps whose Content Security Policy allows
new Function. - jexl is small (6.1 KB) with JavaScript-like syntax and asynchronous transforms. Its last npm release was in 2020.
- expr-eval and expr-eval-fork suit math formulas and offer partial evaluation through
simplify().
Bonsai's weaknesses. It is not the smallest. Rules checked on a server and evaluated in the browser with bonsai-js/runtime/core take 29.0 KB gzipped, about 1.2 times cel-js and json-rules-engine and 1.1 times jsonata; checking rules in the browser takes 69.2 KB (56.9 KB from bonsai-js/lite, without explaining and partial evaluation). jexl, expr-eval, filtrex, and json-logic-js are 2 to 8 KB. It is new. It runs only in JavaScript, so a Go or Python service cannot evaluate the same rules. It needs Node.js 22 or newer and is ESM-only. It has no decision tables or visual rule editor of its own (it provides a syntax tree, a printer, and a language service to build one). And it is not a standard: CEL expressions are portable to other CEL implementations, Bonsai expressions are not.
Where Bonsai fits. Bonsai is for text expressions written by people other than your developers, where you want to reject a broken rule when it is saved (with "did you mean" suggestions and exact ranges), show the author why a rule passed or failed, bound every evaluation deterministically, and optionally run the same filter in SQL or MongoDB. It is the only library measured here that does all of these in one package.
What Bonsai's size buys
Size matters in the browser; on a server, none of these sizes is noticeable. Each row is the difference between two measured Bonsai 1.3.1 bundles (gzipped), so the costs add up to the entry sizes above.
| Part | Cost | Where it runs | What jexl or expr-eval do instead |
|---|---|---|---|
| Core: parser, compiler, values (dates, durations), the step budget and size limits, blocked names, run-time type guards, artifact validation, async host calls with timeouts and abort | 29.0 KB | Browser (runtime/core) and server | No documented evaluation budget; a large input or host transform runs as long as it takes |
| Built-in functions, only those a rule calls: 87 for text, lists, maps, numbers, dates and time zones, and a linear-time regex engine | up to 14.5 KB (0.6 KB for five common ones) | Browser and server | jexl ships no functions (only transforms you write); expr-eval ships math functions |
| Type checker with "did you mean" suggestions | about 13 KB | Server, or a browser editor | A misspelled field (user.agee >= 18) silently returns false |
| Explanations and partial evaluation | about 12 KB | Server, or the browser (bonsai-js) | No explanation of a result; expr-eval has partial evaluation (simplify()) |
cel-js fits a type checker and evaluator in 24.3 KB, so a smaller design is possible; it has no evaluation budget, explanations, or SQL translation, and its rules are CEL rather than JavaScript-like. See Runtime for choosing built-ins.
Authoring features
Verified by running code unless the note says "per docs".
| Bonsai | cel-js | zen-engine | json-rules-engine | json-logic-js | jsonata | jexl | filtrex | expr-eval | |
|---|---|---|---|---|---|---|---|---|---|
| Static type check against a schema | Yes | Yes | Yes (note 1) | No | No | No | No | No | No |
| "Did you mean" suggestions | Yes | No | Not observed | No | No | No | No | No | No |
| Editor completions or hover shipped | Yes | No | Yes (note 2) | No | No | No | No | No | No |
| Explains why a rule passed or failed | Yes | No | Partial (note 3) | Yes | No | No | No | No | No |
| Partial evaluation | Yes | No | No | No | No | No | No | No | Yes (simplify) |
| Translation to SQL or MongoDB | Yes | No | No | No | No | No | No | No | No |
| Async host functions | Yes | Yes | Partial (note 4) | Yes (async facts) | No | Yes | Yes (transforms) | No | No |
| Date, time, and time zones | Yes, IANA zones | Yes, IANA zones | Yes, IANA zones | No | No | Partial (note 5) | No | No | No |
| Syntax | JS-like | CEL | ZEN (and, or) | JSON | JSON | JSONata | JS-like | own (and, of) | math-like |
| Rule stored as | text | text | text; decisions as JSON | JSON | JSON | text | text | text | text |
| TypeScript types | bundled, inferred from schema | bundled | bundled | bundled | @types/json-logic-js | bundled | @types/jexl | bundled | bundled |
| Runtime dependencies | 0 | 0 | 0 (plus a native binary per platform) | 4 direct, 7 installed | 0 | 0 | 1 (@babel/runtime) | 0 | 0 |
Works under a CSP that forbids eval | Yes | Yes | Native on Node.js; the WASM glue uses new Function (note 6) | Yes (note 7) | Yes | Yes | Yes | No, compiles with new Function | Yes, except toJSFunction() |
expr-eval-fork matches expr-eval in every row of this table.
Notes:
- The WebAssembly build's
VariableType.typeCheckreportedOperator `+` cannot be applied to types `number` and `"x"`foruser.age + "x", but typed the misspelleduser.ageeasAnywithout an error. The Node.js package'sWorkspaceperforms static type checking across policies and graphs (per its README; not exercised here). - In Node.js,
slotBatchreturned the field types available afteruser.; the WebAssembly build'sgetCompletions()returned 87 function entries. GoRules also publishes a React decision editor (per its README). - A decision evaluated with
{ trace: true }reports each node's input and output and each named expression's result, not the sub-expressions inside one expression. - Standalone expressions have no host function API in the type declarations; decision graphs can call an async
customHandlerfor custom nodes (perindex.d.ts). $fromMillis(..., "[H01]", "-0500")returned"07"; with"America/New_York"it returned"NaN".- The wasm-bindgen glue in
@gorules/zen-engine-wasmcontainsnew Function(...). The Node.js package is a native addon. jsonpath-plus, a dependency, containsnew Functionfor itseval: 'native'mode; its default is'safe', and json-rules-engine does not change it.
How a misspelled field behaved without a schema check (user.agee >= 18 with user.age = 20): jexl, json-logic-js, and expr-eval returned false; jsonata returned undefined; json-rules-engine did not fire the rule; filtrex returned an Error value at evaluation time; zen-engine threw Opcode Compare: Unsupported type at evaluation time.
Bonsai's guarantees under hostile input
Each probe ran against Bonsai in its own Node.js process. xs is a list of 1,000 numbers, and s is "a" repeated 32 times followed by "!".
| Probe | Result |
|---|---|
user.constructor, user.__proto__, "".constructor | rejected when parsed: Property "constructor" is not accessible |
user["constructor"]["constructor"] | null |
map nested three deep over xs (10^9 operations) | STEP_LIMIT after 8 ms |
s.matches("(a+)+$") | false in 2 ms (linear-time matcher) |
"x".repeat(1000000000) | STRING_LIMIT in 2 ms |
| 10,000 nested parentheses | TOO_DEEP in 2 ms |
The step budget is on by default (1,000,000 steps per evaluation) and deterministic, so the same expression over the same data stops at the same point on any machine. See Safety and Limits.
For the other libraries, the "Evaluation budget" column above records what each documents: cel-js documents parse-time structural limits (maxAstNodes, maxDepth, list and map sizes); zen-engine documents a maxDepth option for decision graphs and a functionTimeoutMillis setting for JavaScript function nodes.
Throughput
user.age >= 18 && user.plan == "pro" in each library's syntax, compiled once where the library allows it, evaluated 200,000 times alternating a passing and a failing context after 20,000 warm-up calls. One run on one machine; treat the numbers as indicative only.
| Library | Evaluations per second | Note |
|---|---|---|
| Bonsai | 27.4 million | |
| filtrex | 16.8 million | compiled with new Function |
| cel-js | 12.6 million | |
| expr-eval | 9.1 million | |
| expr-eval-fork | 6.7 million | |
| json-logic-js | 4.4 million | no compile step |
| jexl | 1.7 million | |
| jsonata | 0.66 million | evaluate() returns a promise |
| zen-engine | 0.66 million | evaluateExpressionSync, which takes the source on every call |
| json-rules-engine | 0.10 million | asynchronous engine.run() per evaluation |
Versions and releases
| Package | Version used (published) | Newest on 2026-10-14 |
|---|---|---|
| bonsai-js | 1.3.1 (2026-10-26), sizes only; features and throughput from commit e2d89dc | |
| @marcbachmann/cel-js | 8.0.0 (2026-07-07) | 8.0.0 |
| @gorules/zen-engine | 2.1.2 (2026-10-02) | 2.1.7 |
| json-rules-engine | 7.3.1 (2025-02-20) | 7.3.1 |
| json-logic-js | 2.0.5 (2024-07-09) | 2.0.5 |
| jsonata | 2.2.2 (2026-07-16) | 2.2.2 |
| jexl | 2.3.0 (2020-09-15) | 2.3.0 |
| filtrex | 3.1.0 (2024-10-14) | 3.1.0 |
| expr-eval | 2.0.2 (2019-09-28) | 2.0.2 |
| expr-eval-fork | 3.0.3 (2026-02-02) | 3.0.3 |
Methodology
- Date and machine. 2026-10-14, Apple M5 (10 cores, 24 GB), macOS (Darwin 25.5.0), Node.js 24.13.0, Bun 1.3.14, npm 11.19.0. The machine was shared; the 1-minute load average was between 3 and 8 during measurements.
- Versions. For each library, the newest version published at least 7 days before the measurement date, installed with
npm install --ignore-scripts --save-exact <pkg>@<version>into its own directory.@gorules/zen-engine2.1.7 was newer but under 7 days old, so 2.1.2 was used. Bonsai was built from commite2d89dc(bun install --frozen-lockfile --ignore-scripts,bun run build,npm pack) and installed from the tarball. - Bundle size. A minimal consumer per library evaluates the rule above (for
bonsai-js/runtime/core, the rule's artifact, compiled on the server and embedded as JSON; the rule calls no built-in function), bundled withbun build <entry> --minify --target=browserand compressed with gzip level 9, the method of Bonsai'sscripts/size-budget.ts. Each bundle was also run in Node.js and returnedtrue; none was run in a browser. No library needed polyfills to bundle. The zen WebAssembly file is loaded withnew URL(..., import.meta.url)and was measured separately. - Features. Each "Yes" or "No" comes from a script that calls the library, except where a note says "per docs" or cites the README or type declarations. "No" for features such as partial evaluation or SQL means no such API appears in the package's README or type declarations; companion packages from other authors were not evaluated.
- eval. The shipped JavaScript of each package and its installed dependencies was searched for
new Function(andeval(, and every match was read. - Bonsai probes. Each probe ran as
perl -e 'alarm 10; exec @ARGV' node probe.mjs <name>. - Throughput. See Throughput. Bonsai's own benchmark against cel-js over several workloads is
bun run bench(benchmarks/vs-cel.bench.ts).