Build a Vite Plugin to Inline Critical Resources

Vite’s defaults will lead to your page loading slower than it needs to. Vite will build your html to look something like this.

...
<head>
...
<script type="module" crossorigin="" src="myJavascript.js"></script>
<link rel="stylesheet" href="myCss.css">
</head>
...

The browser will find these two tags in the head and then have to request them and evaluate them before it can continue rendering the page. This means that your users will just be staring at a blank screen for a couple of seconds, especially on a slow connection or before any caching.

This is a good default. Most modern websites have some JavaScript or CSS that is critical for rendering the page correctly, but Vite has no way of identifying what is critical. So it will load it all at the top to make sure it doesn’t miss anything.

But we do know what is critical, so we should defer everything that is not critical, so we can render early with only the critical assets. Modern advice is to inline those critical elements. This great article, that you’ve probably run into thanks to chromes dev tools, suggests this pattern:

<style type="text/css">
.my-critical-css {...}
</style>

<link rel="preload" href="myCss.cs" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="myCss.css"></noscript>

And we can do the same thing for JavaScript even more easily.

<script>
    runCriticalJS();
</script>

<script type="module" crossorigin="" src="myJavascript.js" defer></script>

Implementation

JavaScript

Implementing this in JavaScript was very easy for my site because I don’t have a lot of JavaScript and most of what I do have is not necessary for rendering.

the only thing that is critical is my themes. I allow users to change themes and colour preferences and store that information in local storage. So I need to check that the user’s preference before rendering anything, otherwise, there will be a flash of incorrectly styled content.

So to solve this I can just minimise the relevant JavaScript and put it all inside a script tag in the head.

<script>
var themePref=window.localStorage.getItem("theme-preference");themePref&&document.querySelector(":root").setAttribute("data-theme",themePref)
</script>

Then I defer the rest.

<script type="module" crossorigin="" src="/assets/main.57999a66.js" defer></script>

To do this last bit I wrote a tiny plugin that finds the script file and adds a defer attribute to the end of it.

// deferNonCriticalJS.ts
export function deferNonCriticalJS(html: string): string {
    const jsRegex = /\n.*<script type="module" /;
    const nonCriticalJs = html.match(jsRegex);
    if (nonCriticalJs === null ) {
        return html
    }

    const deferredJs = nonCriticalJs[0] + 'defer ';

    return html.replace(jsRegex, deferredJs)
}

// criticalPlugin.ts
import {deferNonCriticalJS} from './criticalJS';

export default function (criticalCssDir: string) {
    return {
        name: 'html-transform',
        async transformIndexHtml(html: string) {
            return deferNonCriticalJS(html)
        }
    }
}
// vite.config.ts
export default defineConfig({
...
    plugins: [
        critical(),
    ]
...
}

Inline CSS

I could technically do the same for the CSS. However, there is a lot more css, and it’s more likely to change. So I need an automated solution.

To do this I decided to separate my CSS into critical and non-critical directories. Then I loop through every file in the non-critical directory, minify the content and return a string with all the CSS in it.

export async function findAndMinifyCritical(dir: string): Promise<string> {
    let criticalCss = '';

    fs.readdirSync(dir).forEach(file => {
        const f = `${dir}/${file}`;
        const content = fs.readFileSync(f).toString();
        criticalCss += csso.minify(content).css;
    });

    return criticalCss;
}

Then I append the critical css to the end of the head tag

export function inlineCritical(html: string, critical: string): string {
    return html.replace('</head>', `<style>${critical}</style></head>`);
}

Finally, I defer the non-critical CSS.

export function deferNonCritical(html: string): string {
    const styleRegx = /\n.*<link rel="stylesheet" href=".*">/;
    const nonCriticalCss = html.match(styleRegx);
    if (nonCriticalCss === null) {
        return html;
    }

    const nonCritCss = nonCriticalCss[0]
        .replace(
            'rel="stylesheet"',
            'rel="preload" as="style" onload="this.onload=null;this.rel=\'stylesheet\'"');

    return html.replace(
        styleRegx,
        nonCritCss + `<noscript>${nonCriticalCss}</noscript>`
    );
}

Putting it all together

To put it all together I create this function

import {deferNonCritical, findAndMinifyCritical, inlineCritical} from './criticalCss';
import {deferNonCriticalJS} from './criticalJS';

export async function deferAndInline(html: string, criticalCssDir: string): Promise<string> {
    const htmlWithDefferredJs = deferNonCriticalJS(html);
    return inlineCritical(
        deferNonCritical(htmlWithDefferredJs),
        await findAndMinifyCritical(criticalCssDir)
    )
}

And I call it within the plugin

export default function (criticalCssDir: string) {
    return {
        name: 'defer-and-inline-critical',
        async transformIndexHtml(html: string) {
            return await deferAndInline(html, criticalCssDir)
        }
    }
}

Which finally lets me add it to my config

plugins: [
    {
        ...critical(__dirname + '/src/criticalCss'),
        apply: 'build'
    },
]

I only run it on build because it slows down dev a lot. I could improve the code speed, but it wouldn’t be worth it, most of the slowdown comes from looping through a directory full of CSS files and reading them all.

Conclusions

If you’re working on a larger project you’ll probably want to look into packages like critical.

But for a personal project or if you need fine-grained control over how critical assets effect rendering, you can learn a lot by trying to set something like this up for yourself.