10 déc. 2014

The fastest loading screen for Meteor

Introduction

Meteor offers a great way to build web apps at an incomparable speed. It leverages the power of Javascript anywhere. In production, its powerful server side build system named Isobuild transpiles all your HTML files into JS files, thanks to Blaze, and packs them with your JS files. This operation leads to a single JS file loaded by a tiny initial HTML loader that bootstraps your app.

Depending on your development strategy, this single JS file can end up very fat. This could seriously annoy your first time users who will wait many seconds before seing something on their screen. Of course, proxying your servers with Gzip or SPDY capable fronts greatly helps lowering this initial loading. Truncating this JS file could also help. Using a multiple domains strategy and CDNs also offer great loading time cuts. Still, on mobile, with a bad network access like GPRS, these several seconds rapidly become a problem. To understand the ins and outs of web performance optimization, WPO, I strongly recommend reading the book High Performance Browser Networking by Ilya Grigorik.

What is this about?

Not to be confused with the great masterpiece fast render from Arunoda Susiripala which is used for fastening the initial data required from your collections, the technic that I expose hereafter tackles the initial page load time, PLT. The first paints on the screen, if you prefer. This first paint comes even before the tiny initial HTML loader is fully loaded by your users's terminal.

On a regular Meteor app, while your users connect to your site and till the initial load of the JS file, their browser remains blank. Of course, they could see that something is happening. Almost all browser display at least a progress indicator. But users may not notice it and think that your site is either dead or hosted on a distant other planet. That is just very bad for user acquisition, isn't it? If you want to better understand how this affects user's perceptions as well as your search engine optimization strategy, SEO, Paul Irish has covered this in great details in his incredibly informative talk: Fast enough (check the comments for the slides and the Youtube video). What you need to know is that your PLT will be nice if your initial screen painting is under 14k. That seems like an impossible goal to achieve with an out-of-the-box Meteor app with an average weight of ~50k of compressed initial JS file.

But Meteor is far from being a classic website approach and is definitely not a low end build tool. You can use the raw power of its Isobuild to ask your servers for an injection of your initial screen content directly within the tiny initial HTML loader. It is the key to unlock the 14k barrier.

Performing the injection

This could be a bit scary for the majority of Meteor devs. But once again, Arunoda has you well covered. By using another great masterpiece from Arunoda and Gadi Cohen, this injection process is a no brainer: inject initial. Let us jump in the code.

In your app, installing inject initial is the one step classical stance:
meteor add meteorhacks:inject-initial
For the sake of brevity, the codes hereafter are in CoffeeScript.

In your server code, you can perform the initial injection. Hereafter, I'm just putting a little CSS spinner. Nothing fancy.

/server/startup.coffee
Inject.rawHead 'loader-style',
  # Force the initial scale for Android and iOS as our spinner may be
  #  distorted by their default viewport values.
  '<meta name="viewport" content="width=device-width,maximum-scale=1,' +
    'initial-scale=1,user-scalable=no">' +
  # The loading spinner needs some theming.
  '<style>' +
    'html{background-color: #36342e;}' +
    'body{color:#ddd;overflow:hidden;width:100%;}' +
    '.spinner {' +
      'bottom:0;height:80px;left:0;margin:auto;position:absolute;' +
      'top:0;right:0;width:80px;' +
      '-webkit-animation: rotation .6s infinite linear;' +
      'animation: rotation .6s infinite linear;' +
      'border-left:6px solid rgba(255,194,0,.20);' +
      'border-right:6px solid rgba(255,194,0,.20);' +
      'border-bottom:6px solid rgba(255,194,0,.20);' +
      'border-top:6px solid rgba(255,194,0,.9);' +
      'border-radius:100%;' +
    '}' +
    '@-webkit-keyframes rotation {' +
      'from {-webkit-transform: rotate(0deg);}' +
      'to {-webkit-transform: rotate(359deg);}' +
    '}' +
    '@-moz-keyframes rotation {' +
      'from {-moz-transform: rotate(0deg);}' +
      'to {-moz-transform: rotate(359deg);}' +
    '}' +
    '@-o-keyframes rotation {' +
      'from {-o-transform: rotate(0deg);}' +
      'to {-o-transform: rotate(359deg);}' +
    '}' +
    '@keyframes rotation {' +
      'from {transform: rotate(0deg);}' +
      'to {transform: rotate(359deg);}' +
    '}' +
    '</style>'
# The loading spinner is a CSS animation.
# /!\ WARNING: The trick is to create a fake body by injecting data
# in the HTML's head as Meteor is requesting JS  file in a blocking
# fashion and mobile only allow 1 HTTP request at a time on a GPRS network.
Inject.rawHead 'loader-body2', '<body><div class="spinner"></div></body>'
Here we have somewhat altered, the DOM structure of our app. Indeed, we have just created a body while the head of your your initial HTML loader wasn't even finished to get build. Is that a big problem? No. Your server will carry on filling the rest of your app in the body section that you have created (the styles, the scripts, ...).

At this point, you could tell me: "But wait? There is now a title and meta tags in the body of my app. That's just a disgusting code.". You are right. But the browsers are really not impacted by this. I could reply back: "Who care?". But, indeed, I care. Simply because one of the biggest interest of web technology is that all your codes are readable. It is important to let other developers having the possibility to understand how stuff works. It is part of the learning legacy that we owe to the web. So let us put things in the correct order.

Cleaning our loaded codes is done directly on the client side. We do that once everything is loaded. No hurry as it has no impact except a good readability of our DOM structure.

/client/startup.coffee
Meteor.startup ->
  # Reshape DOM: put back title and meta elements in the head.
  # style and script tags can leave in the body tag.
  $head = $ 'head'
  for tag in ['meta', 'title']
    $tags = $ tag
    $head.append $tags.clone()
    $tags.remove()
That's it. Nice and clean.

Testing it

There are several ways of testing these results and their behavior. On Mac, I use Network link conditionner, a handy tool provided by Apple. It allows you to modify the behavior of you TCP stack and use GPRS, 3G, ... profiles. Very neat when you are testing hybrid or native apps.

If you are testing full web apps, I strongly recommend using Chrome DevTools. The mobile emulator as a special feature allowing to change the network throttling.

Some constraints you need to be aware of

When your initial HTML loader and your initial JS file are being loaded, the JS engine and the SVG rendering engine are paused. I've always been annoyed by this browser behavior but we have to live with it. Thus, you can't put JS loaders or SVG animated loaders. You should favor CSS loader and choose the one that will be the less CPU intensive. Here, you should favor CSS3 animated loader which are animated by the GPU as the CPU will be intensively interpreting what it is finishing pumping from you servers.

A side note to finish this little article. This technic is not here to bypass the necessary work of making your app as lightweight as possible. It is still a good practice to ensure that your app will avoid consuming the data subscription of your users ;-)

Aucun commentaire:

Enregistrer un commentaire