Critical CSS with Rails and Webpacker - SprocketsLess Part 1
Tags: rails, webpacker, criticalcss, sprocketsless
Published: 2019-09-02 20:09:26

---
title: Critical CSS with Rails and Webpacker - SprocketsLess Part 1
published: true
description: Inline CSS in Rails advanced usage of WEBPACKER for CSS.
cover_image: https://thepracticaldev.s3.amazonaws.com/i/2o1orw46c3l6ksi7b47e.png
tags: rails, Ruby on Rails, Webpacker, Critical CSS, SprocketsLess
---

This is the first of hopefully a series of articles on new advanced usage available when moving all assets to Webpacker. In the first part, we will look into optimizing your CSS size.

We all want fast & reliable web pages. When doing a [Page Speed Audit](https://developers.google.com/speed/pagespeed/insights/?hl=en) what often comes up as a recommendation is **critical CSS**. Critical CSS and especially above the fold critical CSS is the ability to inline (in the HTML) the minimal CSS required to render the top of your page (above the fold). I have for some time looked into an easy solution to achieve this in a Rails app but I was never really successful at it.

One of the great thing with Webpack (ie Webpacker in Rails) is all of the echo systems around it. While the JS side in Rails is largely documented they are also lots of tools available for CSS & images.

A few months ago I discovered a great video from [GoRails](https://gorails.com/) for using [PurgeCss in a Rails application](https://gorails.com/episodes/purgecss).

## Understanding PurgeCSS

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/4anqgftf0j9v8zih0mh5.png)

The global concept of PurgeCSS is that on one side you feed PurgeCSS with all of your files where you would have some CSS class used (usually `.html`, `html.erb`, `.js`). PurgesCSS create a list of all token that could be CSS selectors. 

On the other side Webpacker create a CSS bundle using the `mini-css-extract-plugin`. PurgeCSS extract a list of tokens

The result is the intersection of those two lists of tokens.

## Multiple Pack with Multiple Rules

With Webpacker it is easy to have multiple packs. You just need to create a new `some-pack.js` file in `app/javascript/packs` directory.

**The global idea of what we are going to do is:**
* Define a second pack `critical.js` with only some CSS import in it.
* Split our PurgesCss process in PostCss to apply much stricter rules for `critical.css`.
* Inline our Critical CSS in the HTML as `Dev.to` [is doing by the way](https://github.com/thepracticaldev/dev.to/blob/master/app/views/layouts/_styles.html.erb).
* Lazy load our main `application.css`.

#### Our critical.js entrypoint

Given an **application.js** that would look something like this:

```js
// app/javascript/packs/application.js

require("@rails/ujs").start();
require("local-time").start();
require("turbolinks").start();

window.Rails = Rails;

// import CSS
import "stylesheets/application.scss";

// import Stimulus controllers
import "controllers/index";

// import vendor JS
import "bootstrap";
``` 

Our main entry point, **import** our main **application.scss** that usually look something like that:

```scss
// app/javascript/stylesheets/application.scss

// Fonts
@import "config/fonts";

// Graphical variables
@import "config/colors";

// Vendor
@import "~bootstrap/scss/functions";
@import "config/bootstrap_variables";
@import "~bootstrap/scss/bootstrap";

$fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
@import "~@fortawesome/fontawesome-free/scss/fontawesome";
@import "~@fortawesome/fontawesome-free/scss/brands";

// Components
@import "components/index";

// layouts
@import "layouts/sticky-footer";
```

We can create a very basic `critical.js`, the only thing it will do is to import a new critical.scss stylesheet.

```js 
// app/javascript/packs/critical.js
import "stylesheets/critical.scss";
```


In our **`critical.scss`** file we can start to be a little more selective as what we put inside to help PurgeCSS make a better job. (it does make a small difference)

```scss 
// colors
@import "config/colors";

// vendor
@import "~bootstrap/scss/functions";
@import "config/bootstrap_variables";
@import "config/bootstrap_critical"; // pick only the Bootstrap module you need

// Components
@import "components/banner"; //just pick the components you need for the homepage
```

## PostCSS / PurgeCSS configuration

**Then this is the important part. We need to tell PurgeCSS to apply different rules per files. Luckily we have a context full of information in PostCSS.**

So we can pass our information context to the environment:

```js
module.exports = ctx => environment(ctx);
```

Add a context variable to our envrionment 

```js
const environment = ctx => ({
  plugins: [
    require("postcss-import"),
    require("postcss-flexbugs-fixes"),
    require("postcss-preset-env")({
      autoprefixer: {
        flexbox: "no-2009"
      },
      stage: 3
    }),
    purgeCss(ctx)
  ]
});
```

Call our PurgeCss plugin with this context

```js 
const purgeCss = ({ file }) => {
  return require("@fullhuman/postcss-purgecss")({
    content: htmlFilePatterns(file.basename),
    defaultExtractor: content => content.match(/[A-Za-z0-9-_:/]+/g) || []
  });
};
```

And now that we have the filename in PurgeCss, we can specify different rules for each file. For my critical CSS, I specify only pages related to the home page and the usual set of patterns for all other files.


```js 
const htmlFilePatterns = filename => {
  switch (filename) {
    case "critical.scss":
      return [
        "./app/views/pages/index.html.erb",
        "./app/views/shared/_navbar.html.erb",
        "./app/views/layouts/application.html.erb"
      ];
    default:
      return [
        "./app/**/*.html.erb",
        "./config/initializers/simple_form_bootstrap.rb",
        "./app/helpers/**/*.rb",
        "./app/javascript/**/*.js"
      ];
  }
};
```

So in full, it looks like that 

```js 
// postcss.config.js

const environment = ctx => ({
  plugins: [
    require("postcss-import"),
    require("postcss-flexbugs-fixes"),
    require("postcss-preset-env")({
      autoprefixer: {
        flexbox: "no-2009"
      },
      stage: 3
    }),
    purgeCss(ctx)
  ]
});

const purgeCss = ({ file }) => {
  return require("@fullhuman/postcss-purgecss")({
    content: htmlFilePatterns(file.basename),
    defaultExtractor: content => content.match(/[A-Za-z0-9-_:/]+/g) || [],
  });
};

const htmlFilePatterns = filename => {
  switch (filename) {
    case "critical.scss":
      return [
        "./app/views/pages/index.html.erb",
        "./app/views/shared/_navbar.html.erb",
        "./app/views/layouts/application.html.erb"
      ];
    default:
      return [
        "./app/**/*.html.erb",
        "./config/initializers/simple_form_bootstrap.rb",
        "./app/helpers/**/*.rb",
        "./app/javascript/**/*.js"
      ];
  }
};

module.exports = ctx => environment(ctx);
```

## Results

In a small test I did I had those results
* Initial bundle size of 32kb
* With purge CSS this dropped to 9kb
* **My critical.css only 3kb!**

BAMMMMM 🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉 

## Inline CSS from Webpacker

I did scratch my head a bit to inline my CSS file in the HTML. Thanks to [Stackoverflow](https://stackoverflow.com/questions/57663867/how-to-inline-css-in-rails-header-with-webpacker-assets) I could get some help here

```erb
<% if current_page?(root_path) %>
  <!-- Inline the critical CSS -->
  <style>
    <%= File.read(File.join(Rails.root, 'public', Webpacker.manifest.lookup('critical.css'))).html_safe %>
  </style>

  <!-- Lazy load the rest with loadCSS -->
  <link rel="preload" href="<%= Webpacker.manifest.lookup('application.css') %>" as="style" onload="this.rel='stylesheet'">
  <noscript><link rel="stylesheet" href="<%= Webpacker.manifest.lookup('application.css') %>"></noscript>
<% else %>
  <%= stylesheet_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
<% end %>
```

## Et voila!!!!

Demo: https://sprockets-less-rails6.herokuapp.com
Source code: https://github.com/adrienpoly/sprockets-less-rails6