Ember performance tweaks: Optimising build timelines & bundle size

9 min read • 20th April 2020
Rubik's cube

In this blog series, I pen down my experience on performance tweaks that went well for Ember apps. Some of them are simple notes that I wrote down when I was tweaking the build pipelines. I share those tips and tricks that could make your Ember apps faster, accessible and awesome!

Before I go deep into how to solve for Ember's performance tips, I presume you have a good understanding of what Ember is, and you have a basic knowledge of its ecosystem, build pipelines, etc.

Link to this sectionGetting started with Performance optimisations for Ember apps

Key areas to optimise Ember apps:

  1. Optimising build timelines & bundle size ← this post
  2. Optimising Assets
  3. Search engine optimisation
  4. Improving the accessibility of Ember apps
  5. Making Ember apps installable

Link to this sectionImproving on build size and making your builds faster

As a web developer, I like to have my apps or webpages rendered fast, and there are various techniques like reducing asset sizes, caching assets on CDN, reducing requests for initial render, etc. For me, it's important that I can optimize assets by 1Kb or reduce the time to download assets to a user's browser by, say, 100ms. The smaller the page assets, the faster it is for the user to view them. The faster it is for the user to view the page with text and UI, the greater the trust they have on our apps/websites.

Since this series is about optimising for performance in Ember apps, let's see them one by one.

Link to this sectionSpeeding up development: Removing tests from the dev build

By default, ember-cli adds test files and the route /tests during development build when you run ember build or ember serve. This is in case you want to run tests in parallel by visiting localhost:4200/tests, since that is an app with build environment of development but a runtime of test. Indeed, this is a nice feature when the test files are less in number, but for large apps that have numerous unit, integration, and acceptance test cases, it would take a longer time for the test suite to finish execution. Higher the number of files to funnel to the test path, the longer it takes for a rebuild to complete.

To exclude test files from being part of the dev build and slowing things down, you can remove them from the build pipeline by using the following configuration in ember-cli-build.js.

let app = new EmberApp(defaults, {
  tests: false,
  ...
});

Link to this sectionSpeeding up development: Removing mirage from the dev build

If your Ember apps use ember-cli-mirage addon for mocking APIs in tests, then Mirage's files are included in the app's build in non-production environments. This is in case you want to use Mirage via ember serve by visiting /tests.

Similar to test files, this is going to increase the build timelines. You can explicitly exclude Mirage's files from the app's build by adding excludeFilesFromBuild to true, like the following:

let app = new EmberApp(defaults, {
  excludeFilesFromBuild: true
  ...
});

Link to this sectionSpeeding up development: Minification, Gzip or Brotli compression and Fingerprinting

Make sure your production builds are done with the production flag turned on. This ensures the application assets are minified, compressed, and fingerprinted.

ember b -p
# or
ember build --prod
# or
ember build --environment=production

In Ember, the app could be configured to do so using the following lines of code in ember-cli-build.js:

// ember-cli-build.js

module.exports = function(defaults) {
  // ...
  const isProduction = EmberApp.env() === 'production';

  let app = new EmberApp(defaults, {
    fingerprint: {
      enabled: isProduction // Enabled in production by default until you override.
    },
    minifyJS: {
      enabled: isProduction // Enabled in production by default until you override.
    },
    minifyCSS: {
      enabled: isProduction // Enabled in production by default until you override.
    }
  });

  // ...

  return app.toTree();
};

In general, one doesn't have to worry about this configuration as ember-cli takes care of it. Also, fingerprinting, minifying and compressing assets are time-consuming jobs. Therefore, they have the potential of slowing the build. If you are cautious on enabling this only for a production or an equivalent environment and avoid doing the same in dev builds, it could save a lot of productive developer hours.

Link to this sectionAsset size: Analyze bundle size and optimize asset size

ember-cli-bundle-analyzer is an ember-cli addon that analyzes the size and contents of Ember apps. To try it out check ember-cli-bundle-analyzer.

