I'm using a Netlify build plugin to automatically split out and inline critical CSS for my personal website.
Inlining critical CSS (the CSS needed to display the immediately visible part of the page) makes a lot of sense on a static site where the 'above the fold' content is predictable, and it can help with getting a fast First Contentful Paint as the less CSS we have to download, the better (given its render-blocking nature).
With most critical CSS implementations we inline the critical CSS and lazyload the rest of the CSS using the media="print"
and onload
trick:
<link
rel="stylesheet"
href="/assets/styles.css"
media="print"
onload="this.media='all'"
/>
This trick requires JavaScript, so we need to provide an alternative for users without working JavaScript:
<noscript>
<link
rel="stylesheet"
href="/assets/styles.css"
/>
</noscript>
A while ago I'd noticed that the version (or perhaps the configuration) of the Critical package used by the netlify-plugin-inline-critical-css
Netlify build plugin was missing a <noscript>
fallback, so I manually added it to my Eleventy template, no big deal.
A few weeks ago, I switched to using Vite and I noticed that my <noscript>
fallback had stopped working. Vite renames our CSS and JS to give them a hashed file name, then detects links to these assets and updates the path to use the hashed version, like this:
<link
rel="stylesheet"
href="/assets/styles.833a7f93.css"
/>
However, Vite didn't seem to be picking up on the link
element within my <noscript>
fallback.
To get around this problem I used the transformIndexHtml
hook inside a small Vite plugin (inlined into my vite.config.js
file) to add the fallback in myself:
const { defineConfig } = require('vite');
const addNoscriptCss = () => {
return {
name: 'add-noscript-css',
transformIndexHtml(html, { chunk }) {
const tags = [];
Array.from(chunk.viteMetadata.importedCss, assetUrl => {
tags.push({
tag: 'noscript',
children: [
{
tag: 'link',
attrs: {
rel: 'stylesheet',
href: `/${assetUrl}`,
},
},
],
injectTo: 'body',
});
});
return {
html,
tags,
};
},
};
};
module.exports = defineConfig({
plugins: [
addNoscriptCss(),
],
});
That plugin will take the generated HTML file and add in a <noscript>
fallback for every CSS file that Vite is aware of on the page.
I have a really simple setup (a single HTML page where every CSS <link>
needs a fallback), so this snippet might not be bulletproof for other projects, but I hope it helps anyone who stumbles on it.
Update, October 2022
After updating Vite to 3.1.0 and working on my site (adding some new pages, tweaking some settings - the usual) this plugin stopped working. chunk.viteMetadata.importedCss
was coming back empty each time.
I refactored the plugin to get the CSS paths a different way, and now it looks like this:
const addNoscriptCss = () => {
return {
name: 'add-noscript-css',
enforce: 'post',
transformIndexHtml(html, { bundle, chunk }) {
const tags = [];
const cssBundleKeys = Object.keys(bundle).filter(key => key.endsWith('.css'));
cssBundleKeys.forEach(key => {
const cssBundle = bundle[key];
tags.push({
tag: 'noscript',
children: [
{
tag: 'link',
attrs: {
rel: 'stylesheet',
href: `/${cssBundle.fileName}`,
},
},
],
injectTo: 'body',
});
});
return {
html,
tags,
};
},
};
};
Top comments (2)
i found error. how can i solve this ?
What have you tried? :)
A few ideas:
If the error started happening after adding code from this blog post then remember this post is now quite a few years. It was written for Vite 3.1.0 and Vite is currently on 4.4.9.