Hello, my name is Dmitriy Karlovskiy, and I want to share with you the results of my 6-years research of reactivity to build a user interface.
It introduces a clear definition of reactivity, consisting of 6 bases. 4 fundamental requirements for the implementation of reactivity are introduced. Then 12 aspects of reactivity are analyzed in detail, which it is important when choosing a reactivity model. For each aspect, the behavior option that is most practical is selected. As a result, an idea is formed about how the most practical reactivity model should work. After that, well-known libraries and frameworks are taken, and compared according to the degree of approximation to the ideal.
This is a very large article written in Russian. I will keep it up to date. Therefore, I did not translate it manually and repost it, as it is very exhausting, and the result will quickly become outdated. So I suggest reading it either in the original or in an auto-translation:
However, if someone is willing to make and support an English version, I only welcome it. And, of course, be sure to let me know about any inaccuracies so that I correct them and do not mislead people.
Have fun reading, and may supersonic be with you!
Some spoilers..
Lib/FW | Style | Watch | Dupes | Origin | Tonus | Order | Flow | Error | Cycle | Depth | Atomic | Extern |
---|---|---|---|---|---|---|---|---|---|---|---|---|
$mol_wire | ๐คโ | ๐คโ | ๐ญโ | ๐โ | ๐ฆฅโ | ๐จโ๐ปโ | ๐โ | ๐ฆบโ | ๐โ | ๐ปโ | ๐ปโ | ๐โ |
CellX | ๐คโ | ๐คโ | ๐โ | ๐โ | ๐ฆฅโ | ๐จโ๐ปโ | ๐โ | ๐ฆบโ | ๐โ | ๐ปโ | ๐ฆถโ | ๐โ |
MobX | ๐คโ | ๐คโ | ๐โ | ๐โ | ๐ฆฅโ | ๐จโ๐ปโ | ๐โ | ๐ฆบโ | ๐โ | ๐ปโ | ๐ปโ | ๐โโ๏ธโ |
ChronoGraph | ๐งโ | ๐คโ | ๐โ | ๐โ | โฐโ | ๐จโ๐ปโ | ๐โ | โฎโ | ๐โ | ๐โ | ๐คผโโ๏ธโ | ๐โโ๏ธโ |
Reatom | ๐คฏโ | ๐คโ | ๐โ | ๐โ | ๐ฆฅโ | ๐งจโ | ๐ฆฝโ | โฎโ | ๐ซโ | ๐ปโ | ๐คผโโ๏ธโ | ๐โโ๏ธโ |
Effector | ๐คฏโ | ๐คโ | ๐โ | ๐ฅโ | ๐โ | ๐ฐโ | ๐ฆฝโ | ๐ฒโ | ๐คโ | ๐โ | ๐ปโ | ๐โโ๏ธโ |
RxJS | ๐คฏโ | ๐คโ | ๐ฏโโ๏ธโ | ๐ฅโ | ๐โ | ๐ฐโ | ๐ฆฝโ | โโ | ๐ซโ | ๐ปโ | ๐ปโ | ๐โโ๏ธโ |
Vue | ๐คโ | ๐คโ | ๐โ | ๐โ | ๐ฆฅโ | ๐จโ๐ปโ | ๐โ | ๐ฆบโ | ๐ฐโ | ๐ปโ | ๐ปโ | ๐โโ๏ธโ |
Ember | ๐คโ | ๐โ | ๐โ | ๐โ | ๐ฆฅโ | ๐จโ๐ปโ | ๐โ | ๐ฒโ | ๐โ | ๐งโ | ๐ปโ | ๐โโ๏ธโ |
Solid | ๐งโ | ๐คโ | ๐โ | ๐โ | ๐โ | ๐จโ๐ปโ | ๐โ | โโ | ๐ซโ | ๐ปโ | ๐ปโ | ๐โโ๏ธโ |
React | ๐งโ | ๐โ | ๐ฏโโ | ๐ฅโ | โฐโ | ๐จโ๐ปโ | ๐ฆฝโ | โโ | ๐โ | ๐งโ | ๐ปโ | ๐โ |
Angular | ๐งโ | ๐โ | ๐โ | ๐ฅโ | โฐโ | ๐จโ๐ปโ | ๐โ | ๐ฒโ | ๐ฐโ | ๐งโ | ๐ปโ | ๐โโ๏ธโ |
Svelte | ๐งโ | ๐โ | ๐โ | ๐ฅโ | โฐโ | ๐จโ๐ปโ | ๐โ | โโ | ๐ซโ | ๐โ | ๐ปโ | ๐โโ๏ธโ |
Top comments (15)
Very informative.
My project is not yet to be released but it almost meets all your "Practicality" level, except it uses
Object.is
by default (it's a performance concern, while you can set up custom comparator)Sounds interesting, what about the documentation?
I'm currently working on it. The APIs just become stable recently. (Actually I've started writing documents for months, and lots of problems were reflected so it was refactored several times). We can talk about details on discord maybe?
My voice english is too bad for discord.
You may be interested in the $mol_wire approach, where adding/removing a dependency takes O(1). In short: all cells store not only link to peer, but position of back link inside peer array too.
Typing text is OK ๐ I speak poor English as well. I use the same algorithm before (so does s-js, solidjs). The reason why I choose linked list is, the dependencies list are often stable and ordered (and often only one dependency). So the optimization is to reuse the old list as many as possible. Ideally it will be just several read without write.
And I don't have to remove one specific dependency. If I found there is a mismatch, I will give up all remaining history records and build a new linked list from current cursor (and the remaining records will be cleanup later, in batch.)
So you can haveadd/remove O(1) , but what about no add/remove at all๐
(also I don't get it why you emphasise O(1), is double linked list manipulation not O(1)?)
Wait I think there should be some contexts. I'm taking about dependency tracking but I think you are talking about something else. Because I'm not using a pubsub model, but scheduling and topological sort
A linked list is very expensive in memory and slow because of the garbage collector. And iterating on it is significantly more expensive than a conventional array.
In my approach, if the dependencies do not change, then the cursor just runs through the array and that's it, no allocations. Dependency replacement also happens inplace. Adding/removing at the end is the usual push/pop, which does not lead to unnecessary memory allocation. The only point is that inserting/removing dependencies in the middle requires replacing dependencies to the end, but this is done again within the framework of O(1) on dependency tracking. And again, it does not require unnecessary realocations.
But public discussion in the comments is useful not only for the two of us, but also for other readers.)
that is not true I got similar perf, even better.
You could argue in theory an array should be faster than linked list, but in javascript that really depends on a lot. If your array doesn't not store semi or double, the array is not that optimized, unlike in C you are just manipulating the memory. You could also argue that GC time isn't counted, but that's not a serious issue at all, we are using Javascriptโข๏ธ, not to write a real time program. You should blame React who use closure like free resources. (btw, doesn't Array cause GC?)
I do use the method as you described before, and I had done a lot of experiments and benchmarks then I decided to turn to linked list. Using array doesn't gives me significant perf boost (like >10%). And it's not the only factor of performance, a lot things can affect (e.g. inline cache, algorithm: iterative than recursive). But most important thing is, a linked list model gives much less mental overhead. And I even have more to explore (e.g. FWIW concurrent mode like React (fiber is linked list implemented)).
To sum up, array implementation doesn't give me much in return. So I didn't use it.
You should still elaborate what's mean by O(1). I still simply think it's normal.
It is also worth considering that this is a synthetic benchmark, where the nodes of a linked list are arranged sequentially. In reality, they will be scattered across memory, which is much slower.
I certainly don't use React. I have written a more efficient framework.)
Arrays allocate memory only when more elements need to be placed than have already been allocated for them. After "warming up", they usually stop allocating memory at all during the operation of the application.
I don't understand question about O(1).
I should have told you, a SMI_ELEMET (integer) array is optimized while normal array are not. But in your pub/sub.ts you definitely not just store a SMI (integer) but a hybrid of object and integer. That's why I use an object but not
i
directly. THIS CAN DROP PERFORMANCE A LOT.And I carefully read your code, pretty much the same algorithm (about tracking dependency) with minor difference ( and of course different implementations). While I have to consider more like topological sorting/ on-demand subscription and a linked list model make it much more clear. I just want to say linked list is fast, even though array could be the fastest. Anyway you didn't sell me the point. I think your code is good and high-quality, but not impressive to me (my personally take, from my personal experience, I've done exactly the same thing).
Let's end the topic. It's much less funny than your work on the conclusions of reactivity.
Now Chrome stores both references to objects and integers up to a billion in 4 bytes. See the table in my article.. So arrays with it is actually PACKED_SMI_ELEMENT.
latest node tested, it's PACKED_ELEMENTS.
Apart from this the test you posted is unfair. One is manipulating on array and another needs to access property which cause IC.And there are costs of object creation.
It's not C, how could you say a pointer is a number? Of course I know they are fixed-size.
( and if I remember correctly, 64-bit pointer should occupied 8 bytes, how is that a SMI?I find this but it doesn't mean a pointer could be a substitute of SMI. It's just about memory layout, but element kind is a strict assumption of data type, it has nothing to do with memory layout. Otherwise I could say everything is an object so everything could be stored like a SMI? Then what's the point to have element types.Maybe I was wrong at something, as the object could be already created then pushed, unlike a linked node must be created when inserting. But I have did a lot of experiments between different implementations and haven't see a significant perf gap. I think maybe this is not the main contributor of performance overhead. (e.g. 1ms comparing to 10ms is huge different but 1001ms comparing to 1010ms is negligible). And there are several features which rely on linked list(e.g. the unmatched records is not replaced but kept to the end and clean in batch).......I think there are someone else who would be interested in your impl so I will share it to them.
Topic ends indeed. I will not reply anymore. If any further arguments, you are right :) .
I just read through the auto-translated version of the post. Very impressive! You've clearly spent a lot of time thinking about this issue and coming up with categories. The diagrams were very helpful in understanding your ideas as well.
Would definitely love to see a fully-translated version of the post!