DEV Community

Ethan Carlsson
Ethan Carlsson

Posted on

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>
...
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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

<script>
    runCriticalJS();
</script>

<script type="module" crossorigin="" src="myJavascript.js" defer></script>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Then I defer the rest.

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

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(),
    ]
...
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>`);
}
Enter fullscreen mode Exit fullscreen mode

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>`
    );
}
Enter fullscreen mode Exit fullscreen mode

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)
    )
}
Enter fullscreen mode Exit fullscreen mode

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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Which finally lets me add it to my config

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

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.

Original article available here

Top comments (0)