Skip to content

Performance ​

The fork does less work per query than objection.js 3.1.5. The generated SQL and the results are the same; the difference is in the work objection does around the database.

What is faster ​

ChangeWhat it affects
Bound classes share their validatorA class bound to a knex instance with bindKnex() or bindTransaction(), which happens for every transaction that uses them, used to create a validator of its own: two Ajv instances and a new compile of the schema. It now uses the validator of the class it is bound from.
Faster deep clonetoJSON() clones the object and array attributes of every model, and validation clones the input when the schema has default values. Every value is now cloned without lodash, with the same result as lodash's cloneDeep, and the package no longer ships a copy of it.
Schema defaults checked onceValidation used to walk the whole schema on every validate to find out whether it has default values. The answer is now cached per schema.
No relation parsing without relationsfromJson() and the other methods that turn JSON into models skip the relation parsing, and its allocations, when the JSON has no relations.
Joined rows parsed without string keyswithGraphJoined() used to build and hash a key like /1/pets/3/toys/7 for every table of every row. Models are now looked up by id in a map per parent and relation.
Static hooks only when they existEvery query used to create the arguments of the static hooks (beforeFind, afterInsert, ...) and call them, even when the model doesn't define them.
Operation walks without allocationsA query walks its list of operations about twenty times while it is built and executed. The walks no longer allocate functions, and checking whether a query is a find takes one walk instead of five.
Faster keys for idsRelations and graphs are matched by keys made from ids. Numbers and strings, which almost all ids are, are now converted first.
Hooks run without promises (4.1.3)Every phase of a query (onBefore1 ... onAfter3, runBefore(), runAfter()) used to chain promises and await each hook, about 40 promises per query, even with no hooks. The hooks now run one after another and only wait when a hook returns a promise.
Graph inserts in one query per table on MySQL (4.1.2)insertGraph() and upsertGraph() inserted one row per query on MySQL. They now insert the rows of a table together, as on postgres: a person with 2 pets with 2 toys each takes 3 inserts instead of 7.

Benchmarks ​

Measured with the benchmarks in the repository on Node.js 24. They were run against 4.0.0-alpha.0, the release of the fork before these changes. That release has the same implementation of these parts as objection.js 3.1.5.

OperationBeforeAfterChange
A validated insert in a new transaction (bindKnex())1.44 ms122.7 µs−91.5%
Validating 1000 models with default values2.71 ms1.23 ms−54.8%
toJSON() of 1000 models with JSON attributes2.34 ms1.31 ms−44.0%
withGraphJoined('pets.toys') of 100 persons1.51 ms1.21 ms−19.5%
findById()34.4 µs27.7 µs−19.4%
insert() of 1 row14.4 µs11.8 µs−18.6%
upsertGraph() of 20 persons with pets3.97 ms3.38 ms−14.7%
A find returning 1 row18.0 µs15.6 µs−13.6%
insertGraph() of 50 persons with pets, validated1.17 ms1.02 ms−12.6%
A batch insert() of 100 rows181.9 µs161.2 µs−11.4%
Building a query with expressions56.9 µs52.1 µs−8.4%

Cloning without lodash was measured separately, against the fork just before the change:

OperationBeforeAfterChange
toJSON() of 1000 models whose JSON attributes hold a class instance, Map and Set24.91 ms3.19 ms−87.2%

Models whose JSON attributes hold only plain objects, arrays and dates are cloned as before.

These measure objection's own work, against a mocked database. With a real database, the database's time comes on top: reading 1000 rows from SQLite, for example, is mostly SQLite's time and the JSON.parse() of the JSON columns, and is about 6% faster. The full results, including the end-to-end runs on SQLite, are in bench/README.md.

Since 4.1 ​

Measured as the CPU instructions per operation (bench/count.js), which unlike times barely depend on what else runs on the machine:

The change of 4.1.3, against 4.1.1Change
findById()−27.2%
delete() with a where clause−14.2%
A find returning 1 row−11.5%
insert() of 1 row−10.5%
findById() on SQLite−7.4%

