Skip to content

Instantly share code, notes, and snippets.

@jeremenichelli
Last active June 26, 2018 15:33
Show Gist options
  • Save jeremenichelli/12c09cee1bc826b71e81ca3d3482f681 to your computer and use it in GitHub Desktop.
Save jeremenichelli/12c09cee1bc826b71e81ca3d3482f681 to your computer and use it in GitHub Desktop.
[DRAFT] Faster boot up times for static sites with Rollup

Faster boot up times for static sites with Rollup

Bundlers are a fundamental part of any web project nowadays. They allow us to encapsulate, share and organize our codebases better and give us access to vast collection of packages ecosystem to consume from. But did you ever wonder how the output of a bundler looks like and how it impacts your scripts runtime?

The footprint of bundlers

As modules weren't a thing on the web, tools like browserify or webpack needed to create a way for CommonJS modules to run on browsers.

The general approach was to add an overhead on top of the input code. This overhead consists in an array wrapping all the modules (local and from the npm registry) in your project and a iife (immediately invoked function expression) that kicks off the script.

The initial content of the iife will call a predefined method that will crawl into the array of modules returning the resulting export of it. Here's a simplified example of this:

// utils/module-a.js

function a() {
  document.body.innerHTML = 'a';
}

export default a
// index.js

import a from './utils/module-a.js';

a();

Each of the modules' content in placed inside function calls as part of a modules array as previously mentioned. Also, bundlers will replace the original import statements with its own require internal method.

(function(modules) {
  function internalRequire(index) {
    return modules[index](internalRequire);
  }
  
  internalRequire(0);
})([
  // originally index.js
  (function (require) {
    const a = require(1);
    
    a();
  }),
  // originally utils/module-a.js
  (function (require) {
    function a() {
      document.body.innerHTML = 'a';
    }
    
    return a;
  })
]);

Though this is clever, it slows down the timing your scripts kicks off once is loaded and parsed.

Why? Imagine a big project where you are following the best practices and decoupling your application into little reusable pieces, the result will be an array with lots of modules and translate into a huge number of calls for the internalRequire method when your initial script requires a module, that requires a module, that requires another module, and... well, you got it by now.

This is a price we pay to be able to organize our projects today, and tools like webpack have a huge collection of plugins that let us extend its initial behavior to handle all type of assets and make our lifes way easier.

Truth is that if your bundling for smaller project with simpler needs, like a static site, you might not need all this power horse and it might be better to have everything wrapped up in a much simpler architecture with a final output that runs faster.

Here's where Rollup comes in.

Everything is a function

Rollup premise is to inline all your code into a function and turn every module into a method itself, all in the same execution scope. This means the output code will look more like if you've written it in a single file.

For our previous example the output from Rollup would look something like this:

(function() {
  function a() {
    document.body.innerHTML = 'a';
  }
    
  a();
})()

Much simpler, right? Though this is far away from a real case scenario you can see how we don't need extra method calls, which means faster boot up times, no overhead and smaller bundle size.

A bundle per page Rollup recipe

Rollup does not only work as a CLI on your terminal but can also be imported to create our own build script. In this case we are going to see how we can build a collection of specific bundles that we can later include in our static site pages or templates.

Imagine a static site project using Jekyll with this structure:

.
├── _includes
|   ├── header.html
|   └── footer.html
├── _layouts
|   └── default.html
├── about
|   └── index.md
├── blog
|   └── index.md
├── config.yml
└── index.md

For this simple setup we would like to load a specific bundle for about, blog and home page of the project.

Initial setup

By default, Rollup only supports ES modules notation, so in order to handle npm packages and CommonJS notation we will need to import some plugins along with Rollup itself.

// scripts/build.js

const rollup = require('rollup');
const commonjs = require('rollup-plugin-commonjs');
const resolve = require('rollup-plugin-node-resolve');

We can also place our bundle configuration in an object that we can easily access later.

const bundles = [
  {
    "input": "./src/js/about.js",
    "output": "./assets/about.js"
  },
  {
    "input": "./src/js/blog.js",
    "output": "./assets/blog.js"
  },
  {
    "input": "./src/js/home.js",
    "output": "./assets/home.js"
  }
];

The last piece of our initial setup is a base configuration object to pass to Rollup.

const baseConfig = {
  plugins: [
    // resolve node modules
    resolve(),
    // support commonjs
    commonjs({
      include: 'node_modules/**'
    })
  ]
};

It's build time

Rollup has an async internal API, so we are going to generate an array of Promises, passing by our original configuration with each input from our bundles array.

const bundlePromises = bundles.map(b => {
  return rollup.rollup(Object.assign({}, { input: b.input }, baseConfig));
});

When all the Promises are resolved we can save the results into our project's folder:

Promise.all(bundlePromises)
  .then((results) => {
    results.map((b, index) => {
      const output = bundles[ index ].output;

      b.write({
        file: output,
        format: 'iife'
      });
    });
  });

Each result of the rollup method call is an object with a write method that simplifies our workflow of outputing the final bundles into the disk. We call this for each bundle and specify the iife which means Rollup will wrap the output content with one.

At the end of our script we just call the build async function and include it in the scripts of the `package.json file of the project to call it on our terminal.

"scripts": {
  "build": "node ./scripts/build.js"
}

Script injection

Depending on the static site generator you are using, you can declare metadata and consume it at build time. For example in Jekyll, we can add the script url in the yaml front matter.

about/index.md

---
title: About
script_file: 'about.js'
---

...

Later on our layout templates or includes we can check for this and inject the script tag.

_includes/footer.html

{% if page.script_file %}
  <script src="/assets/{{ page.script_file }}">
{% endif %}

Numbers or it didn't happen

What are the benefits of using Rollup on a setup like this one? Nolan Lawson already wrote a great detailed post about how collapsing your dependencies like Rollup does benefits your project load and run time.

I think is a great follow up for this article if you are interested in these optimizations.

Wrap-up

Bundlers are powerful and extensible tool that help us scale big web applications super fast. For smaller projects with simpler architectures, Rollup removes overheads and extra code while providing similar features.

Both webpack and Rollup are excellent tools with different strengths and purposes, if you are more interested about what these differences are I recommend this article by Rollup author Rich Harris on webpack's publication.

Sounds weird right! But truth is webpack team loves all improvements Rollup has come up with and they even incorporated some of these in webpack core functionality like tree-shaking and scope hoisting.

A good outcome of competition in open source is that we the developers get better and smarter tools to use in our projects.

@stefanjudis
Copy link

Heyo. :) this looks great already I have a few notes though. :)

My main concern right now is that with the title "Faster boot up times for static sites with Rollup" I expected a comparison. The article finished and I felt like "But how much faster is it?" – a quick example could help IMO.

Additionally I don't really see the connection to "static sites" maybe you could go into that a bit, too?

But apart from that I think it's really interesting. :)

@jeremenichelli
Copy link
Author

jeremenichelli commented Jun 23, 2018

Hi @stefanjudis, thanks for the comments. After giving it a fresh reading round I got the same feeling, I need either to change the title or create an example comparing speeds.

About the static sites, it's a subjective view around the tools mentioned, Rollup is not that powerful out of the box but leaves no mark or overhead, it's actually what I use to bundle the scripts on my blog. Maybe I need to rephrase the second part of the article and put the reader more in context.

I'll give it a try this weekend and see if I can come up with something better there.

@jeremenichelli
Copy link
Author

Update

I gave it another try and added a Jekyll project context to the build script with better code snippets and a link to Nolan Lawson's article which I think already does a great job around these numbers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment