object-observer
is purposed to be a low-level library.
It is designed to track and deliver changes in a synchronous way, being async possible as opt in.
As a such, I’ve put some effort to optimize it to have the least possible footprint on the consuming application.
Generally speaking, the framework implies some overhead on the following, when operating on observed data sets:
shift
, push
, splice
, reverse
etcPay attention: each and every object / array (including all the nested ones) added to the observed tree processed by means of cloning and turning into observed one; in the same way, each and every object / array removed from the observed tree is being ‘restored’ (proxy revoked and cloned object returned, but not to the actual original object).
Tests described below are covering most of those flows.
Overall, object-observer
’s impact on the application is negligible from both, CPU and memory aspects.
All of the benchmarks below were performed on MacBook Pro (model 2019, Ventura 13.2.1), plugged in at the moment of tests:
// creation, while storing the result on the same variable for (let i = 0; i < creationIterations; i++) { observable = Observable.from(person); }
2. Last observable created in previous step is used to __mutate__ nested primitive property, while 2 observers added to watch for the changes, as following:
```javascript
// add listeners/callbacks
Observable.observe(observable, changes => {
if (!changes.length) throw new Error('expected to have at least one change in the list');
else changesCountA += changes.length;
});
Observable.observe(observable, changes => {
if (!changes) throw new Error('expected changes list to be defined');
else changesCountB += changes.length;
});
// deep mutation performed in a loop of 1,000,000
for (let i = 0; i < mutationIterations; i++) {
observable.address.street.apt = i;
}
for (let i = 0; i < mutationIterations; i++) {
observable.address.street[i] = i;
}
for (let i = 0; i < mutationIterations; i++) {
delete observable.address.street[i];
}
All of those mutations are being watched by the listeners mentioned above and the counters are being verified to match the expectations.
Below are results of those tests, where the time shown is of a single operation in average. All times are given in ‘ms’, meaning that cost of a single operation on Chromiums/NodeJS is usually half to few nanoseconds. Firefox values are slightly higher (worse).
create observable 100,000 times |
mutate primitive depth L3; 1M times |
add primitive depth L3; 1M times |
delete primitive depth L3; 1M times |
|
---|---|---|---|---|
98 | 0.001 ms | 0.0004 ms | 0.0006 ms | 0.0005 ms |
80 | 0.001 ms | 0.0004 ms | 0.0006 ms | 0.0005 ms |
74 | 0.0047 ms | 0.0007 ms | 0.0007 ms | 0.0011 ms |
18.14.2 | 0.0016 ms | 0.001 ms | 0.001 ms | 0.001 ms |
// filling the array of users for (let i = 0; i < mutationIterations; i++) { observable.users.push(person); }
2. __Mutating__ nested `orders` array from an empty to the below one:
```javascript
let orders = [
{id: 1, description: 'some description', sum: 1234, date: new Date()},
{id: 2, description: 'some description', sum: 1234, date: new Date()},
{id: 3, description: 'some description', sum: 1234, date: new Date()}
];
for (let i = 0; i < mutationIterations; i++) {
observable.users[i].orders = orders;
}
users
array is being emptied by popping it to the end:
for (let i = 0; i < mutationIterations; i++) {
observable.users.pop();
}
All of those mutations are being watched by the same 2 listeners from CASE 1 and the counters are being verified to match the expectations.
push 100,000 objects | replace nested array 100,000 times | pop 100,000 objects | |
---|---|---|---|
98 | 0.002 ms | 0.003 ms | 0.0008 ms |
98 | 0.002 ms | 0.003 ms | 0.0008 ms |
74 | 0.0077 ms | 0.0096 ms | 0.0011 ms |
18.14.2 | 0.005 ms | 0.005 ms | 0.001 ms |