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:
- mutations of an observed objects: proxying the changes, detecting if there are any interested observers/listeners, building and delivering the changes
- reading from observed arrays: detection of read property is performed in order to supply array mutation methods like
shift
,push
,splice
,reverse
etc - mutation of values that are objects / arrays: additional overhead comes from attaching / detaching those to the observed graph, proxying newcomers, revoking removed ones, creating internal system observers
Pay 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:
- CPU 2.6 GHz 6-Core Intel Core i7
- 16 GB 2667 MHz DDR4
- Creating in loop 100,000 observable from the object below, having few primitive properties, one non-observable nested object level 1 (Date), one nested object level 1, one nested object level 2 and one nested array level 1:
let person = {
name: 'Anna Guller',
accountCreated: new Date(),
age: 20,
address: {
city: 'Dreamland',
street: {
name: 'Hope',
apt: 123
}
},
orders: []
};
// creation, while storing the result on the same variable
for (let i = 0; i < creationIterations; i++) {
observable = Observable.from(person);
}
- Last observable created in previous step is used to mutate nested primitive property, while 2 observers added to watch for the changes, as following:
// 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;
}
- Then the same setup is used to add 1,000,000 nested primitive properties, as following:
for (let i = 0; i < mutationIterations; i++) {
observable.address.street[i] = i;
}
- Finally, those newly added properties are also being deleted, as following:
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).
CASE 2 - filling an array by pushing objects, mutating nested arrays of those, popping the array back to empty
- Pushing in loop 100,000 objects as below in an array nested 1 level:
let person = {
name: 'Anna Guller',
accountCreated: new Date(),
age: 20,
address: {
city: 'Dreamland',
street: {
name: 'Hope',
apt: 123
}
},
orders: []
},
dataset = {
users: []
},
observable = Observable.from(dataset); // the observable we'll be working with
// filling the array of users
for (let i = 0; i < mutationIterations; i++) {
observable.users.push(person);
}
- Mutating nested
orders
array from an empty to the below one:
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;
}
- Finally, the base
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.