In our ongoing quest to make yoast.com lightning fast, we've reached a bottleneck when it comes to optimising our CSS. The ways in which WordPress themes manage, handle and output stylesheets just aren't good enough. Technology has changed, the web has evolved, and the tools we have are out of date. We think we can come up with something better.
The importance of speed
At Yoast, we understand that improving the speed of your site can have a big impact on your SEO. More importantly, we understand that a fast, responsive website provides a better user experience - which impacts your reputation, and the likelihood of your visitors to engage with your content. Speed matters. We've written about this a lot.
That's why we've become a bit obsessed with optimising the yoast.com ecosystem. We're trying to make our own website as fast as possible, and we're continually working to shave bytes and milliseconds from every page, script, style and more.
Over recent months, we've been working to improve all aspects of our site. We've refined our hosting setup, managed our plugins and scripts, refactored our custom font handling, and much more.
We've made the site really fast. But we want to go faster. And increasingly, we're finding that the slowest part of our site is the CSS we use to display our theme - in particular, how it's structured and loaded.
Our current approach
One of the slowest things our site does is load lots of CSS on each page, in a big
style.min.css file. This takes a lot of time for your browser to download, and then to parse and render.
Even though we're minifying that file, serving it via a CDN, and instructing the browser to 'preload' it, it's still relatively big. And, even though we've applied all of the performance tricks in the book, there's no escaping that your browser stalls whilst it has to process all of those bytes.
What's even worse is that, on most pages, we only use a tiny fraction of that CSS. You can see from the image below that, on the homepage, we load 117kb of CSS - but over 90% of that isn't used or needed on the page. That's a lot of wasted time and bandwidth.
Even more significantly, the way in which browsers work requires all of the CSS to be downloaded and parsed before anything can happen on the screen. So those large, chunky files will hold up everything else on the page - and will often continue to do so, regardless of how much 'optimisation' is done elsewhere.
Put simply, if we can load less CSS, we reduce the bytes transferred, and the processing/rendering time the browser spends painting the screen. Everything gets faster.
But, even with so much waste, this approach comes with some benefits. Though the initial download is slow, the file gets cached by the browser when it's done. That means that if you navigate to other pages, you don't have to get the file again - so it loads instantly. And because we've already loaded all of the CSS you'll ever need, the rest of your experience is really fast. But that initial load is still slow, and inefficient.
This kind of setup is still incredibly common on the web, because this approach used to be best practice. That's not the case any more.
A history lesson on CSS best practice
Once upon a time, browsing the internet was much slower. This wasn't just due to limited bandwidth and slower connections, but, the also way in which your computer negotiates with the servers you request pages and content from.
The web was, originally, designed primarily for displaying text documents, and simple pages. The underlying technologies didn't anticipate the media-rich, complex environment the web would become. And they hadn't changed much since - until recently.
One of the main things that legacy technology failed to anticipate was how many things a modern webpage would try and make a user download. A complex page with many scripts, styles, resources, images, frames and external assets would put those assets into a queue, grind to a halt. That's because browsers only really let users download a few things at once, and this was a hard-coded restriction into the way that the underlying systems worked.
That meant that best practice was to include fewer files. Sending 6 small, individual script files, for example, could take a lot longer than sending one big script - even if that meant using more bandwidth, and the browser taking more time to process that file. This kind of combination of assets, and minification, became best practice.
WordPress is oldschool
If we want WordPress to be fast, it's not enough to rely on theme and plugin developers to own and embrace this transition. Theme developers have the tools to make fast websites, today, but are still making slow sites, using outdated techniques. What they lack isn't in tooling, but rather, is in education, experience, or design/development patterns.
To speed up our platform, we need to define frameworks and constraints to enable people to get it right, as close to 'out of the box' as possible. We have to design a better approach to CSS authoring and serving, and bake it deep into WordPress.
The web has changed
Now, the web has matured. Technologies, frameworks and best practice have evolved. Performance optimisation is no longer an art, but a science.
Of the changes we've seen, I can see five key factors which, when combined, provide us with the tools to explore better approaches to managing CSS in WordPress, and, to better serve the needs and browsing behaviours of our users.
- As individuals, our browsing habits have shifted to include huge amounts of mobile consumption - where data is precious, and connectivity is often limited. That means that optimising for speed requires us to focus on efficiency and flexibility of serving. Every byte and every millisecond counts.
- There's been a broad adoption of HTTP/2, which allows us to send multiple files in parallel. Now we can send lots of small files, in specific 'bundles' for the page/view in question - allowing us to send individual shared/cacheable and page-specific files.
- CSS pre/post-processors (like LESS/SASS, and PostCSS) are increasingly part of most authoring workflows. These let us abstract and better manage the CSS we write. Now we can manage those 'bundles' without enormous overhead and micromanagement.
- There's strong browser support for CSS media queries, which allow us to target specific viewports and device capabilities. Now we can build mobile-first, and only 'unlock' the additional CSS required for larger viewports on appropriate devices.
More broadly, we also have some really solid standards and definitions for what 'best practice' looks like, in particular, the approached outlined and championed by Google, Illya Grigorik and team.
Together, these trends and tools enable us to re-write the rule book. We have the flexibility and capability to redefine how CSS is handled in WordPress, and to define a performant, flexible, universal approach pattern for all themes, plugins and sites.
Our objective - a better approach
Our goal, then, is to create a general approach to CSS authoring and loading, which makes sense for any and every site, and which is as future-proof, scalable and flexible as it can possibly be.
In particular, we want to:
- Only include the CSS necessary for the specific scenario the user has loaded, but also;
- Ensure that shared and global stylesheets are cached by the browser and re-used between requests.
- Limit the total requests to a 'sensible' number, as even over HTTP/2, each individual request comes with an overhead. There's a balance to be struck where loading many small, ultra-specific files might be slower than fewer, slightly more generic files.
- Future-proof against and support current and emerging patterns, as much as possible (i.e., critical path management, inlining, HTTP/2 server push).
- Make it as easy as possible to style/CSS authors/designers to contribute to and manage a theme's styling without knowing or touching the PHP.
We should also consider how we:
- Minimise the overhead on authoring and managing CSS (in LESS/SASS).
- Stick to established WP patterns where possible.
- Ensure backwards-compatibility, so that the new system can gracefully replace the old - in particular;
- Respect the current wp_enqueue_style() function, which is perfectly suitable for the act of 'enqueuing', and critically, used by themes/plugins to transform output, and;
- Avoid 'hacks' like CSS tree-shaking, which paper over bad foundations, and make it hard to achieve some of the benefits which a stronger foundation delivers.
- Be platform-agnostic. Whilst we're primarily focused on WordPress, we should try and define an approach which works, conceptually, on/for any platform.
Let's create a CSS template hierarchy
Conveniently, WordPress already has a system which ensures that the right assets are loaded for a user for any given request, and any given state. The PHP templating system, and WordPress template hierarchy, is designed to do precisely this, and works well.
Conceptually, we can do something similar with stylesheets. With some adaptation, we can define a hierarchy for CSS requirements, and create a system which loads the right assets based on the requested template/state.
The way in which we need to manage and load CSS is a little more complex, however, as we need to manage specificity and inheritance - which means loading multiple files, rather than just the most specific match.
We also want to maintain a component-first approach, where CSS mostly lives in / is associated with specific components and modular, re-usable blocks. We want to be able to 'bundle' those components to create the backbone of the specific template/view/page files we'll be conditionally loading.
These are some challenging requirements! But, if we're clever, we might be able to create a CSS system which marries elegantly to the PHP template system.
Here's the approach
- We should load structural CSS and utility classes on all requests, as separate CSS files (
- We should define and manage all of the components which the site uses separately from the views/pages they're loading in/on, and, whenever possible, adopt a 'component-based' approach to authoring.
- For each request, we should determine the template type (e.g.,
is_archive()) being returned / selected by the query.
- We should detect the most precise existing match from the template hierarchy, and enqueue the appropriate stylesheet.E.g., if the request is for a
Postwith an ID of 12345, we should load
single-post-12345.css(if it exists).
- We should enqueue all of the less specific files which are matched, if they exist, in order of least-to-most specific, using a dependency chain to manage ordering.E.g., a request to a specific recipe (a custom post type) should load
- In our example, the specificity of styling increases as we progress down that chain. E.g.,
singular.csscontains generic CSS, like structure, typography and colours - attributes and components shared by all 'single' pages/posts, like a sidebar.
single.csscontains components shared only by all posts, like a comments form.
single-recipe.csscontains CSS used by all recipe posts, like a recipe card.
single-recipe-12345.csscontains only ~10 lines of very specific CSS, intended for only that post.
It's assumed that this 'bundling' of stylesheets, where, e.g.,
single.css is a composite of components like sidebars and cards, is achieved via SCSS/LESS, but a simpler/partial implementation could feasibly work with 'raw' CSS.
In most scenarios, we recognise that websites won't vary greatly between 'views'. A 'post' will likely be very similar to a 'page', and, individual post types won't vary greatly. However, this structure provides us with the framework to write component-based CSS which focuses the composition on tackling the most common scenarios, and seperates out the most specific / rarer ones.
It does so in a way which removes duplication, and reduces the management of view-types to little more than selecting which components to bundle.
My site might only require a small number of generic, 'high level' styles (like
single.css) in addition to global styles (like
footer.css), and a few ultra-specific stylesheets for certain pages/posts. Those stylesheets are mostly comprised of components I've defined and manage centrally, and the whole thing becomes a joy to manage.
Some of the hard work has already done
Personally, I'm a huge fan of the Query Monitor plugin for WordPress. It's a great tool for assessing and diagnosing performance issues in themes and plugins.
Helpfully for us, it also contains a series of functions which assesses the requested template, and, output a priority-ordered list of all the templates (and partials) which are loaded on the request. See https://github.com/johnbillion/query-monitor/blob/master/collectors/theme.php, (
action_template_redirect() in particular).
This feels like a great shortcut and a starting point for us - we can learn a lot from the work which has already been done.
In our example, we explored how a request to a post with an ID of 12345 should load
single-post-12345.css, but only if it exists.
For performance reasons, we want to avoid relying on 'sniffing' to see if the file exists at the point when it's requested. Given that we're looking for multiple files and potentially traversing folders, this could get slow, quickly.
We also don't want to place hard definitions on where stylesheets should live in a theme; that's a strong architectural constraint and has impact/implications beyond the scope of this challenge (that said, consideration should be given to a sensible file/folder hierarchy).
Instead, we're proposing a method for registering all of the stylesheets available in a theme, so that they can be enqueued on demand without needing to go through a detection process.
I suggest we the ability for themes to register an array of styles (which should, if/when possible, be auto-built, e.g., via PostCSS), which looks something like:
$styles = array( 'front-page' => array( 'file' => '/example/path/front-page.css', 'dependencies' => array() ), 'single-1234' => array( 'file' => '/example/path/single-1234.css', 'dependencies' => array() ), 'archive-cats' => array( 'file' => '/other-example/other-path/cats-archive.css', 'dependencies' => array() ), 'logged-in' => array( 'file' => '/example/conditions/logged-in.css', 'dependencies' => array(), 'function' => 'user_is_logged_in', 'critical' => true ), [...] );
Then rather than sniffing for files, we can check our pre-registered styles to see if we have the CSS available ("This looks like a request for
Post 1234; do we have a
single-1234 entry in the styles array?"). Positive matches pass the file, and any additional properties (e.g.,
It's assumed that this array/function is managed manually for small/simple sites, or built automatically via PostCSS processes in more sophisticated workflows.
I've anticipated a requirement for
function, so that future iterations might also register CSS for conditionals which might impact the page's CSS requirements, but which don't map to the template hierarchy (e.g., user is logged in). When specified, the file should be enqueued if the function returns
I've also anticipated a requirement for specifying whether a stylesheet should be considered part of the
critical path; this might feasibly alter how a theme goes about
enqueuing such an asset (e.g., methods like inlining, rewriting the hostname, preloading, HTTP/2 server push, etc), and should map neatly to any existing filters/hooks which intercept
To maximise performance, not only do we want to load only the specific CSS required for the current page/view/state, but also to load only the CSS required for the current viewport size or device capabilities.
To achieve this, we want to take advantage of the fact that
wp_enqueue_style supports specify a
media property, (e.g.,
media="screen and (min-width:500px)"), which dictates under what conditions a stylesheet should be loaded.
However, with our workflow and current model, using this approach presents a challenge:
- In terms of authoring and management, we want to define the media queries for, e.g., a
card.scss. It's impractical to split media queries into seperate source files, as they're often intertwined with nested classes, mixins, etc.
card.scss, along with other components, is included in
single.scssand therefore compiled into, e.g.,
- In this world,
single.cssis now bloated with un-used media queries, for users on all screen sizes.
So we need a solution - we need to manage our media queries 'centrally', but split them out in production.
To achieve this, I suggest we use a PostCSS package like https://www.npmjs.com/package/postcss-extract-media-query. This, or similar/alternate approaches, can extract media query code from compiled CSS files (e.g.,
single.css), and splinter them into viewport-specific variants.
In this example, that gives us
single.css as a mobile-first stylesheet, but also produces
single-lg.css, etc (depending on which queries exist / are used in the 'base' version).
This also provides future support for other media scenarios, like
I propose that, to register these variations, we add an optional
media property to each of the styles we register, structured as follows:
'media' => array( array( 'query' => 'only screen and (min-width: 600px)', 'file' => /example/path/front-page_md.css' ), array( 'query' => 'only screen and (min-width: 968px)', 'file' => /example/path/front-page_lg.css' ) );
Now our enqueue methods can output each of these variants, and let the browser do the heavy lifting on deciding what to ship to the user.
The world is a complex, messy place, and nothing exists in isolation - especially in the WordPress world. Here are some headaches I'm anticipating us needing to solve.
"We can already enqueue assets selectively based on template"
This is absolutely true - what I'm describing could be achieved today, by any conscientious theme developer, without the need for a new framework or approach.
What's missing, however, is a generally accepted standard for 'the right way' to approach the selective/conditional enqueing of resources based on the template/view, which anticipates the complexity of the real world, and the differences between sites with wildy varying structures, approaches and needs. People can do this manually, but don't know how, or don't know that they should.
By the same logic, people could hard-code their websites in raw PHP. They don't - they use content management systems which come with handy abstractions and wrappers.
More significantly, perhaps, this approach is designed to allow non-developers (e.g., CSS authors) to control the front-end - even to make radical structural changes to the CSS template hierarchy - without ever opening a PHP file. If the styles array can be auto-built by post-processing systems, designers can add to/remove from/edit bits of the hierarchy without needing the kind of (relatively) deep understanding they'd need to enqueue stylesheets in today's ecosystem.
This post is the beginning of us exploring what the need, and documentation, for those kinds of abstractions might look like in the case of supporting performant CSS in WordPress. Managing the complexities of cache optimisation, front-end vs back-end processing speeds, and 'future-proofing' against the need for functionality like media queries, inlining and JS-based loading is hard to juggle. We want to abstract that away, and give people a framework which they can use to get it right, out of the box.
Bootstrap, and other generic frameworks
Many sites build their stylesheets 'on top of' third party frameworks like Bootstrap. These come with their own design patterns, but often ship as a single, monolithic file which stores all of their 'reset' code and utility classes. Most of the time, that's where we see the biggest chunks of unutilised CSS on websites.
For now, that means that our system is only really suitable for people who're developing their own solutions, or heavily modifying (and paring back) the frameworks they use.
In the future, it'd be great to see these kinds of frameworks and libraries do more to encourage their users to modularise their offerings. I'd like to see more people disabling the components they don't need to use globally, or, improve how/where/when they're imported.
Gutenberg, blocks and in-content styles
Historically, many plugins have supported shortcodes which resulted in additional styles being loaded into a page. That's typically been handled inconsistently, with a mix of inline styles, output buffering, and enqueing 'surprise' styles in the footer (which often cause repaint/reflow issues as the browser parses, incorporates and updates those styles).
All of these approaches create different performance issues, and, there's never been a 'good' way of handling these kinds of requirements.
Now, Gutenberg blocks are proliferating this pattern. Blocks often ship with their own CSS, and, it's unclear where this 'lives'. To incorporate blocks into our design pattern, pages need to know about what's in their content, before the <head> is output. That's a performance and architecture challenge.
Given the scope of this challenge, I'm conveniently ignoring Gutenberg blocks for now - but at some point, we'll need better answers to how blocks store, manage and declare style dependencies.
It may be that the up-and-coming browser behaviour described here by Jake Archibald could help, where it becomes easier and safer to load newly discovered stylesheets inline, as they're discovered. In that case, blocks (and other in-content elements) could output their own stylesheets, without disrupting rendering or making a mess.
At this stage, it's still unclear how different browsers will handle those scenarios, and how the nuances of things like stylesheet duplication (e.g., if you have two of the same block on a page), and load jank (where styles conflict with / overwrite existing styles) will be handled.
If the spec is robust, this might be a great addition to our approach - where our templating system handles everything around the content, and individual content blocks manage and output their own styles in the flow, as they're needed. We'll wait and see!
As you might have guessed, we're well-underway on exploring and implementing this approach on yoast.com. We'll no doubt encounter challenges as we go, and we'll keep you updated.
More broadly, we think that this approach is a much better design pattern for WordPress sites, themes and plugins - and if it goes well for us, we'll be keen to explore how we might make a case for broader adoption.
We'd love to hear your thoughts on this direction, potential improvements, issues, and your own experiences. Let's speed up WordPress!