Общо показвания

януари 15, 2016

Reducing time to first paint

This post is about the new techniques recently discussed on events related to web app for mobile: how to make our web application behave more like a native one and reduce friction for our users and meet their greatest expectation.

In this post I will discuss how we managed to reduce the time to first paint in an app we already had developed. We will be measuring several things:


  • time to first meaningful paint
  • time to app readiness
  • time to first paint on first visit
  • time to app readiness on first visit
Why there is a difference? First visit is important for several reasons: even with service workers being readily available in Android one still needs to consider those first visits to the app which can happen at any time and any network condition, forcing the user to wait is not going to be very good for any conversion strategy.

Secondary visits, especially for devices that to not have the service worker enabled (or in case we decided to not use it for our app for some reason) can also be potentially problematic for several reasons: busted browser cache, new version of the app etc.

Lets start with some numbers!

The app is consisting of a single html file, a processed (built) css file and a javascript (also built).

index.html - 566 bytes
app.build.css - 50702 bytes
app.build.js - 188824 bytes

What were the measurements on load time with this initial state:
First visit:
  • time to first paint: 850ms
  • time to readiness: 1000ms
Secondary visit:
  • time to first paint: 15ms
  • time to readiness: 1000ms
Its not that bad, but this is a relatively small application and also we use landline network to load the app.

The one problem is that in first visit the user is staring at blank screen for almost a second, loading on slower networks would only exaggerate that and can make the user give up on our app.

How can be possibly reduce this time to first paint on first visit?

We can attempt and utilize a technique described as 'fake' first paint: provide only a skeleton on the actual app view and how it would be looking like when ready and provide it as soon as possible (possibly in the initial HTML with CSS only for it). 

This sounds okay, but the fact is we already have the application built and we did not wanted to invest too much, also the additional markup and styling would have to be maintained separately and updated every once in a while as the application evolves. 

We decided to go with another approach: separate the code that is responsible for the initial html structure into a module to be loaded first, let it execute (i.e. unblock the event loop) and only then load the rest of that app that will 'decorate' the pre-rendered html and make the application ready for the user.

How does this look like in the context of a mid-sized closure tool driven application?

Several steps were taken:
  • create a new namespace and render the main app template in it, it should be doing only that - requiring only the template and dom utilities
  • create the HTML loading module that invokes the namespace from previous step and asynchronously wait for the next event loop tick to start loading the application logic module
  • create the application logic module which should simply invoke your app (basically your main entry point from static (non modular) version
  • make sure your main app logic accepts a new option to tell it that it should decorate a preexisting html structure as opposite of rendering itself
An example:

//Step 1
app.html.rootElement = goog.dom.htmlToDocumentFragment(
    app.template.App({
      user: app.settings.RUN_AS_USER
    }).getContent());

//Step 2
html_init = function() {
  // Add thr whole view to the document.
  document.body.appendChild(app.html.rootElement);

  // Wait for the rendering to kick off.
  setTimeout(function() {
    // Configure modules.
    var mm = goog.module.ModuleManager.getInstance();
    var ml = new goog.module.ModuleLoader();
    mm.setLoader(ml);
    mm.setAllModuleInfo(goog.global['MODULE_INFO']);
    mm.setModuleUris(goog.global['MODULE_URIS']);

    // Tell module manager that the html module is loaded
    goog.module.ModuleManager.getInstance().setLoaded('html');
    // start loading tha app module.
    goog.module.ModuleManager.getInstance().execOnLoad('app',
        goog.functions.NULL);
  }, 10);
};


// Fireoff things immediately.
html_init();

// Step 3
app_init = function() {
  // Instanciate the app controller.
  (new app.Main(true));
  // Tell the module manager that the app has been loaded.
  goog.module.ModuleManager.getInstance().setLoaded('app');
};

app_init();

// Step 4
app.Main = goog.defineClass(pstj.control.Control, {
  /**
   * @constructor
   * @param {boolean=} opt_useDecorate
   */
  constructor: function(opt_useDecorate) {
    pstj.control.Control.call(this);
    /**
     * @type {boolean}
     * @private
     */
    this.useDecorate = !!opt_useDecorate;
    this.init();
  },
...
});


As you can see the change is trivial, but it allows us to do two things: reuse the actual application template (might it be not functional yet, but it looks exactly like the final result) and thus its automatically kept in sync with the application as it evolves and two: reduce the initial load size (the size of bytes needed by the app to reach first paint that resembles the app view for the user).

What are the new results (after this change):

First visit:

  • time to first paint: 400ms
  • time to app readiness: 1100ms
Secondary visit:
  • time to first paint: 15ms
  • time to app readiness: 1100ms
Not that bad for 20 lines of code: we managed to reduce the time to paint on first visit (or cache miss) by a half with a simple change of order of execution. 

Now one question arises: why not simply do it inside the main build: render the view first as a template, then free the event loop and then decorate the application logic on top of it. 

Yes, this was initially the intent, that is why we went with the option to tell the main app entry point to use rendering or decoration (step 4 in the example), but since closure compiler is so good with modules we decided to start using this technique to be ready for the growth of the application and instead of adding feature after feature to the main build simply module-base new features and load them on demand, but not before the user needs them. This will allow us to have a 10 times bigger application but still keep the initial load size (and times) the same. 

As you might have notices the separation of the built JS binary to two modules had a cost: ~100ms delay until application readiness, you should b aware of that and carefully examine the trade-offs when deciding if you should use this approach or instead go for the one described in the beginning and simply use a dummy scaffold in the HTML. In our case we wanted to stay as close to the existing code as possible.

How does this change result in code size?

index.html - 782 bytes (because we included the module info in the html, it could be instead pre-pended to the first module)
app.build.css - 50702 bytes
module_html.js - 70231 bytes
modules_app.js - 118707 bytes 

As you can see the size impact is insignificant. Interesting side effect since the change we made for the modules is that the initial module (module_html.js) does not change significantly or all if the templates do not change (for example if we add new views but use the rendering path instead of decoration one the views logic and its structure will still be in the later loaded modules thus not impacting the initial load).

As a final though: working with modules in closure is fun but is not as intuitive as one might expect it. Also there is no guided module separation and for this reason the developer has to be an expert on how closure dependencies work and how to arrange the files in such a way as to avoid direct dependency and instead add dependencies and code indirectly. In more modern solutions (like Dart for example) requiring a piece of code (or a whole library that is) lazily / on demand is as simple as adding a modifier to the import and whenever you need make sure to load it first

import 'myasynccode.dart' deferred as myasync;
// later on in my code
main() {
  // Wait for user to request that piece of functionality
  await myasync.loadLibrary();
  // call code from library
  myasyncMethod();
}

Never the less closure can be used successfully to manage async code loading and thus help us with the task of greeting our newly come users as fast as possible with the best product we can offer.

Happy coding!
Публикуване на коментар