Enabling Ergonomics 🚐 and Performance 🏎

Frameworks like Ember and Glimmer are constantly attempting to walk a fine line between runtime performance and developer ergonomics. If we lean too heavily towards developer ergonomics we ship a bunch of extra code which hurts runtime performance, but if we lean too much in the direction of runtime performance we make the lives of our users (developers in this case) massively more difficult.

So what do we do!?!? BOTH πŸ’ͺ! I'll show you how‼️

Development Builds πŸ‹

In development builds we want to do things like:

import { assert, deprecate } from '@ember/debug';

assert(`You must pass an array to xyz method, you passed '${obj}'`, Array.isArray(obj));

deprecate(  
  'Using `fooBar` is deprecated, please migrate to `bazQux`.', 
  false, 
  { 
    url: 'https://emberjs.com/deprecations/v2.x/#some-url',
    until: '2.12.0',
    id: 'some-deprecation-id'
  }
);

This allows us to provide feedback to developers when they are doing things incorrectly. For example, our deprecation process makes it possible for folks to have much more confidence when upgrading between Ember versions. You can be assured that if you have satisfied all deprecations in your current version, that you can upgrade to the next version without issue. We have even worked on custom deprecation tooling (e.g. ember-cli-deprecation-workflow and ember-inspector) to help squelch the noise of deprecations while you work through them.

Production Builds πŸš€

In a production build we want the built assets to be as small and performant as possible. This makes production builds completely opposite of development builds πŸ™ƒ. This means we want to remove all of those deprecations, assertions and warnings that we added to be able to support the nice developer ergonomics that we wanted from our development builds.

Community To the Rescue! πŸ†

There are at least a couple ways that I've seen this done. The most common way to do it (at least in Ember or Glimmer land) is to add a custom babel plugin that checks the current build type (generally using process.env.EMBER_ENV and sometimes falling back to process.env.NODE_ENV). The other way that I've seen this done (but mostly in other communities) is by configuring your minifier to replace certain tokens (like the aforementioned process.env.NODE_ENV) with static values. Both of these techniques seem difficult to configure and get right, what to do, what to do...

I have some good news for you though πŸ˜‰. Gavin Joyce began paving the path for us to add automatic code stripping in production builds as a default feature of Ember CLI. More recently Chad Hietala did a bunch of work to enable Ember and Glimmer to have this functionality.

Now, thanks to all the hard work done in the community, we can have automatic debug statement stripping without doing anything 😱.

How do I use it?

Assuming that you have upgraded your application and/or addons to ember-cli-babel@6.1.0 (which you will want to do to take advantage of the awesome targets feature!), then you can just start using the syntax that is enabled.

For example, if you would like to deprecate an API in an addon that you maintain, you can start doing:

import { deprecate, assert } from '@ember/debug';

export default Ember.Component.extend({  
  init() {
    this._super(...arguments);
    deprecate(
      'Passing a string value or the `sauce` parameter is deprecated, please pass an instance of Sauce instead',
      false,
      { until: '1.0.0', id: 'some-addon-sauce' }
    );
    assert('You must provide sauce for x-awesome.', this.sauce);
  }
})

This will run the debug statements (assert and deprecate in the example above) when running in the development and test environments, but they will be stripped (via dead code elimination) in your production builds.

Another common task is to expose certain API's in debug builds only (e.g. to make testing easier). A good example of this is in liquid-fire where they setup a test waiter conditionally if testing. If we wanted to refactor this to take advantage of the built-in features of ember-cli-babel, we could rewrite it as:

import { DEBUG } from '@glimmer/env';

var TransitionMap = Ember.Service.extend({  
  // existing contents without testing code
});

if (DEBUG) {  
  TransitionMap.reopen({
    init() {
      this._super(...arguments);

      // custom test waiter init 
    },

    willDestroy() {
      // cleanup test waiter
      this._super();
    }
  });
}

export default TransitionMap;  

As with the debug statements, the result of this refactoring is that the test specific code is completely removed from during production assets.

@glimmer/env ⁉️

Now, you are probably wondering why the example above is using @glimmer/env instead of something from the Ember namespace. The explanation is simply that the @glimmer namespace is for base primitives which we can use to implement Ember on top of. You can read more about this in my blog post discussing Glimmer vs Ember.

Recap APIs 🎩

The following named exports are available from @ember/debug:

  • function deprecate(message: string, predicate: boolean, options: any): void - Results in calling Ember.deprecate.
  • function assert(message: string, predicate: boolean): void - Results in calling Ember.assert.
  • function warn(message: string, predicate: boolean): void - Results in calling Ember.warn.

The following named exports are available from @glimmer/env:

  • DEBUG: boolean - Truthy when running in a non-production build.

See the ember-cli-babel README for more detailed information.

Glimmer Apps Too‼️

The import { DEBUG } from '@glimmer/env'; API works automatically in Glimmer applications, but you may be wondering how things like deprecate / assert / etc are handled. Certainly, you wouldn't require all of Ember just for these basic debug helpers.

Indeed, you would not. The following works (and uses only console API's) in Glimmer applications:

import { assert, deprecate } from '@glimmer/debug';

assert(`You must pass an array to xyz method, you passed '${obj}'`, Array.isArray(obj));  
deprecate('Using XYZ API is deprecated, please migrate to PDQ.', false);  

This again shows that Glimmer as a primitive layer that underpins the Ember ecosystem pays off for both sides. Read more on the relationship between Ember and Glimmer here.

Call To Arms 🎺πŸ’ͺ

We must keep both developer ergonomics and production performance at the forefront of our minds. The shared ecosystem and tooling of Glimmer and Ember makes this possible.

App and addon authors can upgrade to ember-cli-babel@6.1.0 and start using these features to provide their users with a much better experience without negatively impacting performance. If you have any troubles or issues, feel free to ping me (@rwjblue on GitHub) in your pull requests / issues. πŸ’œ 😻