Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
On "The Future of JavaScript MVC Frameworks" (reverberate.org)
73 points by swah on Feb 24, 2014 | hide | past | favorite | 54 comments


I appreciate the skepticism :) But remember the goal of Om is to be simple and fast in more scenarios than are allowed in traditional designs. Emphasis on simple.

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.


David, thanks for your work on Om and, especially, writing about it. Your writing introduced me to React and how to use it with a de-emphasis on state. Having very little FP experience and zero Clojure experience, however, sometimes you're going a little fast for me, so was hoping whether you or someone else who reads this could help me out.

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!


Yes in Om the application state is treated like a database - it contains all the relevant application data. However we don't encourage accessing the application state as a global if it can be avoided. Components should only receive the parts of application state they need to render.

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.


Yeah, good point on the not-global. I didn't mean globally accessible, more like, a single "current state" object, rather than many little ones as you would see in a typical backbone application. Thanks!


Fast persistent data structures, combined with the idea of "reference types" that encapsulate mutation points when you really need them.

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.


In order to understand the frame of reference for Om, you really need to watch some Rich Hickey presentations [1][2] (or have a good understanding of functional programming). The first one is more relevant to your questions here, the second is just because Clojure programmers have very specific definitions of "simple" and "easy" and because it's a good talk.

[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.

[5] https://github.com/swannodette/mori


Thanks a lot for the detailed explanation. I'll watch the videos (saw the Simple/Easy one already, they're good definitions of the words and it would be great if they'd be adopted more broadly outside the Clojure community too).

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.


Hi there -- thanks for your work on Om, it's helped to bend my mind and think about different ways of building apps. I agree completely that the easy snapshot ability is very cool and it wasn't my intention to minimize it.

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.


I personally don't see much value in automatic two way binding. It's a little bit of convenience that's really easy to build over React/Om if that's your cup of tea, http://facebook.github.io/react/docs/two-way-binding-helpers.... Immutability doesn't present any challenges here.


ReactLink (which you linked to) calls setState() on the React component state when the DOM changes -- wouldn't this violate the assumptions of your shouldComponentUpdate() implementation?


We don't use setState, but we have om.core/set-state! which does pretty much the same thing - an OmLink would not be very hard to do.


In OP's conclusions: "the DOM is slow, so use a framework that batches updates to it."

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.


That's something I've recently added to Backbone.LayoutManager[1], but it's still sitting in a PR while we figure out the 1.0 API. The PR is complete, however, so feel free to use my branch. It does offer very meaningful speedups, especially when data changes rapidly or causes a chain reaction. I have not benchmarked it in comparison to React/Om but it is very fast.

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


Do you have a open example to this? I would like to know how is implemented.


He just described the _.debounce function in underscore.js

http://underscorejs.org/#debounce


Well, we can check a few of those guesses pretty easily. Here is the stock React TodoMVC demo augmented with Swannodette's benchmarks:

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...


Thanks for posting this; I wished when writing the article that I had plain React benchmarks to run.

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.


> it seems like it is invoking React's event loop more than I would expect ... Perhaps this difference is because of requestAnimationFrame?

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.


> 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.

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.


EDIT: not true. see below.

----------------------------

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.


The timings you are presenting in the UI are not accurate. But you are correct that this approach results in timings that are almost identical to Om. Use the Chrome Dev Tools profiler flamegraph if you want to confirm what I'm saying.


Ah, right. Benchmark 1 is actually ~350ms on my machine. Thanks for the lesson in profiling asynchronous code. I revisited the React+_tree benchmark claims, too, and I think they are still spot on. If you're interested at all, I'd appreciate you taking the time to check my work there.


I took a look but it's hard to judge since you are using React 0.8.0 and Om is now on React 0.9.0.


Interesting stuff. Have you compared _tree with Mori?


Thanks. I knew of Mori, but hadn't looked at it yet. Now that I've glanced, you could definitely implement something like _tree with Mori, but it'd be missing a few key things.

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.


Mori's data structures are the ones from ClojureScript, so they're designed in a functional, rather than OO, style. I agree it's more foreign in a JS context. For some operations, there are advantages to being able to write functions that just work with generic data rather than objects. I could imagine having a layer on top of Mori that provides a more familiar face while still giving access to the "just data" stuff underneath for performing functional operations.

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).


