Skip to content

Asynchronous, non-blocking loading pattern for CSS and JavaScript resources

License

Notifications You must be signed in to change notification settings

jkphl/shortbread

Repository files navigation

shortbread NPM version NPM downloads Build Status Coverage Status Dependency Status Development Dependency Status

is a Node module that helps you implement an asynchronous, non-blocking loading strategy for your CSS and JavaScript resources, thus improving the start render time of your websites. It's intendend to be part of your toolchain and plays well with Grunt, Gulp and alike.

Installation

To add shortbread as a development dependency to your Node project, run

npm install shortbread --save-dev

Asynchronous resource loading

Most often, not all the CSS and JavaScript resources referenced by your website are really necessary for initially rendering the page to your visitor. It makes sense to distinguish between critical and non-critical resources and defer the loading of non-critical ones until after the page got rendered to the screen. To further speed up things, it might also make sense to inline the most critical stuff directly into your HTML source when the page is loaded for the first time (and leverage browser caching on all subsequent visits).

Let shortbread simplify these things for you and provide it with

  • the JavaScript resources you want to load,
  • your CSS resources,
  • possibly some critical CSS / JavaScript resource (subject to inlining),
  • an optional cookie slot (see below) and
  • an optional JavaScript callback you want to call after all resources have been loaded for the first time.

Based on these values, shortbread creates two HTML fragments to be included into the <head> of your documents depending on whether it's the first or a subsequent page load. You will need some server side code to accomplish that.

First page load

First page load example

The first page load HTML fragment will do the following:

  1. It inlines a small JavaScript (including prelink) which is needed for creating a client-side shortbread instance and performing the following steps.
  2. It inlines your critical CSS and / or JavaScript resources (if any).
  3. It loads your non-critical JavaScript resources with async and defer and registers them with shortbread once they finished loading. If a JavaScript resource is listed as both critical (inlined) and non-critical (external), the non-critical instance is loaded with rel=prefetch (polyfilled if necessary) to prevent double evaluation.
  4. It loads your CSS resources with rel=preload (polyfilled if necessary) and registers them with shortbread once they finished loading.
  5. It wraps up with a <noscript> fallback for (synchronously) loading at least the CSS resources in case there's no JavaScript available.
  6. As soon as all CSS and JavaScript resources finished loading*, shortbread
    • sets a cookie to let your server distinguish between initial and subsequent page loads and
    • runs the final JavaScript callback you provided (if any).

* Please be aware that, depending on the browser in use, the non-critical version of hybrid JavaScript resources (see 3) may or may not have finished loading when the callback gets triggered, as e.g. Internet Explorer 11 (which has rel=prefetch support) doesn't fire an onload in this case. In the worst case, the resource has to be loaded in a synchronous manner during the second page load.

Subsequent page loads

Subsequent page load example

Based on the cookie set during intial page load, your server should be able to detect subsequent visits and include the alternative HTML fragment. This one leverages the browser cache and synchronously loads both

  1. your JavaScript and
  2. CSS resources.

Shortbread cookie

