Externalise CSS

By default Gatsby will embed CSS into the <head> of each HTML page. This is not ideal. In this post I take a look at how to move that CSS into an external file and how the contents of that file can be optimised to remove unused CSS.

🚀 TL;DR Show me the code. Look at the 19-external-css branch. This site is deployed here.

Text:HTML Ratio

The text:HTML ratio measures the proportion of (visible) text content on a page relative to the amount of HTML code needed to display it. Ideally one should be targeting a ratio of between 25% and 70%. Why? Although this ratio doesn’t appear to be a factor directly affecting searh engine ranking, it likely affects SEO performance indirectly.

Many SEO tools will calculate the text:HTML ratio as a standard component of their reporting. For the purpose of this post we’ll use the free SiteGuru tool.

Embedded Style

Looking at a previous version of the “What is Gatsby?” page I see that the document contains 32977 characters, of which 27434 characters are associated with the <style> tag in the <head>. That tag includes a load of Tailwind CSS along with a smattering of custom CSS for the site. The HTML file is being dominated (83.2%) by the style directives. The <body> of the document (which is where the page content actually resides) is only 4814 characters, 14.6% of the file size. The SiteGuru tool, which is somewhat more refined in its analysis of the page gives a text:HTML ratio of just 7.6% and comments “that’s a bit low” (2520 characters of text versus 32962 characters of HTML ).

Why would you want to embed the CSS directly into the HTML document? On the one hand this will reduce the number of HTTP requests required to load the page (just request the HTML rather than HTML and CSS separately). And if the volume of embedded CSS is low then this makes complete sense.

However, if there’s a large volume of CSS embedded in each page then this amounts to a lot of extra data being sloshed back and forth between the server and the client on each page load. A more efficient strategy would be to load the CSS separately. Since the CSS will be cached locally on the client it will not need to be loaded with subsequent pages, and this will reduce the bandwidth requirements (in addition to improving the Text:HTML ratio!).

What is SSR?

SSR (Server-Side Rendering) is a fancy term for the process of rendering the content of a web page on the server rather that on the client (which would be the user’s browser). SSR produces a static (versus dynamic) site. For a page generated via SSR all (or at least most) of the work of generating the page happens on the server. The browser only has to display the contents rather than having to run JavaScript code to first generate those contents! This means that the amount of data delivered with a SSR page is larger but that the page will potentially display quicker.

A Gatsby site doesn’t strictly use SSR. Gatsby renders all pages at build time. SSR will typically render a page at the time that it’s requested.

Gatsby does, however, have a SSR API. You can interact with this API via the gatsby-ssr.js file and use it to customise the SSR process. Here are some of the reasons that you might want to tweak this file:

  • customise HTML rendering (this is how we’ll be using it!);
  • injecting scripts and styles;
  • custom rendering of individual pages; and
  • enhance performance (via specific optimisations of the rendered HTML).

Similar changes can be applied in the browser via the gatsby-browser.js file.

💡 The gatsby-ssr.js file is only executed when you build a Gatsby site and won’t be active if you’re running a development server.

The solution is to add some code to gatsby-ssr.js which will find all <style> tags in the document and convert them into <link> tags referencing an external CSS file.

export const onPreRenderHTML = ({ getHeadComponents }) => {
    if (process.env.NODE_ENV !== 'production')
        return

    getHeadComponents().forEach(el => {
        if (el.type === 'style' && el.props['data-href']) {
            el.type = 'link'
            el.props['href'] = el.props['data-href']
            el.props['rel'] = 'stylesheet'
            el.props['type'] = 'text/css'

            delete el.props['data-href']
            delete el.props['dangerouslySetInnerHTML']
        }
    })
}

💡 The conditional at the top of the function ensure that the body of the function is only executed for a production build. You might need to manually set the NODE_ENV environment variable to "production".

After making this change the entire document is reduced to only 5657 characters, of which the <body> still accounts for 4814 characters. This means that a much higher proportion of the file is now associated with actual content. The SiteGuru tool gives a text:HTML ratio of 44.7% with comment “that’s good, nothing to worry about” (2520 characters of text relative to 5642 characters of HTML).

Purge CSS

The gatsby-plugin-purgecss plugin can be used to further trim down the volume of CSS. The plugin uses the PurgeCSS tool to remove unused CSS. CSS frameworks like Tailwind or Bootstrap contain a lot of code, much of which is typically not used on any one site. PurgeCSS will remove the unused CSS so that the CSS delivered to the browser is as lightweight as possible.

There are a host of options for configuring PurgeCSS. We’ll tap into a few of those via gatsby-config.js.

I set the printRejected option to true so that I can see a list of selectors which are removed by PurgeCSS.

Removed Selectors: [
  'hr',
  'abbr:where([title])',
  'h4',
  'h5',
  'h6',
  'b',
  'strong',
  'code',
  'kbd',
  'samp',
  'pre',
  'small',
  'sub',
  'sup',
  'sup',
  'table',
  'button',
  'input',
  'optgroup',
  'select',
  'textarea',
  "[type='button']",
  "[type='reset']",
  "[type='submit']",
  ':-moz-focusring',
  ':-moz-ui-invalid',
  'progress',
  "[type='search']",
  'blockquote',
  'dl',
  'dd',
  'hr',
  'figure',
  'fieldset',
  'legend',
  'ol',
  'menu',
  'dialog',
  'textarea',
  'input::-moz-placeholder',
  ' textarea::-moz-placeholder',
  'input::placeholder',
  'textarea::placeholder',
  '[role="button"]',
  ':disabled',
  'svg',
  'video',
  'canvas',
  'audio',
  'iframe',
  'embed',
  'object',
  '[hidden]',
  'abbr[title]',
  'dfn',
  'h1 > b',
  'hgroup',
  'fieldset',
  'ol',
  'pre',
  'dt',
  'th',
  'ol li',
  'li > ol',
  'blockquote *:last-child',
  'kbd',
  'samp',
  'abbr',
  'acronym',
  'tt',
  'code',
  'pre code',
  'code:before',
  'code:after',
  'tt:before',
  'tt:after',
  'pre code:before',
  'pre code:after',
  'pre tt:before',
  'pre tt:after',
  'ol > li',
  'ol > li > p',
  '.prose code::before',
  '.prose code::after'
]

That equates to a bunch of CSS content which is not being used on the site. How significant is this? The CSS file was reduced from 27340 bytes (CSS file before purging) to 24558 bytes (CSS file after purging), which is a 10% saving.

Conclusion

Moving embedded CSS into an external file and optimising the resulting CSS will make your site more efficient and should have a positive SEO impact.

🚀 TL;DR Show me the code. Look at the 19-external-css branch. This site is deployed here.