Skip to content

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 ​

LibraryRules areMin / gzip (browser)Static check against a schemaExplains resultsEvaluation budget (as documented)
bonsai-js/runtime/core 1.3.1, rules checked on a servertext, JS-like86.6 KB / 29.0 KBYes, where rules are savedYes, where rules are savedYes, on by default
bonsai-js 1.3.1, checking in the browsertext, JS-like206.7 KB / 69.2 KBYes, with suggestionsYesYes, on by default
@marcbachmann/cel-js 8.0.0text, CEL84.0 KB / 24.3 KBYesNoParse-time limits
@gorules/zen-engine 2.1.2text expressions, JSON decisionsNode native addon; WASM build belowYes (see notes)PartialGraph depth and function-node timeout
json-rules-engine 7.3.1JSON83.5 KB / 24.3 KBNoYesNo
json-logic-js 2.0.5JSON5.8 KB / 2.0 KBNoNoNo
jsonata 2.2.2text, JSONata84.0 KB / 26.5 KBNoNoNo
jexl 2.3.0text, JS-like20.4 KB / 6.1 KBNoNoNo
filtrex 3.1.0text, own syntax26.5 KB / 7.8 KBNoNoNo
expr-eval 2.0.2text, math-like26.4 KB / 8.0 KBNoNoNo
expr-eval-fork 3.0.3text, math-like27.9 KB / 8.4 KBNoNoNo

@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.

PartCostWhere it runsWhat 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 abort29.0 KBBrowser (runtime/core) and serverNo 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 engineup to 14.5 KB (0.6 KB for five common ones)Browser and serverjexl ships no functions (only transforms you write); expr-eval ships math functions
Type checker with "did you mean" suggestionsabout 13 KBServer, or a browser editorA misspelled field (user.agee >= 18) silently returns false
Explanations and partial evaluationabout 12 KBServer, 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".

Bonsaicel-jszen-enginejson-rules-enginejson-logic-jsjsonatajexlfiltrexexpr-eval
Static type check against a schemaYesYesYes (note 1)NoNoNoNoNoNo
"Did you mean" suggestionsYesNoNot observedNoNoNoNoNoNo
Editor completions or hover shippedYesNoYes (note 2)NoNoNoNoNoNo
Explains why a rule passed or failedYesNoPartial (note 3)YesNoNoNoNoNo
Partial evaluationYesNoNoNoNoNoNoNoYes (simplify)
Translation to SQL or MongoDBYesNoNoNoNoNoNoNoNo
Async host functionsYesYesPartial (note 4)Yes (async facts)NoYesYes (transforms)NoNo
Date, time, and time zonesYes, IANA zonesYes, IANA zonesYes, IANA zonesNoNoPartial (note 5)NoNoNo
SyntaxJS-likeCELZEN (and, or)JSONJSONJSONataJS-likeown (and, of)math-like
Rule stored astexttexttext; decisions as JSONJSONJSONtexttexttexttext
TypeScript typesbundled, inferred from schemabundledbundledbundled@types/json-logic-jsbundled@types/jexlbundledbundled
Runtime dependencies000 (plus a native binary per platform)4 direct, 7 installed001 (@babel/runtime)00
Works under a CSP that forbids evalYesYesNative on Node.js; the WASM glue uses new Function (note 6)Yes (note 7)YesYesYesNo, compiles with new FunctionYes, except toJSFunction()

expr-eval-fork matches expr-eval in every row of this table.

Notes:

  1. The WebAssembly build's VariableType.typeCheck reported Operator `+` cannot be applied to types `number` and `"x"` for user.age + "x", but typed the misspelled user.agee as Any without an error. The Node.js package's Workspace performs static type checking across policies and graphs (per its README; not exercised here).
  2. In Node.js, slotBatch returned the field types available after user.; the WebAssembly build's getCompletions() returned 87 function entries. GoRules also publishes a React decision editor (per its README).
  3. 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.
  4. Standalone expressions have no host function API in the type declarations; decision graphs can call an async customHandler for custom nodes (per index.d.ts).
  5. $fromMillis(..., "[H01]", "-0500") returned "07"; with "America/New_York" it returned "NaN".
  6. The wasm-bindgen glue in @gorules/zen-engine-wasm contains new Function(...). The Node.js package is a native addon.
  7. jsonpath-plus, a dependency, contains new Function for its eval: '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 "!".

ProbeResult
user.constructor, user.__proto__, "".constructorrejected 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 parenthesesTOO_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.

LibraryEvaluations per secondNote
Bonsai27.4 million
filtrex16.8 millioncompiled with new Function
cel-js12.6 million
expr-eval9.1 million
expr-eval-fork6.7 million
json-logic-js4.4 millionno compile step
jexl1.7 million
jsonata0.66 millionevaluate() returns a promise
zen-engine0.66 millionevaluateExpressionSync, which takes the source on every call
json-rules-engine0.10 millionasynchronous engine.run() per evaluation

Versions and releases ​

PackageVersion used (published)Newest on 2026-10-14
bonsai-js1.3.1 (2026-10-26), sizes only; features and throughput from commit e2d89dc
@marcbachmann/cel-js8.0.0 (2026-07-07)8.0.0
@gorules/zen-engine2.1.2 (2026-10-02)2.1.7
json-rules-engine7.3.1 (2025-02-20)7.3.1
json-logic-js2.0.5 (2024-07-09)2.0.5
jsonata2.2.2 (2026-07-16)2.2.2
jexl2.3.0 (2020-09-15)2.3.0
filtrex3.1.0 (2024-10-14)3.1.0
expr-eval2.0.2 (2019-09-28)2.0.2
expr-eval-fork3.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-engine 2.1.7 was newer but under 7 days old, so 2.1.2 was used. Bonsai was built from commit e2d89dc (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 with bun build <entry> --minify --target=browser and compressed with gzip level 9, the method of Bonsai's scripts/size-budget.ts. Each bundle was also run in Node.js and returned true; none was run in a browser. No library needed polyfills to bundle. The zen WebAssembly file is loaded with new 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( and eval(, 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).

Released under the MIT License.