Ads ●●●

Improving TailwindCSS critical rendering path in Next.js web applications

Harrison Ifeanyichukwu
By Harrison Ifeanyichukwu

Sr Software Engineer at Delivery Hero, Investor, Sports analyst and Tech Evangelist

TailwindCSS and Next.js

The use of Non-CSS-in-JS libraries such as TailwindCSS with Next.Js helps improve performance. We can further improve the performance of Non-CSS in JS libraries by optimizing the generated, production-ready CSS bundle for Web Browser's critical rendering path.

Critical Rendering Path

When a web browser downloads HTML files, it goes through parts to build up the DOM tree and render the layout into pixels. The critical rendering path is the Browser's series of steps to convert HTML, CSS, and JavaScript into rendered pixels on the screen.

Improving the critical rendering path improves performance and leads to an improved First Contentful Paint. The critical rendering path includes the document object model (DOM), the CSS object model (CSSOM), the rendering tree and the layout.

Asset files or web resource files that affect the Critical Rendering Path are known as critical asset files. The key to improving the Critical Rendering Path is to make critical asset files available as early as possible to improve the time to first render.

Moreover, any file loading that does not affect the Critical Rendering Path should be deferred until the DOM is completely rendered and becomes interactive or after the page load event has fired.

<html>
  <head>
    <!--non css in js library are not optimized for critical css loading. generated bundle should be inlined
      and not linked using the link tag below-->
    <link rel="stylesheet" href="bundle.css" />
  </head>
  <body>
    <!--content goes here-->
  </body>
</html>

The Problem Statement

When Non-CSS in JS libraries such as TailwindCSS is used in Next.js projects, the generated CSS bundle is not optimized for the critical rendering path. They are not inlined in the document's head, as shown in the HTML snippet above.

Instead, they are connected to the HTML document via the Link tag. This requires another network trip that delays the time to first render.

This article discusses how to optimize TailwindCSS for critical rendering path by inlining the production generated CSS in the document's head.

Before showing how to inline TailwindCSS in Next.js applications, let's discuss a little bit about CSS in JS libraries, a counterpart to TailwindCSS.

CSS in JS Libraries

As web performance became a more and more pressing issue following Google's core web vitals integration with search ranking, the existence, usage and runtime of many web frameworks and libraries came into scrutiny.

CSS in JS libraries has been on the rise for the past few years, as developers abandoned the old and traditional way of styling web applications favouring the component-based model of styling applications through JavaScript.

CSS in JS promises unique class name generation by hashing of CSS content. Ensuring unique class names in a big web application using the traditional CSS styling was a pain in the ass, very buggy and hard to maintain until the rise of CSS in JS libraries.

//example styling in styled-components, a css in js library
import styled from 'styled-components';
const StyledButton = styled.a`
  display: inline-block;
  border-radius: 3px;
  padding: 0.5rem 0;
  width: 11rem;
  background: transparent;
  color: white;
  border: 2px solid white;

  ${props => props.primary && css`
    background: white;
    color: black;
  `}
`
export const Button = ({children, ...props}) => {
  return (
    <Button {...props}>{children}</Button>
  );
};

CSS in JS is opinionated, as it introduced the need to learn JavaScript to write CSS. Not many liked this, but it helped write component-based styled applications and made it easy to maintain and scale large applications.

Moreover, CSS generated by CSS in JS libraries during server-side rendering is optimized and inlined within the HTML head section. Hence, it is by default optimized for browsers' critical rendering path.

The problem with CSS-in-JS Libraries

With the advantages come the disadvantages. CSS in JS libraries come with huge javascript footprints. It performs a lot of computation and string hashing on the client-side, increasing the total blocking time and the Time to Interactive.

The problem becomes exacerbated when used with statically generated React applications such as in Gatsby and Next.js because of the need for rehydration. During rehydration, the application is forced to recompute CSS content hashes to ensure content integrity.

How to inline TailwindCSS in Next.js

