43 ms·
I like the React model, but it does make one key assumption. It assumes that DOM mutation is so slow that re-rendering and diffing the entire virtual DOM for e
by haberman 11y ago
I like the React model, but it does make one key assumption. It assumes that DOM mutation is so slow that re-rendering and diffing the entire virtual DOM for every update is basically free in comparison. I like the simplicity of React's approach, but this assumption makes me a little uncomfortable.
- Silhouette 11y agoAs an anecdotal data point, that assumption survived approximately no time at all during a recent exercise I was involved with evaluating how to build a new and reasonably large web app. With similar scepticism but trying to keep an open mind, we did an experiment allowing the entire DOM to rerender from a top-level React component when the underlying data model changed. It performed pretty much as badly as everyone thought it obviously would. We then switched to exploring more standard approaches like having individual components monitor the relevant model events and manage their own state, using React's rendering model at a much finer granularity. We haven't finished our investigations yet and there are some other concerns about React's performance under certain conditions, but so far, the finer-grained approach seems more promising and is our most likely way forward for this particular project.
- haberman 11y ago:( I was afraid of that. React docs oversell this point a bit: https://facebook.github.io/react/docs/multiple-components.html#a-note-on-performance https://facebook.github.io/react/docs/multiple-components.ht... "You may be thinking that it's expensive to change data if there are a large number of nodes under an owner. The good news is that JavaScript is fast and render() methods tend to be quite simple, so in most applications this is extremely fast. Additionally, the bottleneck is almost always the DOM mutation and not JS execution. React will optimize this for you by using batching and change detection. [...] Don't underestimate how fast JavaScript is relative to the DOM."
- Silhouette 11y agoIn my humble opinion, React docs oversell a lot of things. The point about the DOM mutation being a bottleneck is valid, but there is no magic.
- Akkuma 11y agoWas your data mutable or immutable? The former requires you to write shouldComponentUpdate logic that isn't just old == new. If you didn't write any of that logic and didn't use immutable data rendering everything again from the top down due to a single change is of course going to be terrible performance. If you wanted to be more adventurous you should look at alternative virtual dom implementations http://vdom-benchmark.github.io/vdom-benchmark/ http://vdom-benchmark.github.io/vdom-benchmark/. What you'll find is that React is actually the slowest one out there based on performance. For instance, on dbmonster InfernoJS sees 50-100% more fps and on my machine ~20x better benchmark performance. If you want the next popular implementations look at virtual-dom, which is leveraged in a variety of frameworks like https://github.com/ashaffer/vdux https://github.com/ashaffer/vdux, or Google's Incremental DOM, https://github.com/google/incremental-dom https://github.com/google/incremental-dom.
- Silhouette 11y agoWe've also found that some of the other virtual DOM implementations that are popping up can be somewhat faster than React's, though I confess I can't remember exactly which ones as I write this. But I don't think that really matters. Fundamentally, once we have a moderately complicated data model such as configuring a few thousand entities of perhaps 50-100 different types with assorted properties each and assorted constraints between them, even a relatively passive scan over the model to rerender the UI from the top following each transaction on the data model does not appear to be a viable architecture today. Fortunately, that architecture is also unnecessary, as a modest amount of finer-grained integration work presents a much more realistic alternative. The declarative style of specifying a UI still has considerable potential as a way of composing manageable components, we just need a scalable way of decomposing the overall design to match, and it seems for now that means smaller-scale components need to be aware of their own concerns in some cases.
- foota 11y agoIsn't this supposed to be a non issue because react can intelligently compare the higher level components of your application and decide whether they need to be re-rendered?
- Silhouette 11y agoThe trouble is, those comparisons might be relatively cheap compared to a DOM update, but they still aren't free. Even with an efficient overall algorithm and with all the multiples having a key property set to minimise unnecessary changes, if you're dealing with hundreds of components in your page that need rendering at any given time and that rendering in turn relies on significant conditional logic or derived information, those comparisons to see what rendering is needed (or, if you prefer, to determine whether re-rendering is needed) start to add up.
- foota 11y agoWhat's the scenario where you need to re-render hundreds of components? Is this something like a state change propagating down a long line of components? If you assume a roughly balanced tree like structure to your components though wouldn't even this still be bounded logarithmically by the number of components on the page?
- morgante 11y agoIf you're manually monitoring model events, you're almost certainly doing it wrong.
- deleted 11y ago[deleted]
- Lazare 11y ago> we did an experiment allowing the entire DOM to rerender from a top-level React component when the underlying data model changed. That's really not how you'd write a large idiomatic React application. Conceptually, you should be running render() methods on the actual items which have changed, and a very small number of parent components with little-to-no DOM elements. You should also be executing a relatively small number of shouldComponentUpdate() methods which should be extremely fast to execute. If you are doing that, then where exactly was the performance bottleneck, and how was your alternative approach avoiding it? I mean, sure, if there's simply no possible way for you to figure out what to re-render cheaply, then your app will have poor performance. But that doesn't seem to be true from your other comments. In fact, it sounds like your second approach might be closer to a normal idiomatic React app than your first implementation. Incidentally, are you using some sort of flux architecture?
- Silhouette 11y agoThat's really not how you'd write a large idiomatic React application. That's a bit No True Scotsman for me. Just in the replies to my previous comment, there are several quite clear yet apparently contradictory views expressed about how these things are supposed to work. I doubt that anyone, probably including the React developers themselves, yet has enough experience to sensibly determine what is and isn't idiomatic or best practice here. In any case, I'm far more interested in effective results than dogma, which is why we've been doing practical investigations into what really happens when the tools are used in different ways. Conceptually, you should be running render() methods on the actual items which have changed, and a very small number of parent components with little-to-no DOM elements. You should also be executing a relatively small number of shouldComponentUpdate() methods which should be extremely fast to execute. And this is where the assumptions started breaking down in our experiment. For one thing, we have a somewhat complicated data model and a somewhat complicated UI to present it. Managing the relevant shouldComponentUpdate mechanisms was getting tedious even in our short term experiment. If you're rerendering top-down in a heavily nested UI then it seems you need to provide shouldComponentUpdate in quite a lot of places just to deal with the parent-child relationships with acceptable efficiency, and each of those implementations is a potential source of errors because now you have multiple sources of truth. Also, for things that aren't extremely fast to execute because they rely on derived data, you wind up with questions of what intermediate results to cache and where. Caching intermediate results goes against the kind of approach much of the React community seems to advocate, hence props in getInitialState being an antipattern and all that. Time will tell, but once the abstraction started leaking in these kinds of ways, it wasn't clear to us that trying to follow the pure, declarative approach that a lot of React advocacy promotes was really better than other strategies. If you are doing that, then where exactly was the performance bottleneck, and how was your alternative approach avoiding it? Bottlenecks were as above, among others. Avoiding them comes down to the simple principle that if each component attaches to listen to events in a modular way and sets its own state accordingly, then the default is essentially not to rerender anything and has zero overhead. As soon as you start making active decisions about what to rerender, you immediately have the overhead of those decisions to worry about, and sometimes that overhead is significant if you're, say, responding to every character typed in a text box. If you respond at a more local level, you can choose the granularity of responses to keep the overheads more controlled, which it seems so far is effectively getting the best of both worlds. In fact, it sounds like your second approach might be closer to a normal idiomatic React app than your first implementation. Incidentally, are you using some sort of flux architecture? In the sense that user interactions send meaningful messages to an internal component, that component is responsible for updating some stored data accordingly, and the internal component then signals changes that other parts of the UI can respond to, yes, it's somewhat like Flux in overall architecture.
- grayrest 11y agoPutting everything in one map and naively rendering it is a pattern that comes from the clojurescript subset of the React community and it works because the cljs wrappers assume cljs' immutable datastructures and all implement shouldComponentUpdate for you. React isn't magically fast, it's just O(n_size_of_diff). Getting it to perform well here requires you to figure out how to prune the size of the diff. You're doing it by splitting the app state into finer grained trees (each tree is pruning the branches it's not listening to) but any gains you're picking up from doing this could be done on the single root solution with appropriate shouldComponentUpdate methods. The alternative (and easier) way of reducing the diff size is to simply window your data before passing it into the render, which can work for things like grids and lists but doesn't sound like your problem. You also need to occasionally reshape your data to make the diff smaller. I work on a search app that thrashed for 1.2s every time new search results came in. The problem was that the results were invalidating a recursive menu component which was re-rendering 4 times per update. Avoiding the invalidation fixed the specific delay but the menu was still slow. Switching the menu's data organization from at tree of objects with `open` attrs to a static tree + open path and only rendering the visible nodes solved the perf problems in the app.
- Silhouette 11y agoGetting it to perform well here requires you to figure out how to prune the size of the diff. The most efficient way of doing that is by never doing an unnecessary diff in the first place. Everything after that is a trade-off between convenience and overhead. Sometimes it will be worth incurring some overhead for greater convenience, sometimes it won't. You're doing it by splitting the app state into finer grained trees (each tree is pruning the branches it's not listening to) but any gains you're picking up from doing this could be done on the single root solution with appropriate shouldComponentUpdate methods. But shouldComponentUpdate is the wart that makes the whole abstraction leaky. The assumption that such tests have negligible cost is essential to the assumption that React used in the style you're advocating is efficient, but with non-trivial data models that won't necessarily be the case. Even if that assumption holds under any particular set of circumstances, you still now have two sources of truth, with more code to write and more scope for introducing errors. At that point, it seems you're not necessarily any better off than you would be with a more traditional, event-driven architectural style, you're just making different trade-offs. For example, given a parent-child relationship between components, rendering top-down using React might avoid some of the need to co-ordinate updates if underlying data is appearing or disappearing. On the other hand, it might also mean writing a bunch of boilerplate shouldComponentUpdate functions just to keep rendering efficient.
- tlrobinson 11y ago"diffing the entire virtual DOM for every update is basically free in comparison" If you use something like Immutable.js you can avoid a lot (most? all?) of the diffing since comparing trees of objects becomes a simple object reference comparison. This is common in the Om world, and becoming more popular in JS too.
- haberman 11y agoIf you have a complicated, deeply interconnected model, updating immutable data structures properly doesn't seem much easier than properly implementing shouldComponentUpdate().
- chrisseaton 11y agoYou could use a lens to make the programming easier, and the logarithmic depth property of DOM-like trees makes it likely that allocation will also not be a problem.
- haberman 11y agoWhat is a lens in this context?
- audreyt 11y agohttps://www.npmjs.com/package/safety-lens https://www.npmjs.com/package/safety-lens https://www.fpcomplete.com/school/to-infinity-and-beyond/pick-of-the-week/a-little-lens-starter-tutorial https://www.fpcomplete.com/school/to-infinity-and-beyond/pic...
- wereHamster 11y agoIs it not easy because the syntax is cumbersome (eg. chained calls to setIn([...]) in immutable.js), or because it is fundamentally difficult? The former is something that's specific to Immutable.js and many other javascript frameworks. Other languages have solved that problem rather elegantly (cursors, lenses). If it's the later then I doubt shouldComponentUpdate will be much less complex than the state update function (in terms of complexity, lines of code, easy of understanding etc).
- deleted 11y ago[deleted]
- nostrademons 11y agoReact's assumption isn't actually true. DOM manipulation is fast - all modern browsers use a dirty-bit system where modifying the DOM is basically a few pointer swaps. Layout, however, is slow, and dirtying the DOM requires at least one layout as soon as control returns from JS. If you intersperse measure and modify calls, it's more. However, what is true is that: 1. Both JS and the DOM are fast enough that you can lay out most reasonable pages within the threshold of visual perception (~150-200ms). 2. If you need to layout the page during an animation frame, you're hosed anyway. You have a frame budget of 16.67ms and layout will take 100ms+ on mobile. And so the practical, qualitative conclusion is the same: you can use React for your basic page transformations & rendering, but you need to use CSS transforms and offload the work to the GPU if you want to have a prayer of making animations work on mobile. And React also protects you from completely fucking up performance (which is trivially easy with JQuery), so many real-world pages from inexperienced webdevs experience a very significant speedup.
- jesstaa 11y agoThat's an optimisation. It's not core to the React model. If DOM mutation was ever to be faster than the cost of the diffing then they'd just remove the diffing and your applications would still work the same with the same API.