Refactoring: Smarter data loading

Ember Concurrency and Liquid Fire team up to help us load less data and improve the initial render of a critical page.

Summary

Our goal is to speed up the Video page of EmberMap. Right now, for a given video, the entire series for that video – including all of that series' clips and those clips' encodes – are loaded, just to render the video page.

We can improve this data-loading story.

First, we'll slim down the topic route's model hook to just load the parent series. No more requesting the entire series-clips-encodes graph.

  model() {
    return this.get('store')
      .loadAll('series', {
-       include: 'clips.encodes',
        filter: { slug }
      })
      .then(series => series.get('firstObject'));
    }
  }

Now that our parent topic route doesn't load all the data, we can fine-tune the data loading for the video route. Instead of looking up the video by slug in Ember Data's cache, we return a new query:

-  model({ video_slug }) {
-    return this.store.peekAll('clip').findBy('slug', video_slug);
+  model(params) {
+    let slug = params.video_slug;
+    let filter = { slug };
+
+    return this.get('store')
+      .loadAll('clip', {
+        filter,
+        include: 'encodes,series'
+      })
+      .then(clips => clips.get('firstObject'));
   },

This way, the only data that blocks our Video page's initial render is the video, its series, and its encodes.

Now our Video page is rendering faster, but it doesn't have the data for the series sidebar. Let's turn that series-playlist component into a data-loading component, so it can lazily fetch its data after initial render. We'll use an Ember Concurrency task.

import Component from '@ember/component';
import { task } from 'ember-concurrency';
import { inject as service } from '@ember/service';

export default Component.extend({
  series: null,
  activeClip: null,

  store: service(),

  loadSeries: task(function*() {
    let slug = this.get('series.slug');
    let filter = { slug };

    yield this.get('store')
      .loadAll('series', {
        filter,
        include: 'clips'
      })
      .then(posts => posts.get('firstObject'));
  }).on('didInsertElement')

});

Now we can use loadSeries.isRunning combined with a liquid-if to subtly fade in the sidebar content when its ready:

{{#liquid-if loadSeries.isRunning use='crossFade' enableGrowth=false}}

  {{!-- empty state --}}

{{else}}

  {{#ui-title style='small marginless'}}
    {{series.title}}
  {{/ui-title}}

  <div class='text-silver mt-1 mb-3'>
    {{#ui-p style='small marginless unmeasured'}}
      {{series.clips.length}} {{inflect-word 'videos' series.clips.length}}
      &middot;
      {{time-format series.duration}}
      <div class='pull-right'>
        {{x-autoplay}}
      </div>
    {{/ui-p}}
  </div>

  {{video-list videos=series.clips activeVideo=activeClip}}

{{/liquid-if}}

Finally, we discuss how wrapping all this in an {{#if loadSeries.performCount}} prevents some flashing during the initial render, due to the way isRunning behaves when an Ember Concurrency task is kicked off on didInsertElement.

👋 Hey there, Ember dev!

We hope you enjoyed this free video 🙂

If you like it and want to keep learning with us, we've written a free 6-lesson email course about the fundamental patterns of modern component design in Ember.

To get the first lesson now, enter your best email address below:

You can also check out more details about the course by clicking here.

Questions?

Send us a tweet:

Or ask us in Inside EmberMap, our private Slack workspace for subscribers.