Having said much about the pitfalls of using CSS in JS libraries, let's talk about TailwindCSS, a non CSS in JS library that solves the performance challenges associated with the former.

Non-CSS in JS libraries do not rely on JavaScript on the client. Hence, they have zero JavaScript footprint. Non-CSS in JS libraries work by generating a single CSS bundle that can be included in the application.

To improve TailwindCSS's rendering path in Next.Js, the generated production CSS file should be inlined in the document's head instead of linked to by the HTML link element. This avoids network roundtrip.

To achieve this, there is a need to set up and run PostCSS build separately before Next.js build. This can be achieved using a prebuild yarn or npm hook.

Read here to learn how to set up TailwindCSS in Next.js web applications.

Follow the steps below to inline TailwindCSS output in the Next.js document head.

1. Install raw-loader & add a prebuild script

The key is running the Postcss build separately just before the Next.js build script. This will execute tailwind and generate the production-ready CSS file, which we output to src/styles/tailwind-ssr.css.

Install raw-loader, which will be used to import and inline the generated CSS output file.

yarn add --dev raw-loader

Add a prebuild script that will run a separate PostCSS build which will generate the production ready tailwind css file.

"scripts": {
  "postcss": "NODE_ENV=production postcss src/styles/index.css -o src/styles/tailwind-ssr.css",
  "prebuild": "yarn postcss",
  "build": "next build"
}

Prebuild scripts are automatically executed by yarn or npm before the build script. Hence, the src/styles/tailwind-ssr.css output will be available before the Next.js build kicks off.

2. Inline generated CSS in the Next.js _document.jsx file

The next step is to inline the generated CSS bundle in the Next.js application's _document file. If you do not have a custom Next.js document file created, create one and continue below.

Import the generated CSS file using raw-loader and inline it as shown below:

import Document from 'next/document';
// @ts-ignore
import bundleCss from '!raw-loader!../styles/tailwindSSR.css';

export default class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps: any = await Document.getInitialProps(ctx);

    return {
      ...initialProps,
      styles: [
        ...initialProps.styles,

        process.env.NODE_ENV === 'production' ? (
          <style
            key='custom'
            dangerouslySetInnerHTML={{
              __html: bundleCss,
            }}
          />
        ) : (
          <></>
        ),
      ],
    };
  }
}

The CSS is inlined only in production mode. Hence, the development environment stays the same with no changes.

3. Extend and Tweak Next.js Head

Since we now inline the CSS file, Next.js should not link to the Webpack generated CSS bundle nor preloaded the CSS bundle. Hence, the last step is to prevent Next.js from linking to Webpack's generated CSS bundle.

To achieve this, we will extend and tweak the Next.js document's Head, as shown below.

// file pages/_document.jsx
import Document, { Head as NextHead, Html, Main, NextScript } from 'next/document';
import bundleCss from '!raw-loader!../styles/tailwindSSR.css';

/**
 * we extend and tweak next js HEAD here
 */
class Head extends NextHead {
  getCssLinks(files) {
    if (process.env.NODE_ENV !== 'production') {
      return super.getCssLinks(files);
    }

    // do not return any css files in production
    return [];
  }
}

export default class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps: any = await Document.getInitialProps(ctx);

    return {
      ...initialProps,
      styles: [
        ...initialProps.styles,

        process.env.NODE_ENV === 'production' ? (
          <style
            key='custom'
            dangerouslySetInnerHTML={{
              __html: bundleCss,
            }}
          />
        ) : (
          <></>
        ),
      ],
    };
  }

  render() {
    return (
      <Html>
        <Head /> {/** <------------------- render the tweaked head here */}
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

The code snippet above extends Next document's Head and tweaks its functionality. In production, it makes sure that no Webpack's generated CSS files are included in the head.

Conclusion

It is not enough to use non CSS in JS libraries when 100% performance is needed. The final generated CSS output or bundle should be optimized and inlined with the document's Head to improve core web vital metrics such as First Contentful.