Rikki BochowFront end Developer
Performance improvements with Drupal 8 Libraries
For a long time I’ve been compiling my Sass into a single CSS file - styles.css, but recently, with our component based design/frontend process and Drupal 8’s lovely Library system I’ve been wondering if the single file was still a good idea. Looking at the amount of unused CSS loading into any given page was a little bit painful.
So when a recent project actually had a decent performance budget I took the opportunity to test out the Libraries approach and was quite impressed with the savings!
Now libraries aren’t new to Drupal 8, you can do this in Drupal 7 too, but the developer experience is vastly improved in 8.x. If you’ve taken a peek at the Classy theme’s libraries.yml file you’ve already seen an example of components broken out into their own libraries. It’s the recommend method according to the documentation. But these are normal CSS files, not ones compiled from Sass.
Sass and component based design
For a little history, component based design has been our practice at PreviousNext for a few years now, since John Albin introduced Styleguide-driven development to our process back in 2014. Now it’s embedded in everything, including design. Our Sass was slim, our components where namespaced and it was as DRY as could be. Sass components shared quite a bit between files, so importing one component into another was fairly common, and duplication was managed by our (ever-evolving) front-end automation tools.
Having everything compiling into styles.css meant I had a styles.scss file which imported EVERYTHING else. It was ordered so that shared variables and mixins came first, base styles and layout next followed by the components. Doing this meant that the components had access to everything else that was loaded in that file before they were. The components themselves were “globbed” (loaded in no particular order), so we couldn’t rely on having my nav component loaded before the header, and therefore be available to use in it. But we could import nav into header to access it and avoid duplicating code, or define nav as a dependency of header.
// Header @import 'mixins/image-replace/image-replace' @import 'components/nav/nav' .header {...}
One file per component
In order to utilise libraries, I needed one CSS file per component. I had to really consider how I was using my Sass imports. I now had nav.css and header.css, but header.css included all the styles from nav as well - major duplication! Refactoring this meant moving anything that was shared between components out into a Sass mixin, and importing the mixin instead. Then I could declare the dependency in the themes libraries.yml file instead of inside my Sass.
// Header @import 'init/init' @import 'mixins/image-replace/image-replace' @import 'mixins/nav/nav' .header {...}
And the Style-guide?
I could list every single components css file as a style-guide asset as well but that would be get annoying for future additions. I’m way too likely to forget that step and spend 5 minutes scratching my head about why my new component isn’t styled! So I decided to keep the style.scss (renamed to everything.scss) file just for the style-guide or any 3rd party who wanted to add everything.
The added benefit of this effort is that our style-guides often get exposed to other departments of our clients organisation who use it to build their own websites or apps - in Drupal or something else entirely, so this work around component isolation should help other developers use the codebase too.
Drupal 8 libraries
Once the Sass refactoring is complete, and the individual CSS files are nice and lean we can start defining our libraries in Drupal. As explained in the documentation, this is all done in the themes libraries.yml file and is pretty straight forward. Base styles, layouts and components all have different categories which control the weight they are loaded with. You can group Javascript and CSS for a component into one library (think the pesky carousel) and you can define dependencies with core (like jquery) or even with another library in your theme.
I really like this dependency part, for ensuring everything needed for a component is included (like nav and header) but also just for documentation sake, so it’s really clear to your team members or future maintainers of the code.
nav: version: 1.x css: component: css/components/nav/nav.css: {} header: version: 1.x css: component: css/components/header/header.css: {} js: src/components/header/header.js: {} dependencies: - core/jquery - my_theme/nav
Then you “attach” a components library to the relevant components twig file! Or preprocess it, or call it from a custom module (think field formatters), or override or extend an existing library, or just load it everywhere! There are sooo many ways to attach your library.
/** * @file * Theme override to display the header region. */ {{ attach_library('my_theme/header') }} <header class="header">...</header>
A note on aggregation
The other big part of this, which I didn’t realise straight away from reading the docs is the effects on CSS aggregation. Now that the components CSS and JS is ONLY loading on the page WHEN that component is included (think regions, blocks, fields, paragraphs, panels panes, etc) it’s essentially become a conditional asset.
Drupal aggregation will build a new aggregate CSS and JS file everytime a page is visited that has a different set of components than any previous page. A new file means it isn’t cached and the browser downloads it fresh - even if only 2% of that file is different from one it already has cached! This is obviously a bad thing.
The docs about adding assets to a theme don’t mention this part, but the ones about adding assets to a module do, so I strongly recommend reading both. On the module docs page there’s a section called ‘Disabling Aggregation’ which applies to themes as well. That little preprocess:false flag is the missing piece to the puzzle.
Armed with that information, I then broke my libraries into two sections. Libraries that are loaded on 90% of the pages I put into my ‘Global’ group. Libraries I knew would only be needed on a few pages are in my ‘Conditional’ group (by group I just mean I put a comment above them).
The global group I left the default aggregation settings on and added them to my theme’s info.yml file so they were included on every page. They get aggregated into a single file, which is cached after the first request and doesn’t change (until cleared or expires of course).
To the conditional group I added the preprocess:false flag to and attached to specific components. These libraries aren’t aggregated at all and only load when needed. They do mean a few extra HTTP requests but for this project it was a tradeoff worth taking, and if you have HTTP2 enabled you don’t even need to worry about it. Just make sure your sass output is set to compressed.
# Conditional libraries. accordion: css: component: css/components/accordion/accordion.css: { preprocess: false } js: src/components/accordion/accordion.js: { preprocess: false } dependencies: - core/jquery - core/drupal - core/drupal.collapse alert-banner: version: 1.x css: component: css/components/alert-banner/alert-banner.css: { preprocess: false } ...
The gains?
Well the size of CSS and JS loaded onto the homepage almost halved!
All without any visual regressions or loss of functionality, which I thought was pretty impressive. Think of all those unused kilobytes! It was quite a chubby pull request refactoring an entire theme, but it resulted in a much slimmer website :)
This change hasn’t made it through to production yet - testing is going to need to be thorough, but as soon as it does I’ll update this post with some proper before and after stats. For now I can definitely say that, exluding 3rd party scripts like google analytics and twitter widgets, what was once 470kb of Javascript and CSS loading onto the homepage is now 270kb.
It should be easier to implement next time around if I follow this process from the start, so I don’t see myself ever looking back! Although a small/simple website might not warrant it.