On "The Future of JavaScript MVC Frameworks"
blog.reverberate.org
blog.reverberate.org
Take a look at my undo example implemented in about 5 lines of relevant code http://swannodette.github.io/todomvc/labs/architecture-examp.... Note the render time when undoing changes, much less than 16ms. That's undo playback at 60 frames per second.
The ability to snapshot the application state is not a small thing, it's a global application concern with lots of benefits besides user level undo - UI rollback on network request failure, app state restoration sans boilerplate when the user cancels something, simpler model for offline applications, and UIs based on scrubbing come immediately to mind.
I'm sure you could achieve similar or better performance in a system that relies on events with batching and mutable objects but I'm not convinced that it can be done as simply as we can do it today in Om.
I don't completely understand how entirely immutable data can be effectively used in front end apps (I do understand how, if you have immutable data, React can be made fast and undo is easy). If data changes, it needs to mutate, right? Do you then maintain one global object that does change, and have that object be a giant immutable tree with all model data? Or am I completely missing the approach?
If that's the way, how can you efficiently do updates on relatively large chunks of data? Say, I'm coding Gmail and my user wants to star the 2053rd email in a list? Doesn't that mean 2052 items in a list have to be copied? In general, doesn't this make model code much much more complicated? Or am I missing something and is it really not that bad?
Yeah, a bit many question marks, but I hope you catch my drift :)
If anyone could point me to some good reading about thinking in immutable data terms, that would be warmly appreciated too. Thanks!
[1] http://www.infoq.com/presentations/Are-We-There-Yet-Rich-Hic... [2] http://www.infoq.com/presentations/Simple-Made-Easy
That said, on to answering your questions:
> If data changes, it needs to mutate, right? ...
Data doesn't change in this model (see [1]). Incoming events (keyup, network request, page load) occur and your program responds to those events by producing a new, immutable set of data that you're going to use going forward. Om apps keep one reference to the root of the data tree–Om examples use app-state as the name–which represents the official current state of the data. You can reasonably argue that swapping out app-state is changing data except that anything that had a reference to the previous root(s) still has that reference and you have to go dereference app-state to get the current value instead of having it changed out from under you.
> If that's the way, how can you efficiently do updates on relatively large chunks of data? ... Or am I missing something and is it really not that bad?
[3] http://eclipsesource.com/blogs/wp-content/uploads/2009/12/cl...
Clojure(script)'s data structures use structural sharing. The above picture shows inserting a single node into the middle of the structure. The red outlined nodes are the parents which need to be copy+updated, as shown on the right with all the dotted lines being shared references. The most misleading thing about the picture is that the actual cljs trees have 32 branches at each node instead of 2 or 3 so the lookup time is log32 N (basically constant [4], impl in a systems language vs classic datastructures for comparison). In your Gmail example you'd have to make ~3 new nodes.
[4] https://github.com/michaelwoerister/rs-persistent-datastruct...
> In general, doesn't this make model code much much more complicated?
It requires a different mindset and generally some helper code. In Clojure using Om it's pretty straightforward once you're over the initial hurdle. In javascript using Mori [5] it looks a lot like awkward Backbone with very heavy Underscore use. I've poked around at it and if I were going to try to adopt Mori+React for a real project I'd want to do some quality of life tweaks on Mori. Mostly setting a prototype on the Mori objects to get the feel closer to Backbone+Underscore and trying to get the data structure console output to be more useful.
I think I'm catching the drift here. This app-state variable was the concept I was missing. That, and how smart the Clojure(Script) data structures really are.
Mori looks pretty damn nice, actually. Would consider using it in practice.
Updating the 2053rd email in an immutable vector is not going to copy the previous 2052 items. ClojureScript data structures are modeled directly on Clojure's - structural sharing avoids needless copying.
http://www.infoq.com/presentations/Value-Identity-State-Rich...
So yes in Om there is a single mutable point which then points to the current immutable tree to be used for rendering.
This is very fast. Clojurescript performance is not a bottleneck, most of the time is spent within React.
The main reason I dug into this so deep is because after reading your article I had a nagging feeling that using anything other than React or immutable data structures was an architectural dead end. But I'm not so worried about that now; I think that any framework that aggressively batches DOM updates will probably be ok performance-wise, and I think that mutable data structures can perform well.
Also I think that some features are easier to provide with mutable structures, such as automatic two-way data binding a la Angular. How do you propagate DOM changes to your models? Manual event handlers or do you have something more automatic?
I definitely think that immutable data structures are worth consideration for their snapshotting ability though.
It would be nice to see a comparison of React/Om with some less naive approach to handling Backbone events.
In my own apps, for instance, I've dealt with rapid batch DOM updates simply by putting a short timeout on the event handler and canceling it if another draw-inducing event arrived in the timeframe (5ms, let's say). Doing that has led to similar order-of-magnitude speedups, and while not as sophisticated as using requestAnimationFrame, has been sufficient and maintainable.
The implementation is simple; when render() is called, the render is queued up. Every ~16ms (RAF or setTimeout), any pending renders are executed. If another render() is called within that time frame, the earlier is dropped.
So long as you expect an async API and listen only to events/hooks ('beforeRender', 'afterRender'), it works pretty well. Backbone.LayoutManager supports it fully and it is configurable in case you rely on synchronous renders, which can be helpful in tests.
1. https://github.com/tbranyen/backbone.layoutmanager/pull/421
http://drfloob.github.io/todomvc/architecture-examples/react...
Benchmark to your heart's content! On my system, Om is about 4x faster on benchmark 1, and 1000x faster on benchmark 2. I don't think requestAnimationFrame has a lot to do with it, and immutability only slightly more; I think the real performance gain is in having an application data policy tailored to make the most of React's behavior.
To plug my own work a bit, I built a TodoMVC example with React and my own immutable data structure library in pure JS. It performs a lot like Om on both benchmarks, and actually seems a bit snappier in places, like toggling and clearing all completed todos:
http://drfloob.github.io/todomvc/labs/dependency-examples/re...
However when I profiled your benchmark it seems like it is invoking React's event loop more than I would expect (and more than the React/Om benchmark does) -- do you have any idea why this would be? It seems like for this to be apples to apples, it shouldn't be invoking React's update logic (ie. re-rendering everything) until the end of the benchmark. Perhaps this difference is because of requestAnimationFrame?
I don't understand this comment:
> I don't think requestAnimationFrame has a lot to do with it, and immutability only slightly more; I think the real performance gain is in having an application data policy tailored to make the most of React's behavior.
As I mentioned in my article, "Benchmark 2" is a no-op on the DOM. So literally all React should be doing is calling render() before the benchmark (which returns basically nothing), letting the entire benchmark run, then calling render() again (in which it again returns basically nothing).
In other words, I don't see what the "application data policy" has to do with making React efficient in this case; all we're asking React to do here is calculate a no-op diff and then do nothing.
I think so, yes. The performance difference is already negligible with Om, so I didn't see a real need to optimize it any further. Pete Hunt's react-raf-batching (mentioned already) could probably be dropped in to get that last bit of optimization, but I haven't tried.
As for what I mean by "application data policy", I think that if you work with your application's data in such a way that it batches modifications, renders entirely from the top down, and uses fast `shouldComponentUpdate` implementations, you'd achieve most of the improvement you see in the Om vs React+Backbone TodoMVC comparison. Lots of things could achieve that. Immutability isn't a hard requirement to get those features done. And if my React+_tree example is any indication, requestAnimationFrame isn't buying you that much performance either.
I haven't really analyzed what Om's Benchmark 2 was doing under the hood, so thank you for that. I assumed it was doing something, but with the ~4ms benchmark, I just assumed that particular something was wizardry.
Sorry, I was unclear: my comments were about the (slow) first benchmark you posted that is using React but not using your library. I think it would be orders of magnitude faster with requestAnimationFrame.
----------------------------
Good call! Benchmark 1 is about 33x faster in my browser (with the setTimeout fallback being used, I think).~
http://drfloob.github.io/todomvc/labs/architecture-examples/...
This is the same React TodoMVC example with a different `react-with-addons.js`, built using React v0.9.0 and https://github.com/petehunt/react-raf-batching.
What jumps out most is that I'd really miss batch mode, which lets you escape from immutability and get a big performance boost for complex atomic operations. I think Mori must use something like this internally, but the docs don't indicate it being exposed.
It may be subtle, but I also really prefer _tree's syntax, where objects are fitted with their own methods (Mori does something like `mori.get(m0, 'foo')` instead of the more succinct `m0.get('foo')`). This is also the backbone of _tree's data modeling layer, which lets you work in terms of your domain, rather than a tree.
Performance comparisons are on my list of things to do.
Pete Hunt did just that:
https://github.com/petehunt/morimodel/
Mori is sophisticated under the hood. It doesn't do massive amounts of copying or anything like that, so I don't think you need a "batch mode" to make it perform. Perhaps I'm missing something though.
Edit: I took a quick look at _tree's code. It's quite different from how I understand Mori to work. Mori exposes Clojure's data structures which implement their own immutable maps, lists and vectors in a way that is very efficient for copying (both in time and space).
_tree leans heavily on functional programming techniques, so it's not so different at all. The furthest it gets from functional is the modeling layer implementation, which is prototypal, and still just a thin layer over the core.
I haven't studied any clojurescript, but conceptually, compound operations on immutable structures, such as re-parenting a subtree or sorting a vector/list/set, would be implemented inefficiently without mutable intermediate objects. For example, imagine writing quicksort where exchanges cost O(n) instead of O(1). Performance would totally tank. It'd be silly.
That's why I expect clojurescript has mutable intermediates. I'd love for someone to prove this guess wrong, it would blow my mind. Anyway, that's what _tree's batch mode is, in essence: exposed access to the mutable, pre-finalized versions of _tree primitives.
Not really funny come to think of it :(
But yes, I heared 3 to 4 should be a PITA
Fun fact: Angular is older than Backbone (first commit to angular was four years ago).
But maybe I just don't like JS.. Recently I was trying to use Fay (http://fay-lang.org a proper subset of Haskell that compiles to JS) and I loved it. Now if only I can use it with some sort of FRP library :)
Benchmark 1) This benchmark deals with adding a bunch of todos and seeing how fast they render. The major performance impact here seems to be jQuery. Specifically jQuery event delegation. I was surprised to find that binding DOM events in jQuery are incredibly slow, in fact the majority of the time in that benchmark is spent event binding. For comparison have a look at exoskeleton, a Backbone fork which removes the jQuery dependency and uses the native DOM methods instead. It's also available on todomvc.com and uses pretty much the same code as the Backbone todo implementation.
Given the following benchmark :
var benchmark = function() {
app.todos.reset();
var s = [];var i=0; while (i<1000) {s.push({title:'foo'});i++};
var t = performance.now();
app.todos.reset(s);
return performance.now() - t;
}
Backbone: (http://todomvc.com/architecture-examples/backbone/) >>benchmark()
>>667.2439999965718
Exoskeleton: http://todomvc.com/labs/architecture-examples/exoskeleton/ >>benchmark()
>>226.4119999963441
It's worth mentioning that Backbone has a couple pull requests open that is meant to address this.Benchmark 2) This benchmark deals with toggling all of the todos events at once. It builds up 200 todos and toggles them on and off 5 times.
The problem lies with using 'all' and 'change' events way too liberally within the todo application. Which causes a ton of needless render calls. During the course of this benchmark the main view has render called on it 6000 times (!) while the child views have render called on them for a total of 3000 times. I'm actually surprised that Backbone is as fast as it is given those type of numbers. What should happen is the main view should only need to render a total of 5 times (once for each toggle) and the child views 1000 times (200 items x 5 times).
[1]http://swannodette.github.io/2013/12/17/the-future-of-javasc...
Om is an immutable data structure library:
a structure is never mutated once created,
which means that the contents of the entire
tree are captured in the identity of the object
root. Or in simpler terms, if you are passed an
object that is the same object (by identity)
that you saw earlier, you can be assured that it
hasn't changed in the meantime. This pattern has
a lot of nice properties, but also generates more
garbage than mutable objects.
Is this true to the extent that it typically matters? I would expect so with sort of naive immutability (copying everything whenever you make a change), but not with the kind of structural sharing that Clojure(script)? persistent data structures use.Even with Clojure's sharing, it is still the case (as I understand it) that you're forced to copy all nodes between the root and any node that actually changed. A mutating approach doesn't have to do any copies; it can just mutate the interior node of the tree.
As an example of a degenerate case, imagine that your root has 10,000 children. If I understand correctly, any mutation to any object in the entire graph requires duplicating the root and the entire 10,000-element array.
I honestly have no idea if this would be an important issue in practice or not, I just wanted to mention it as a super-brief pro/con list of immutable data structures.
I did a bunch of GC testing with Om, the GC profile wasn't dramatically different than naive Backbone.js on TodoMVC which is also working GC by blowing away the DOM on every update.
Node cloning can be done fairly quickly if the bulk of the node data is shared. The `_tree` immutable tree library (https://github.com/drfloob/_tree#quality-metrics) has a 1,024 child-node benchmark. On my slow laptop, it can build 1024 node trees at ~10/second, and the performance appears logarithmic on node count (http://jsfiddle.net/9x7aJ/2734/). I found it fast enough for my client-side use cases, but Clojurescript's optimized branching scheme could probably be implemented to boost performance. And anyway, if 10ops/second isn't fast enough, `_tree.Tree.batch()` lets you temporarily bypass immutability if needed, making modifications much quicker.