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.
Original article available here
Top comments (0)