The Neuronen Schmiede concerns itself with topics related to software development. One focus are robust web applications.

Speed Up Ember.js List Rendering By Example

Permalink

While Ember.js helps you a lot when it comes to building robust web applications it's not an out of the box solution for building mobile apps. Building a snappy app for already underpowered mobile devices requires work and deliberate thinking about performance.

My recent article about lessons learned while buidling an Ember.js mobile app for triathlon results showed that rendering lists is a point where Ember.js doesn't shine by default.

What Are Possible Techniques to Speed up List Rendering in Ember.js

  • Use pagination to limit rendering to a specific number of entries. The fewer elements are displayed on a page, the faster the list will be rendered. Users have to use some kind of navigation to move between pages. Infinite scroll is very similar and can also work. Make sure that rendering a new batch of entries doesn't require a re-render of already visible entries.

  • Only render entries that are currently visible to the user. When the user starts scrolling you have to add and remove entries accordingly. This technique keeps the number of rendered entries to the absolute minimum. Scroll event handling in mobile browsers and handling entries with varying heights can make this tricky. Ember.ListView and ember-cloaking are probably worth looking into.

  • Reduce the number of DOM elements per entry. A single entry constructed from ten independent DOM elements seems innocent enough. But when you have four hundred of these your list suddenly consists of four thousand DOM elements. The fewer DOM elements it has to deal with the happier the mobile browser is.

  • Use custom functions and helpers to construct the HTML manually. By sidestepping Ember.js and doing it manually you can save a lot of CPU cycles that would be otherwise spent within the Ember.js source code. Needless to say sidestepping Ember.js has its downsides.

Which Techniques Did You Pick?

The first step was replacing the built in each helper with a custom helper that constructed the HTML manually.

Initial version with the default each helper


<ol>
{{#each result in filteredResults}}
  <li class="result-item">
    {{#link-to 'results.team' result.id}}
      // ...
    {{/link-to}}
  </li>
{{/each}}
</ol>

Updated version with the custom helper


<ol>
  {{each-result filteredResults}}
</ol>

The each-result helper is not complicated, actually the concept is straightforward. It takes an array of entries and builds the entire list via string concatenation. The resulting string is returned and inserted into the DOM via Ember.js. Since most attributes need formatting, like the zero padded rank, the helper relies and uses other custom helpers.

app/helpers/each-result.js


import Ember from 'ember';
import { formatListRank } from './format-list-rank';
import { formatTime } from './format-time';
import { joinArray } from './join-array';

export function eachResult(parameters) {
  return new Ember.Handlebars.SafeString(
    parameters[0].map(function(result) {
      return '<li>'
        + '<a href="/ergebnisse/details/' + result.id + '">'
          + '<span>' + formatListRank([result]) + '</span>'
          + '<span>'
            + '<span>' + result.number + '</span>'
            + '<span>' + result.teamName + '</span>'
            + '<span>' + formatTime([result.totalTime]) + '</span>'
            + '<span>' + joinArray([result.teamMembers]) + '</span>'
            + '<ul>'
              + '<li>' + formatTime([result.timeOfSwimmer]) + '</li>'
              + '<li>' + formatTime([result.timeOfBiker]) + '</li>'
              + '<li>' + formatTime([result.timeOfRunner]) + '</li>'
            + '</ul>'
          + '</span>'
        + '</a>'
      + '</li>';
    }).join('')
  );
}

export default Ember.HTMLBars.makeBoundHelper(eachResult);

Initially the mobile app used inline SVG icons for their convenience. A single list entry had four different icons. One for each of the three disciplines swimming, biking and running. And a fourth one for the link to the details page. These inline icons dramatically increased the number of DOM elements needed. The fact that every icon itself had a lot of characters due to the <path> element made matters only worse.

Besides using the custom each-result helper a second step helped reducing the render speed further. Switching to a PNG based sprite-sheet and removing the SVG icons dramatically reduced the number of DOM elements needed. Which in turn lead to a decrease in render time.

How much did the render speed improve?

Without any of the improvements the initial render time of the list hovered around 1200 ms. Navigation to a different screen and then coming back to the list resulted in 850 ms spent for re-rendering the list.

Switching to the custom each-result helper reduced the initial render time to 130 ms. The subsequent re-render time of the list when coming from a different screen dropped to 80 ms.

The second step, reducing the number of DOM elements used per list entry, improved the render time even further. The initial render time went down to 50 ms and the re-render time clocked in at 25 ms.

What should I be aware of?

Switching from inline SVG icons to a PNG based sprite-sheet complicates the development workflow. At least if you intend to experiment and switch icons. In terms of developer experience sprite-sheets are inferior to inline SVG icons. But compared to the downsides of sidestepping Ember.js with the custom helper it's negligible.

Using a custom helper as seen above has downsides, depending on the app it can be even impossible to use this technique. The most obvious limitation are the missing bindings. Whenever something changes, and even if it's only a single attribute of one list entry, the entire list has to be re-rendered. By sidestepping it, Ember.js has no knowledge of the template and can't update specific parts.

Another limitation is the missing support for actions and links. As seen in the original code the link-to helper is used to link a list entry to a details page. In the custom helper the link-to is replaced by a common <a> element. While this works it's not perfect, since Ember.js doesn't know the link clicking it will result in a full pageload.

To get the normal behaviour back we have to add a little workaround to the route. When the route is activated we use jQuery to bind the click event of every relevant link within the list. The callback grabs the href attribute of the clicked link and instructs the router to transition to it. This restores the old behaviour where you can navigate the mobile app without having to do full pageloads.

import Ember from 'ember';

export default Ember.Route.extend({
  activate: function() {
    var self = this;

    Ember.$('body').on('click', '.result-item a', function(event) {
      event.preventDefault();
      self.transitionTo($(this).attr('href'));
    });
  },

  deactivate: function() {
    Ember.$('body').off('click', '.result-item a');
  }
});

Summary

While rendering long lists is certainly not a strength of Ember.js is absolutely possible to improve the render speed. By using a custom helper that builds the list via string concatenation and reducing the number of DOM elements due to dropping inline SVG icons it was possible to cut the initial render time from 1200 ms down to only 50 ms.

The techniques used to accomplish such a massiv performance boost also have their downsides. The absence of bindings, actions and link helpers limits the number of apps where it's feasible to use a technique such as the custom helper.

Thanks for reading, if you have any additional questions on this topic hit me up on Twitter or via email.