It helps to:

  • Analyze which individual modules make it into your final bundle
  • Find out how big each contained module is, including the raw source, minified and gzipped sizes
  • Find modules that got there by mistake; and
  • Optimize the overall bundle size

Install this addon using the following command:

ember install ember-cli-bundle-analyzer

Once done, navigate to http://localhost:4200/_analyze in your browser to analyze the output.

On a different note, if you are using Github for version controlling your code, you should definitely check out this Github action for validating the bundle sizes of a PR. This Github action evaluates the asset sizes of the base branch, and the current PR branch and shows up the differences as a PR comment. The following is an example of the same for my blog's repo while adding this blog post:

PR commit message on adding a change

Link to this sectionAsset size: Removing dependencies not needed on app boot

Before Ember 2.12 version, the simplest way to add a 3rd party Javascript plugin is by adding them in vendor.js using app.import statements in ember-cli-build.js. Perhaps, it contributes to increased vendor.js file size too.

For example, if you want to use lodash-es, the application needs to know that the plugin is available on the page either as part of vendor.js or as part of app.js or as a standalone JS file that is imported. But adding it into vendor.js through the ember-cli-build.js pipeline is going to increase the size of vendor.js. However, there is a technique using ember-auto-import where you can dynamically import these dependencies at run time.

Before diving deep into this approach, let's analyze the size of vendor.js file of a sample app that is the blueprint created via the ember new command:

Current vendor.js size
It is at 454Kb

To add lodash-es, let's do the following:

npm install --save-dev lodash-es # Adds lodash-es for example to your dependencies

In the traditional approach of adding lodash plugin via ember-cli-build.js (app.import('node_modules/lodash/lodash.js');), this is the size of vendor.js:

Current vendor.js size after lodash added
It is at 551Kb

But what if this can be reduced by ~100Kb by making it dynamically imported or fetched at places where lodash is used? Enter ember-auto-import. Let's install ember-auto-import:

ember install ember-auto-import

And go ahead and mention in your components or controllers or any parts of your javascript code to import lodash-es.

import { capitalize } from 'lodash-es';

Wait, doing this, simply increased the file size of vendor.js by another ~180Kb to 632Kb 😱.

vendor.js size after ember-auto-import with lodash-es
It is at 632Kb

Didn't I say that the code should reduce the vendor.js file size? Yes, for which you need to do a little bit more:

Link to this sectionWelcome Dynamic Import

Dynamic import is currently a Stage 3 ECMA feature, so to use it there are a few extra setup steps:

  1. Install babel-eslint

    npm install --save-dev babel-eslint
    
  2. In your .eslintrc.js file, add

    parser: 'babel-eslint'
    
  3. In your ember-cli-build.js file, enable the babel plugin provided by ember-auto-import:

    let app = new EmberApp(defaults, {
      babel: {
        plugins: [ require.resolve('ember-auto-import/babel-plugin') ]
      }
    });
    
  4. Now all you need to do is, dynamically import the dependency. For example:

import Route from '@ember/routing/route';

export default class SampleInnerRoute extends Route {
  model() { // This will be render-blocking, you can also move this to your controller' or component' JS file
    return import('lodash-es').then(({ capitalize }) => {
      return capitalize('Sample App');
    });
  }
}

vendor.js size with lodash-es lazy-loaded
It is back to 456Kb

Moving to dynamic imports brought it back down to 456Kb (Increased 2Kb from original).

For more on this addon, read here.

To conclude, we saw various techniques on speeding up dev build, analysing asset sizes and making that as part of a PR review process, and we also saw how to reduce the final asset size using ember-auto-import. In the next post, I will talk about asset caching using different techniques.


If you find something misleading, feel free to submit a PR


Thanks to Isaac Lee, Sivakumar Kailasam and Vasanth for helping review this content.

Enjoyed this article? Tweet it.

I guess you might be looking to add your comments? Glad to tell you that this section is under construction. But don't hold on to your thoughts! DM them to me on Twitter