DEV Community

Cover image for Writing a Vite plugin to add lazyloaded critical CSS fallbacks for users with JavaScript disabled or unavailable
Phil Wolstenholme
Phil Wolstenholme

Posted on • Updated on

Writing a Vite plugin to add lazyloaded critical CSS fallbacks for users with JavaScript disabled or unavailable

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

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

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

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

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

Latest comments (2)

Collapse
 
ali_butt profile image
ali butt

i found error. how can i solve this ?
Image description

Collapse
 
philw_ profile image
Phil Wolstenholme

What have you tried? :)

A few ideas:

  • Google the error messages you're seeing
  • Make sure you've installed all the dependencies you need
  • Check for updates to your dependencies
  • Review what has changed in your codebase between when this last worked and when it didn't

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.