RSS

Bundling and async loading Javascript for fast page loading using require.js

10 Jul

With thanks to Benja Eidelman for help with this Post

Main Idea

Browsers delay page first render until all resources mentioned in the <head> are fully loaded. That’s why the one of the most basic tips to optimize page perceived speed is to move all scripts to the bottom of the body. The obvious next step is to remove from body all scripts that can be loaded asynchronously using one of the many script loaders out there.

We know this is only the tip of the iceberg of optimizations and fine tuning techniques that can quickly introduce lots of noise in your codebase.

We’ll share an approach to perform all common page speed optimizations while keeping your javascript and templates clean and organized.

The tools involved are require.js, grunt.js and a small library we authored to achieve clean declarative integration between templates and javascript modules.

Optimizing page speed

First stage we used require.js as an async script loader.

We got async loading (not blocking the page render or dom ready), callbacks urls, handling errors, cascading, all these with paved cross-browser inconsistencies.

Besides more control on the page load waterfall, this gives you a consistent mechanism to deal with 3rd party scripts loading errors and consistent mechanism to run JavaScript code only when a 3rd party library is loaded successfully.

Organizing the JavaScript Codebase

But require.js is much more than a script loader. require.js implements AMD in browsers. Asynchronous Module Definition is a standard way to organize JavaScript code into modules that can supports dependency resolution and prevent global object pollution.

This is a must in order to write non-trivial JavaScript apps or libraries. This gives us the freedom to structure code semantically as we do in other programming languages. Saying goodbye to the typical 1-giant-file libraries.

Having automatic dependency loading means the end of hacky async initialization mechanisms (to ensure scripts are correctly initialized when the load order is not guaranteed), like register functions, temporal polyfills or process queues.

Also by setting up require.js aliases and shims we keep our js files agnostic of infrastructure decisions (like chosen CDNs or url fallbacks).

Also this leaves us in a pretty good position to move forward when ES6 is ready, including JavaScript native module support.

Bundling

Bundling and compressing static resources, as JavaScript files is one of the basic tips we’ll get from any page speed analysis tool available out there.

The purpose is to reduce the number requests (avoiding http handshaking costs) and bandwidth (by reducing size).

Compressing is done typically by compression algorithms like gzip or image compression formats (jpeg, png), and for text files (like JavaScript or CSS) we can go further using minifiers.

Using AMD to structure code gives us the ability to create bundles automatically based on the declared dependencies, we user the r.js optimizer tool (part of the require.js project) which gives enormous flexibility in terms of configuring the creation of different bundles.

This way using AMD we can keep excellent debuggability both on development environments (where bundling is off) and on production-like environments.

When bundling is off, require.js takes care of loading the necessary modules (dependencies) asynchronously, making the file tree on browser dev tools to show the same structure and content on the source project.
When bundling is on, we use uglify2 minifier as part of r.js optimization that provides source maps. Source maps are supported in most modern browsers and allow us to debug JavaScript code on production-like environments by seeing the same file structure and code that was used to produce the bundle, breakpoints and step-by-step debugging works as expected.

The final step in our bundle process is versioning bundle files. Versioning is a critical part of deploying applications. In order to use a CDN with long cache times you need to ensure cache is purged when new versions are deployed, the safest way to do this consistently is using versioned filenames to ensure cache busting.

And using content hashing ensure cache is busted if (and only if) needed (ie. when content changes).

To keep all this process in place we use grunt.js (a node.js build tool), which provides us with a great ecosystem of 3rd party plugins for most of these tasks, and it’s pretty easy to extend.

Declarative html+js integration

Once we moved to a fully AMD-based JavaScript structure, and setup r.js automatic bundling, we realized there was an important missing piece in this architecture.

How do we make use of this modules in our html UI without introducing noise or polluting the beautiful declarative-ness of our templates.

Now that we embraced the clean code organization that AMD allowed us, we decided to keep our templates equally clean, free of embedded JavaScript code.

