Tailored CSS

The site should be fast

We value site speed a lot at SpanishDict.com. As we've made growing Latin
American traffic a priority, this has become even more important. Connection
speeds tend to be slower than in the USA, so optimizing for them is always on
our mind.

State of CSS at SpanishDict

One of the things that can slow down how a site feels (it certainly did for us)
is when CSS blocks rendering.
When you add a link tag, the browser has to first load and parse the entire
file, because it doesn't know how to render the content without it.

For desktop, we build one large file that contains all the styles for our
read-only pages (pages
like
translations,
conjugations,
and grammar articles).
This was a
conscious decision, because having only one file meant the browser only had to
make one request to get styles, and that file could get cached over multiple
pageviews.

Our desktop css file comes out to about 40kb after being minified and gzipped.
That's pretty large for something that blocks rendering.

We picked up an approach known as
critical CSS,
in which you inline all the styles required to render the above the fold content
of the page and then you load the rest of the styles asynchronously. Doing so
lets that content load much faster, so users feel the site is faster 🙌

How do we build it?

For about a year, we took an mvp approach to this. We built a separate
critical.css file and we would decide what styles could go above the fold
and which couldn't.

This wasn't easy. It meant that every time we added new styles, we had to figure
out which ones could possibly end up styling above the fold content. We were
very liberal with we included in critical CSS (because we reeeally didn't
want users to
see FOUC 😱).That
meant that our critical CSS file kept growing and growing.

What is tailored CSS?

To combat this, we built a way to keep all of our styles in a single CSS file
and through an automated process generate a critical CSS file. We call this
tailored CSS.

Our strategy leverages a tool
called Penthouse. It's a library that
receives a CSS file and url, determines which styles are needed by the above the
fold content, and outputs just those styles.

Multiple sizes and pages

We realized quickly that we couldn't just take the results of a single
Penthouse run and give it to users.

The types of pages that we serve 90% of our traffic can be split up into 7
different categories. Each has styles that aren't used at all by the others, so
it made sense to generate a separate tailored CSS file for each.

Our desktop site uses media queries to render slightly different styles for
three ranges of screen widths. Because we can't know ahead of time which ones a
user is going to need, our tailored CSS needs to include the styles for all of
them.

Even within a page category, a page can load in many different states. Different
styles will be rendered above the fold as a result. For example, one of our
grammar articles might have a custom table above the fold, but another might
have a dialogue box.

Our solution is to run Penthouse for each state, for each screen size, for each
page category. The styles for each page category are then concatenated together
and run through a series of PostCSS plugins that
deduplicate styles, organize media queries, and minify the files.

Increased complexity in development

Though Penthouse is an amazing tool, we haven't found using it to be completely
reliable. To be fair with the team behind it, most of that difficulty is
probably the result of complexities in our own codebase.

However, debugging when and why certain styles do or don't get included has not
been an easy task. Because of that, we decided as a team that tailored CSS
should be built at development time instead of at deploy time. Doing so means
changes are reviewed as part of our normal pull request process.

All in all, we run penthouse 43 times. The process takes around 5-6 minutes, and
it affects our ability to iterate quickly.

Tailored CSS also needs to be generated from all the most up to date styles.
That means that before generating it, we have to pull and merge our master
branch. When there are multiple pull requests open that change styles, we often
end up having to the process multiple times before a PR can be merged.

Generating production code based on what the site looks like in development
introduces a dependency between the state of our development environment and
production. To generate tailored CSS accurately, we now need to have running all the
services that make up our site and recent database snapshot.

Moving forward

Tailored CSS definitely got the job done, but it also generated a huge amount of
technical debt. We are definitely open to other alternatives.

One approach that's on the horizon is reorganizing our UI code into components.
It's hard to figure out which styles are above the fold, but much easier to
reason through which components could be rendered above the fold. With that, we
could manually list the components that should be considered critical and build
a CSS file from that. However, like most refactorings, reorganizing styles would
require a significant time investment with no immediate end-user value.

Have you tried building anything like tailored CSS? From what I can tell, it's
widely recommended, but not widely done. If so, let me know!

Tailored CSS script

If you're interested in how we build our tailored css, an abridged example is
available
here.