The scenarios that are dominated by rows, graphs and validation are unchanged by it. On a real MySQL, the batched graph inserts of 4.1.2 made a nested insertGraph() (a person with 2 pets with 2 toys each, in a transaction) go from 29% slower than objection.js 3.1.5 to 16% faster: it runs 5 queries instead of 9.

Compared with other ORMs ​

bench/orms runs the same scenarios on a real MySQL 8.4 through the mysql2 driver with this fork, objection 3.1.5 and 4.4.2, raw knex, kysely, drizzle-orm, sequelize, typeorm and mikro-orm, each with its own API. These are the results of 4.1.1, before the speed-ups above, on Linux with Node.js 24.17 and MySQL 8.4 in Docker, median time per operation:

ScenarioThis forkobjection 3.1.5objection 4.4.2Fastest other ORMknex (no ORM)
Find by id257.6 µs260.0 µs285.2 µstypeorm, 281.7 µs194.6 µs
Find 100 rows¹411.6 µs433.9 µs531.7 µsdrizzle-orm, 451.6 µs291.3 µs
Find 1000 rows¹1.40 ms1.95 ms2.26 msdrizzle-orm, 2.41 ms914.1 µs
Delete by id243.9 µs307.5 µs297.6 µsmikro-orm, 303.4 µs178.9 µs
Eager loading, separate queries5.39 ms7.52 ms7.89 msdrizzle-orm, 6.97 ms4.84 ms
Eager loading, one query5.44 ms6.81 ms7.93 mstypeorm, 7.62 ms3.04 ms
Batch insert of 100 rows8.92 msnot on MySQLnot on MySQLsequelize, 7.37 ms6.32 ms

¹ Measured again for this fork, both objection versions and knex on a quiet machine; the others are from the first run.

Among the ORMs, the fork was clearly the fastest at finding 1000 rows, deleting and eager loading, and even with objection.js 3.1.5 at finding by id and finding 100 rows: there, the differences are within what changes between runs. kysely, a query builder, is faster than knex itself on the simplest queries: the fork builds on knex, so knex's own work per query is part of every query. Writes of single rows, updates and transactions take mostly MySQL's commit to disk, about 4 ms here, and the ORMs were close to each other there. Times on a database depend on the machine and on what else runs on it; run the benchmark on your own setup before relying on these numbers.

The TypeScript build ​

The source is TypeScript, compiled to CommonJS by tsc. The compiled code does the same work as the JavaScript it replaced, and it's as fast. Against the last JavaScript build, 7 rounds of every benchmark scenario, the largest differences were within noise:

OperationJavaScriptTypeScriptChange
A batch insert() of 100 rows118.9 µs118.7 µs−0.2%
toJSON() of 1000 models979.6 µs969.4 µs−1.0%
Validating 1000 models with default values919.3 µs919.7 µs+0.0%
withGraphJoined('pets.toys') of 100 persons821.5 µs815.0 µs−0.8%
Reading 1000 rows1.25 ms1.27 ms+1.2%

4.0.0-beta.6 to beta.8 were up to 20% slower, because two modules exported getters (see Contributing for why that matters); beta.9 fixed it.

Eager loading on MySQL ​

4.0.0-alpha.0 ran every query of withGraphFetched() and fetchGraph() one at a time on MySQL, along with the graph writes, which deadlocked when they ran concurrently. Reads don't deadlock, so eager loading runs its queries 4 at a time again, as in objection.js 3.1.5. Graph writes still run one at a time on MySQL, but since 4.1.2 they insert the rows of a table in one query. With 1 ms per query, withGraphFetched('[pets.toys, movies]') takes 23.5% less time than in 4.0.0-alpha.0.

The number of queries doesn't change: eager loading runs one query per relation in the expression for all the models, not one per model.

Running the benchmarks ​

sh
npm run bench                                   # all scenarios
npm run bench -- --filter graph                 # scenarios whose name matches
npm run bench:compare -- --baseline ../other    # compare two checkouts
node bench/sql-check.js --baseline ../other     # check that the SQL and results are the same
node bench/count.js --baseline ../other         # CPU instructions, for busy machines
npm run bench:orms                              # against other ORMs on MySQL

bench/README.md explains how to set them up.

MIT Licensed. Built on objection.js by Sami Koskimäki and its contributors.