The shortbread cookie serves two purposes:

  1. If it is set, the server recognizes that the resources have already been loaded and switches into subsequent page load mode (i.e. it doesn't make the client load the resources asynchronously as they should have already been cached).
  2. The expected cookie value represents a unique set of resources and changes whenever one of the resources changes in content. This way the cookie serves as a cache busting measure and ensures that the resources get updated as soon as they're modified on the server. Please be aware that you also need to adapt the server-side code whenever the resources change.

API

To use shortbread from JavaScript, you'd do the following (example for NodeJS):

const shortbread = require('shortbread');
const fragments = shortbread(js, css, critical, 'main', 'allLoaded', {prefix: '/'});

The return value will be an object with the following properties:

{
    initial: '<script>...</noscript>',      // Initial page load fragment
    subsequent: '<script>...</link>',       // Subsequent page load fragment
    resources: {                            // Single resource and corresponding hashes
        '422a6fc6': 'path/to/resource/1',
        '60062743': 'path/to/resource/2'
    },
    hash: 'df5bf8f7',                       // Cookie value when all resources are loaded
    cookie: 'sb_main'                       // Cookie name (here: including "main" slot)
}

You can use this object as a source of templating values when rendering the the server-side code to handle client requests.

The signature of shortbread() looks like this:

/**
 * Create HTML fragments for assynchronously loading JavaScript and CSS resources
 *
 * @param {File|Array.<String,File>|Object.<String,File>} js  [OPTIONAL] JavaScript resource(s)
 * @param {File|Array.<String,File>|Object.<String,File>} css [OPTIONAL] CSS resource(s)
 * @param {File|Array.<File>|Object.<File>} critical          [OPTIONAL] Critical CSS / JS resource(s)
 * @param {String} slot                                       [OPTIONAL] Cookie slot
 * @param {String} callback                                   [OPTIONAL] Callback
 * @param {Object} config                                     [OPTIONAL] Extended configuration
 */
function shortbread(js, css, critical, slot, callback, config) {
    // ...
}

JavaScript and CSS resources

shortbread expects you to use Vinyl objects to pass in your JavaScript and CSS resources (js, css and critical arguments). When creating <script src="..."> and <link href="..."> elements, it uses the Vinyl objects' relative property to determine the request paths for your resources, so you can easily use virtual paths for your HTML output like in this example:

const vinyl = require('vinyl-file');

const script = vinyl.readSync(path.join(__dirname, 'path/to/script.js'));
script.path = `${script.base}/js/mysite.js`;

// Will create `<script src="js/mysite.js" async defer></script>`

As you see, I recommend vinyl-file for creating Vinyl objects of your files.

For external resources you can also pass in absolute URLs for js and css instead of Vinyl objects.

shortbread uses regular expressions to filter and separate the critical CSS & JavaScript resources. Resources not matching any expression will be ignored. You may configure custom file name patterns via the extended configuration options css and js.

Cookie slot

By default, the name of the shortbread cookie is sb. If you're using multiple resource sets, however, you'll have to keep track of them separately by "slotting" the cookie. When you pass a slot argument to the shortbread() function, say "set1", the cookie will be named sb_set1. The actual cookie name is returned in the cookie property of shortbread()'s result object.

Extended configuration

The sixth argument to shortbread() may be an object with following properties:

Property Type Description
prefix String Prefix for all resource URLs (JavaScript and CSS). Set it to "/" for instance in order to use root-relative paths like <script src="/path/to/script.js">.
css Array List of regular expressions to match critical CSS resource file names (defaults to ['\\.css$'])
js Array List of regular expressions to match critical JavaScript resource file names (defaults to ['\\.js$'])
debug Boolean Print some debug messages to the console (output level needs to be configured appropriately)

Server side load type detection

The way you implement the server-side load type detection totally depends on your environment and the technologies involved in your website. In most cases you will want to plug shortbread into a templating process that creates code snippets you can integrate into your setup and use for handling client requests. An example handlebars template for creating a PHP script could look like this:

<!DOCTYPE html>
<html lang="en">
    <head><?php

        // If the shortbread cookie is present and matches the expected master hash: It's a subsequent page load
        if (!empty($_COOKIE['{{cookie}}']) && ($_COOKIE['{{cookie}}'] === '{{hash}}')):

            ?>{{{subsequent}}}<?php

        // Else: It's an initial page load
        else:

            ?>{{{initial}}}<?php

        endif;

        ?><meta charset="UTF-8">
        <title>My site</title>
    </head>
    <body>
        <!-- Page content -->
    </body>
</html>

This is, by the way, pretty much the code used by the example implementation included in this package. You can run it and see shortbread in action by typing

npm run php

in your console and directing your browser to 'http://localhost:8080'. Obviously, you will need PHP (CLI) to be installed on your machine for this.

Gulp usage

shortbread.stream() is an additional interface that's intended to be used with streams and supports some extended features. Simply pass in the JavaScript and CSS resources you'd like to include (shortbread will seperate them internally — see below) and you'll get two HTML fragment files as the output.

The shortbread.stream() signature looks like this:

/**
 * Streaming interface for shortbread
 *
 * @param {File|Array.<File>|Object.<File>} critical    [OPTIONAL] Critical CSS or JavaScript resource(s)
 * @param {String} slot                                 [OPTIONAL] Cookie slot (optional)
 * @param {String} callback                             [OPTIONAL] Callback(s)
 * @param {Object} config                               [OPTIONAL] Extended configuration
 */
shortbread.stream = function stream(critical, slot, callback, config);

Again, the critical resources (if any) need to be passed in as single Vinyl object, Vinyl object array or object of Vinyl objects. Also the slot and callback arguments are identical to the regular API. The special config object defaults to these values:

{
    css: ['\\.css$'],               // List of regular expressions to match CSS resources
    cssUrl: [],                     // List of external CSS resource URLs
    js: ['\\.js$'],                 // List of regular expressions to match JavaScript resources
    jsUrl: [],                      // List of external JS resource URLs
    initial: 'initial.html',        // Name for the initial page load HTML fragment
    subsequent: 'subsequent.html',  // Name for the subsequent page load HTML fragment
    data: false,                    // Whether to create a JSON file with shortbread's return values
    prefix: ''                      // Prefix for all resource URLs (see extended configuration)
}

As you see, shortbread uses regular expressions to filter and separate your CSS and JavaScript resources. Any file that's not matched by any of the regular expressions will simply get passed through (and might be used for further templating processes, see below). External resources given as URLs (cssUrl and jsUrl) don't get filtered or validated in any way.

In case you're using a cookie slot, the slot name will be added to the fragment file names as in initial.<slot>.html and subsequent.<slot>.html.

If you set data to true, an additional JSON file shortbread.json (respectively shortbread.<slot>.json) will be created, containing the standard shortbread result object. You can use this file as variable source for a downstream templating process when generating your server side code. However, there's also a much smarter approach to this:

As mentioned above, shortbread will simply pass through any file that's not recognized as a CSS or JavaScript resource. Additionally, it sets the data property of these files to shortbread's result object which effectively emulates gulp-data's behaviour. That way (and with the help of gulp-filter) you can immediately plug this into a variety of templating engines like gulp-swig or gulp-jade / gulp-pug. Here's a gulpfile example that uses gulp-template to parse a Lo-Dash / Underscore template and create a simple PHP request handler:

const gulp = require('gulp');
const shortbread = require('shortbread').stream;
const vinyl = require('vinyl-file');
const path = require('path');
const filter = require('gulp-filter');
const template = require('gulp-template');

gulp.task('default', () => {
    const critical = vinyl.readSync('test/fixtures/critical.css');
    const tmpl = filter(['**/*.php'], { restore: true });

    // Start with your JavaScript, CSS and template resources
    gulp.src(['**/fixtures/script.js', '**/fixtures/style.css', 'gulp/*.php'], { cwd: path.join(__dirname, 'test') })

        // Run shortbread
        .pipe(shortbread(critical, 'main', null, { prefix: '/' }))

        // Filter all but the template file
        .pipe(tmpl)

        // Run the template engine
        .pipe(template())

        // Restore all files
        .pipe(tmpl.restore)

        // Write the files to their destination
        .pipe(gulp.dest('./tmp'));
});

The Lo-Dash / Underscore template might look like this:

<!DOCTYPE html>
<html lang="en">
    <head><?php

        // If the shortbread cookie is present and matches the expected master hash: It's a subsequent page load
        if (!empty($_COOKIE['<%= cookie %>']) && ($_COOKIE['<%= cookie %>'] === '<%= hash %>')) {
            include 'subsequent.main.html';

        // Else: It's an initial page load
        } else {
            include 'initial.main.html';
        }

        ?><meta charset="UTF-8">
        <title>My site</title>
    </head>
    <body>
        <!-- Page content -->
    </body>
</html>

Known problems

Currently there are no known problems. However, the module is in a very early stage and might have severe bugs. Please let me know if you spot one!

To-do

  • Add tests

Changelog

Please refer to the changelog for a complete release history.

Legal

Copyright © 2019 Joschi Kuphal [email protected] / @jkphl. shortbread is licensed under the terms of the MIT license.