Thanks dangoor. It's a good day when someone shows me two recent projects that are very similar to my own. I started _tree a few months back because I couldn't find a project like it. Turns out I'm in good company.

_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.


It's a bit more sophisticated than that.

https://news.ycombinator.com/item?id=7292588


Right, it sounds like they use tries with 5 bit keys. It's a very neat data structure, for sure, but _tree is just at a lower level. It doesn't impose structure. Tries can be implemented in _tree.


The most amazing and irritating thing about JS frameworks is that every week you find something better.


And then you decide on one to do a really big single page app (100.000 lines of code) and end up stuck on a legacy codebase watching the pretty new frameworks sailing by. (Happened to me on an ExtJS codebase started in 2008.)


Yep, I'm in the same boat. And good luck upgrading ExtJS 3.x to 4.x. Occasionally minor point releases are painful to upgrade to as well. Good lord.


Ha! We're even having trouble going from 4.0 to 4.1

Not really funny come to think of it :(


I started with 4.0 and switched to 4.1 and 4.2 without any problems.

But yes, I heared 3 to 4 should be a PITA


Likewise; we started building a BackboneJS app two years ago; in the meantime, I'm rebuilding it in Angular for mobile devices, while the main development is going to port the big app to Angular too.

Fun fact: Angular is older than Backbone (first commit to angular was four years ago).


Pick one every _other_ project. This way you will at least skip the ones that will die in that period.


I am also kind of worried that now angular.js has quite a high adoption, and plugins / modules becoming better and better, the devs will focus on angular 2.0. Not only will it use new core technology, but they'll have less time working on the 1.x branch. With over 1000 open issues, this is not a good sign.


I think the only viable route is to migrate to Angular 2 - the evolution of JS frameworks is way to dynamic to stick with older versions.


Ugh, tell me about it :P. But is more something with a different approach than better most of the time. And others times are things that are already there since the beginning of time but remain overlooked


I've used Backbone, Spine, Angular and some handrolled solutions. It never clicked.

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 :)


If it compiles to JS, I assume that you can call JS from it? The framework of question in this thread, React, actually meshes really well with an FRP kind of thinking and with immutable data. Read the post Haberman links to.


I hadn't read the original article, "The Future of JavaScript MVC Frameworks", and the article itself[1] is unreadable for what seems like either font or CSS issues. I made a pastie of the text with links to the text there for those who want to read it too.

[1]http://swannodette.github.io/2013/12/17/the-future-of-javasc...

[2]http://pastie.org/8768093


Are you using Chrome? When I visited the page I had this bug, which I've experienced all too often all over the web since the latest update, where no text was visible on the page until I nudged the window size by a couple pixels.



Also happens to me when using Chrome. They must have broken something with the latest updates.


There's been a surprisingly bad number of font bugs in the last few months. It's bizarre; I haven't seen browser bugs this visible in years.


It's kind-of funny to me see an article entitled "the future of javascript" anything that only talks about a ClojureScript framework.


Having recently worked on optimizing the performance of my own view extension library to Backbone (kettle.js), I can give some explanation about the relatively poor performance of Backbone's todoMVC implementation in benchmark#1 and #2.

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).


    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.


It's hard for me to say how much it matters, because that would depend a lot on the efficiency of the GC and how many copies are forced (which is a property of the application's data structures).

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.


Clojure and ClojureScript data structures use 32 way branching nodes. This means even with >2 billion elements in say a persistent vector you'll never have more than 7 hops on any particularly path. This means in the worst case you will need to copy and update 7 children all which are small arrays.

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.


Ah, that's clever. Thanks for the info.


Part answer to your question, part shameless plug.

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.


You're right in a sense, it generates more garbage than mutable objects because there is more data available than with mutable objects. In most cases it won't matter. Of course if you're holding a global interface undo queue for the lifetime of your page, and your page changes often, you might run into memory issues. But Clojure's immutable datastructures work as well - or better - than you'd expect.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: