While profiling Discourse on my gigantic Nexus 6P which is the flagship Google phone at the moment I discovered we are spending a huge amount of time "requiring" various files on intial load.
Our boot sequence for the front page is pretty much this
- Download JS files and parse them
- Ember will "evaluates" itself requiring all its modules, this costs 290ms. Discussing options with @stefan_penner on speeding this up.
- Discourse invokes its "compatibility layer" evaluating every ES6 module we ship, this costs 700ms
- Ember runs various intializers (both internal to Ember and ones we wrote) 400ms
- Ember starts up the Ember router 140ms
- We turn our JSON into Emberish objects 300ms
- We render the front page ... 1700ms a huge part of which is looking up factories in containers in Ember (150ms) and applying class name bindings (300ms)
We clearly want to tackle everything here, but a very easy win which in local testing saved us 580ms is removing the ES6 compatibility layer.
What is this compatibility layer?
Before @eviltrout moved us to ES6 all of Discourse's JS was defined under the Discourse
global.
Want a User class, use Discourse.User
... want the UserNotificationsController use Discourse.UserNotificationsController
.
This worked alright but had a very huge drawback, we were forced to evaluate large amounts of code we have no need to.
Evaluating classes in Ember can be VERY expensive when lots of classes are involved. The innocuous snippet below can cost 3ms.
var Something = RestModel.extend();
This is due to Ember's object model and implementation of mixins, initilizers, overrides, computed properties and so on.
Ember does a lot of fiddeling with object prototypes, so the convenience of OO like features in a prototypel language does not come free.
We need to avoid all this work of defining our entire object model before rendering a single page and ES6 makes this very easy.
What will happen?
At the moment we have a "special" flag called DISCOURSE_NO_CONSTANTS
which strips off the compatibility layer.
I will be working on getting everything in core and core plugins working in this mode. Once I feel this is ready:
- I will deploy meta without the compatibility layer
- A few days later I will deploy standard and business containers without the layer
- A few days later the flag will be removed and become default
How do you migrate to the new pattern?
We strongly recommend you use ES6 for all your plugin needs, when using ES6 you would do:
// old code
Discourse.PermissionType.create();
// new code
import PermissionType from 'discourse/models/permission_type';
PermissionType.create();
In some cases you may not be able to use ES6. (for example, site customizations or narrow backwards compat conditions)
var PermissionType = require('discourse/models/permission_type').default;
PermissionType.create();
How do I know what to require?
Usually guessing will do fine ... Discourse.User maps to discourse/models/user
... its in the models directory under user.
In rare cases you may need to browse: javascripts/discourse to find the actual file name.
Some stuff is going to bypass the need for a move
I will be whitelisting a few globals such as Discourse.Route
and Discourse.Model
, main reason is that they are always needed for any page and this cuts down on ceremony.
How long until this is all done?
I expect most of the changes to be done by the end of next week so expect the new default to land in master in 2 weeks or so
What can plugin authors do?
Be sure to run your plugin in with:
rm -fr /tmp/cache
DISCOURSE_NO_CONSTANTS=1 bin/thin start
This will ensure your plugin will be compatible when the new default lands.