Gentle Migration from Marionette to React
Backbone.js provides a pretty great model layer for front-end web applications, but a lot of the rendering is still DIY. At OrderUp, we've been using Marionette on top of Backbone to handle our rendering in the customer-facing app as well as several internal apps. All was well, we worked out some good usage patterns that kept us productive and the code from becoming spaghettiesque.
My colleague Kyle Fritz developed a new dispatcher to allow us to monitor orders in multiple markets (market = one of the various cities in which we operate) simultaneously. Our original dispatcher could only handle one market at a time, so this was a huge deal for our support team who had to jump around between these markets for every call, email, or customer chat.
To understand what all this entails, I'll go into a few details. First, this app monitors everything in real time, so every time an order comes in or a driver makes themselves available/unavailable for deliveries or updates their location or the status of a delivery ("on my way to pick up the food", "on my way to the customer", etc), we send it to Pusher. The Marionette views in the dispatcher monitored all of these Pusher channels and updated the display whenever any of the underlying models changed.
Spoiler alert: the DOM is slow
Marionette made the development of this new, more powerful dispatcher pretty simple, but it had one major problem: at dinnertime when orders were coming in hot and driver/delivery statuses were getting updated like crazy from hundreds of drivers in multiple markets, all the constant DOM updates were pushing our support team's browsers to the limit.
Kyle and I tried a lot of different tactics to throttle these updates but we couldn't get more than a few percent out of it. We went ahead and opened up a profiler and had a look. Because every Pusher update triggered a DOM update, 80-90% of the time was spent doing this. At peak times, rendering even some of those tiny DOM elements (many of these views were a single <span>
tag) was taking longer than we could afford to spend before the next message came in from Pusher.
Maybe Marionette isn't the right tool for this job?
Coincidentally, we had both been learning and playing with React recently. We had already written a new UI for market managers to schedule drivers with it to get our feet wet.
Since React is known for its extremely efficient rendering through laser-focused, coalesced DOM updates, I threw out the idea of rewriting the app with it. Kyle's response was a mix of "that'd be cool" and "I really don't want to rewrite this whole fucking thing."
It then hit me that Marionette views and React components had something in common: their entire operation hinges on the render
method. This gave me an idea. What if we replaced some of the bottlenecked Marionette views with React components? Neither of us had any idea if it'd work, so we gave it a shot.
Step 1: Marionette views become React components
One of the biggest bottlenecks the profiler showed was rendering the lists of orders in each market, so we tackled that first.
We took our Order
view, did a quick port of its .eco
template to .jsx
(primarily a matter of converting <%= @foo %>
to {@state.foo}
), stuffed that in the render
method and converted it from Marionette.LayoutView
to a React.createClass
:
class Views.Order extends Marionette.LayoutView
tagName: 'tbody'
className: 'order'
template: App.JST('orders/templates/order')
events:
'click': 'showOrderPopover'
onDestroy: => # ...
onRender: => # ...
# etc...
… became …
Order = React.createClass
render: ->
# JSX content (what was previously the .eco template) goes here
componentDidMount: -> # was onRender
componentWillUnmount: -> # was onDestroy
handleClick: -> # was showOrderPopover, the click handler in the Marionette view
Notice the onDestroy
and onRender
callbacks from Marionette had direct React analogs in the form of the componentDidMount
and componentWillUnmount
methods.
Step 2: Rinse and repeat as high as necessary up the DOM
We did a similar conversion for the Orders
view which contains each of the orders. The main difference was that Orders
now also mixes in Backbone.React.Component.mixin
to help Backbone and React work together. According to the README for this library, this mixin will need to be included in your top-level React component.
Step 3: Make container view render the React components
Then to kick off all of the rendering, we replaced the view that contains this section of the dispatcher with this:
class Views.OrdersList extends Backbone.View
initialize: (@options) ->
render: =>
React.render
<Orders
collection={@collection}
trigger={@trigger.bind(@)} />
@el
@ # Backbone views must return `this`.
We used a simple Backbone view because all we cared about was the render
method. Once the OrdersList
was rendered, it went ahead and rendered the Orders
component into its element, which rendered each Order
component based on its @collection
attribute.
One suspenseful page reload later and, to our amazement, nearly everything worked.
Notice the collection
and trigger
attributes we passed into Orders
. The reason we passed the collection
should be obvious if you're familiar with Backbone, but trigger
may not be. Some of the functionality of the dispatcher relied on bubbling events up the view hierarchy, so we still needed to call Backbone's trigger
method to bubble those events up.
How'd it do?
With Marionette, the Pusher events were happening faster than the DOM updates and the browser couldn't keep up. You can see from that graph that there was almost no idle time. Those events were pegging the CPU so much that even scrolling was impossible sometimes.
We repeated this process on a couple other hot spots in the dispatcher with results that were slightly less spectacular but no less important during our peak hours.
Even on Super Bowl Sunday, easily one of our biggest nights of the year, our new React-based dispatcher handled it like a champ. Given that the Marionette-based one was struggling on an average weeknight, I can't imagine it would've been usable at all that night.
How long did it take?
The majority of the work happened in just a few hours. The React-based dispatcher was deployed to production on the same day we started.
Kyle and I were both amazed at how easily and smoothly we were able to move parts of it from one to the other. We did have a few things to touch up due to event handlers being a bit different between the two, but we didn't spend more than a few days on this total.
So you're saying we should give up on Marionette?
Hell no. Marionette is pretty great at what it does. For most browser apps, the rendering performance is only required to be as fast as the user can navigate the app and, for that, Marionette is just fine. The performance requirement for this specific app wasn't that we had to keep up with a user clicking around the app; it was a data firehose we had to keep up with. React's DOM diffing and coalesced updates could keep up with that.