Right-Click → View Page Source

Generate a Critical JavaScript Bundle Using Rollup

For performance and user experience reasons there are certain tasks that we want to complete before DOM content, stylesheets, or scripts have loaded. Critical JavaScript, inlined into <script> blocks in the <head> of an HTML document, is a pattern for achieving this:-

<!DOCTYPE html>
<html>
  <head>
    <script>
      console.log('I am critical!');
    </script>
  </head>
</html>

Examples of such tasks include running CSS and JavaScript feature tests, loading polyfills, and performing conditional logic.

Keep Things Simple

Given that Critical JavaScript is inlined directly into the <head> of every HTML response, it's essential that we keep the bundle size small.

A step towards this is keeping the code simple with respect to the JavaScript and DOM APIs used and in-turn disabling transpilation tools from doing anything other than syntax transformation (see below for instructions on how to configure Babel).

Configuring Browserslist to reflect your support matrix will allow for the use of complimentary tools such as eslint-plugin-compat to ensure that you're not using unsupported features in production.

All of this means you can leverage modern ES6 syntax such as Arrow Functions, but omit JavaScript APIs such as Promises and DOM APIs like Element.classList. However, the syntax and features available to you are entirely dependent on your own browser support matrix.

If polyfilling a feature is unavoidable then I would recommend doing so by directly importing specific modules from core-js.

Why Rollup

There’s no 2-ways about it: Rollup creates bundles that are simpler and smaller than those created by alternatives such as Webpack.

The following is a starting point for how to best configure Rollup to output critical.js bundles with the smallest footprint:-

// rollup.config.js
import path from 'path';

import resolve from '@rollup/plugin-node-resolve';
import commonJs from '@rollup/plugin-commonjs';
import replace from '@rollup/plugin-replace';
import babel from 'rollup-plugin-babel';
import { terser } from 'rollup-plugin-terser';
import filesize from 'rollup-plugin-filesize';

const { NODE_ENV = 'development' } = process.env;

const isProduction = NODE_ENV === 'production';
const ANALYZE = process.env.ANALYZE ? process.env.ANALYZE === 'true' : false;

export default {
  input: path.join(process.cwd(), 'critical.js'),

  output: {
    dir: path.join(process.cwd(), 'critical.min.js'),
    format: 'iife',
    sourcemap: isProduction ? false : 'inline',
  },

  plugins: [
    replace({
      'process.env.NODE_ENV': JSON.stringify(NODE_ENV),
    }),

    resolve({
      browser: true,
    }),

    commonJs({
      include: 'node_modules/**',
    }),

    babel({
      exclude: 'node_modules/**',
      babelrc: false,
      presets: [
        [
          '@babel/preset-env',
          {
            debug: ANALYZE,
            modules: false,
            useBuiltIns: false,
          },
        ],
      ],
    }),

    isProduction && terser(),

    isProduction && filesize(),
  ].filter(Boolean),
};

Below is just an example of some of the tasks that you might perform via critical.js. It's by no means comprehensive, but it's a good example of the purpose of this category of JavaScript bundle.

// critical.js
// polyfill asynchronously loading CSS from `<link rel="preload" />` stylesheets
import 'fg-loadcss/src/cssrelpreload';

// find out more about `font-display` here:
// https://css-tricks.com/font-display-masses/#article-header-id-3
import supportsFontDisplay from '~/utilities/supports/font-display';

const htmlClassNames = ['js'];

if (supportsFontDisplay) {
  htmlClassNames.push('supports-font-display');
}

// overwrites any existing classes on the `<html>` element (e.g. `"no-js"`)
document.documentElement.className = htmlClassNames.join(' ');

This pattern is especially useful when used in conjunction with custom web fonts in order to control FOUT, FOIT, and FOFT.

Inlining the Script

The final step, and the one that I can write the least about, is inlining the compiled critical.min.js into your HTML. The mechanism for this will vary drastically depending on your technology stack and deployment processes.

Field Notes

Are you currently using this approach in your sites and applications? If so, how has your experience been so far? Do you plan to implement Critical JavaScript in the future? What kinds of tasks do you think this is suited to?

💬

Join the converstation on the DEV Community or send a Webmention

About the author

A profile photo of Saul Hardman

Hiya, I'm Saul Hardman, a front-end engineer based in Copenhagen, Denmark. I'm currently available for work, so if you need help building websites, web applications, and everything in between get in touch!

To stay up to date, follow me on Twitter and GitHub and be sure to subscribe to the RSS feed.