Yoav Kadosh
Yoav Kadosh

Reputation: 5155

How to print critical CSS into index.html using Webpack?

I'm working on a large single-page application, with very large CSS & JS files that are generated by Webpack, which causes a flash of unstyled content when the app is initially loaded.

To avoid that, I would like to print the critical part of the CSS into the head of the document, so that the document will not appear unstyled while the browser is processing the rest of the JS & CSS.

This CSS is written in SASS, and is dependant on global app variables, so webpack must process the files to compile them from SASS to CSS before printing it to the document head.

How can this be done with Webpack?

Upvotes: 1

Views: 2247

Answers (2)

stsloth
stsloth

Reputation: 1850

The way I did it is

  1. Separate critical CSS into its own asset with separate loader rule and ExtractTextPlugin instance;
  2. Disable default inject of html-webpack-plugin to inject assets manually in the template;
  3. Non-critical assets are injected via links to files (example). Critical CSS is injected inline using compilation variable (example).

Note. You can put the logic in a function in HtmlWebpackPlugin options. Example is at the end of the answer.

This way, one has pretty much full control of how the assets are included, as in tags, attributes, positions.

There are several plugins that might also help with inlining CSS and JS, such as

but they take away the flexibility of having full control over format, logic, and output, and substitute it with limited config options.


Example config

plugins: [
    //...

    new HtmlWebpackPlugin({
        template: 'path-to-index-template',
        inject: false,

        injectCriticalCss(htmlWebpackPluginStats, compilation) {
            return lodash(htmlWebpackPluginStats.files.chunks)
                .map(chunk => chunk.css)
                .flatten()
                .filter(cssFilename => /^critical\b/.test(cssFilename))
                .map(cssFilename => `<style>${
                    compilation.assets[cssFilename].source()
                }</style>`)
                .join('\n');
        },

        injectNonCriticalCss(htmlWebpackPluginStats) {
            return lodash(htmlWebpackPluginStats.files.chunks)
                .map(chunk => chunk.css)
                .flatten()
                .filter(cssFilename => !/^critical\b/.test(cssFilename))
                .map(cssFilename => `<link rel="preload" as="style" href="${
                    cssFilename
                }" onload="this.rel='stylesheet'"/>`)
                .join('\n');
        },

        //...
    }),

    //...
]

and then in the template

<html>
<head>

    <!-- ... -->

    <%= htmlWebpackPlugin.options.injectCriticalCss(htmlWebpackPlugin, compilation) %>

</head>
<body>

    <!-- ... -->

    <%= htmlWebpackPlugin.options.injectNonCriticalCss(htmlWebpackPlugin) %>

</body>
</html>

Upvotes: 2

wrufesh
wrufesh

Reputation: 1406

You need use ExtractTextPlugin in order to extract it from js file and write to a single file so that you can link it to head of html.

See how it is used with sass-loader.

const extractSass = new ExtractTextPlugin({
  filename: '[name].css',
})

module: {
    rules: [
      {
        test: /\.(scss)$/,
        use: extractSass.extract({
          use: [{
            loader: 'css-loader', options: {
              sourceMap: true,
              minimize: true
            }
          }, {
            loader: 'sass-loader', options: {
              sourceMap: true,
              minimize: true
            }
          }],
          // use style-loader in development
          fallback: 'style-loader'
        })
      }
    ]
  },
  plugins: [
    extractSass,
  ]

Upvotes: 1

Related Questions