In order to do so, we modeled the typical cases where JavaScript modules are related to the  UI, and made them explicit in our model:

Widgets: modules that attach to a specific DOM element, modifying it’s appearance or behavior, many instances can exist on a page (a concept similar to jQuery UI Widgets)

Behaviors: modules that custom behavior that can be enabled or disabled on a page basis, they are not associated to any specific visual element. eg. Keyboard shortcuts, auto-scrolling.

View-Modules: very small modules that can wire-up elements in a specific template. (a concept similar to codebehind files in webforms)

Now these are the only entry-points in the html templates to JavaScript modules (all the other modules are dependencies of these).

And we established an html convention to declare them:

On pure html:

<h1>Using pure html</h1>
<section>
    <H2>Widgets</H2>

    <!-- simplest example, will load "user-profile-pic" AMD module, and initialize this DOM element with it -->
    <div data-widget="user-profile-thumb"></div>

    <!-- specifying parameters for this widget instance -->
    <div data-widget="user-profile-thumb" data-user-profile-thumb-parameters='{"size": "large"}'></div>

    <!-- multiple widgets per element -->
    <ul data-widget="accordion, dockable" data-dockable-parameters='{"position": "left"}'>
        <li>item 1</li>
        <li>item 2</li>
        <li>item 3</li>
    </ul>

</section>
<section>
    <h2>Behaviors and View Modules</h2>
    <!-- lists of behaviors or view modules are specified on json format in script[data-modules] tags -->
    <script data-modules type="application/json">
        {
            "view":{
                "views/home/index":""
                },
            "behavior":{
                "keyboardShortcuts":"",
                "iefixes":">=8"
                }
        }
    </script>
</section>

And using razor:

<h1>Using Razor</h1>
    <h2>Widgets</h2>
    <div data-user-profile-thumb-parameters='{"size": "small"}'></div>
    <div></div>
    <!-- Razor helper to register multiple widgets using a selector -->
    @{ Widget.RegisterByCssSelector("user-profile-thumb", "div.profile", new { size = "large" }); }
    <!-- an optional parameter (C# anonymous object) is used as default it no parameters exists on the element -->
</section>
<section>
    <h2>Behaviors and View Modules</h2>
    <!-- Activate Behaviors -->
    @{ Behavior.Activate("iefixes", ">=8"); }
    <!-- Activate multiple Behaviors -->
    @{ Behavior.Activate("keyboardShortcuts, iefixes"); }
    <!-- Load the view module associated (by path) to the current template -->
    @{ ViewModule.Register(); }
    <!-- Finally, print the script tag with all the behavior and view modules declared so far -->
    @Module.LoadAll()
</section>

Finally this data-widget attributes and script tags are parsed and modules get loaded on the client using a small JavaScript library that runs on page initialization, and supports Ajax loaded content too.

Moving towards this declarative syntax allowed used to customize our bundling process, now we analyze templates looking for AMD modules (widgets, behaviors or view-modules) and create bundles based on them (dependencies are added automatically by r.js)

Now each time a module is used on a template it’s automatically bundled without extra work.

Later, if desired, this default behavior can be overridden by moving specific modules to separate custom bundles (eg. modules that are only used on less visited areas of the site)

Conclusion:

As you can see, we found an architecture that allowed us to tune the page load towards perceived performance (the time to first render, and the time until main elements are responsive reduced significatively), while still keeping the codebase cleanly organized and optimal in terms of maintainability, by keeping configuration as lean as possible.

Here are waterfall charts of one of the hottest pages in our website:

Image

Below is the same page in dev without bundling so you can really see how require has changed the waterfall. And of course see the benefit of bundling where we save over 24 http requests.

Image

The benefit is a much faster feeling website. As you can see above on the production /live page, we are seeing start render times under a second when we still have things loaded. Start render is before the main JS file is loaded.

Advertisements
 

Tags: , ,

One response to “Bundling and async loading Javascript for fast page loading using require.js

  1. SutoCom

    November 20, 2013 at 7:11 am

     

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
%d bloggers like this: