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}}
·
{{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
.