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
Promise
s
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 import
ing 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?