Skip to main content

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.

by Rikki Bochow /

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.
 

Posted by Rikki Bochow
Front end Developer

Dated

Comments

Comment by Wim Leers

Dated

So glad to see the asset library system being used the way it is intended to be used, and I'm satisfied to read that you're seeing the rewards: dependencies are documented, and a significant front-end performance gain is being observed.

As one of the co-maintainers of both the asset library system and the documentation you've linked to: thank you for writing this! I'm adding a link to this article to the documentation!

Comment by Rikki Bochow

Dated

Thank you! Seeing how to get the docs updated was going to be my next task :D

Comment by Zack Hawkins

Dated

Thanks for the response! Best I can tell adding libraries to a theme's `*.info.yml` will only add them to the "dynamic" aggregate that can vary based on page and not the "every page" aggregate that's persistent. I could be doing something incorrectly. It looks like your "preprocess: false" approach may be a way around this.

Comment by Hamza Zia

Dated

Hey Ricky, did you notice any significant speed difference with your Drupal site through this approach? It looks quite promising, speed improvements would just be like icing on the cake!

Comment by Rikki Bochow

Dated

I haven't compared page speeds just yet, but I am expecting there to be some! I'm hoping to update this post next week with some more before and after stats.

Comment by Tobias Krause

Dated

Thank your for this article. It is an interesting approach although I bet that an user who clicks around your project will have loaded most of your css at the end though. What I mean: seperating some component styles from the basic theme css may rise your work as you need to decide for each component if it should be loaded generally/ globally or just on specific pages and you need to add the library - but all this work might not have such a big impact: your homepage's css might be small but when the user clicks to the other pages where the components will be loaded he will have loaded all the css with additional HTTP requests instead of having loaded one big css one time when initially requesting a page from your project (which might not beyour homepage).

Especially on dynamic projects with Drupal usually the same component (may be field formatter for example) are re-used so that the chance that the separetated components might be loaded during a visit are high. But your idea is still very important: think of big css and javascripts needed for tools and apps which might be part of a sub-section of your project but which might not be used by every user on your page. I think it is important to keep your idea in mind to deal with larger packages of css and javascript.

Comment by Rikki Bochow

Dated

Late reply I know, sorry! It's true that if a user is clicking around they'll get most of the styles anyway but that's assuming people do click around. Most people who have read this post came straight to this page, read it and left the site. They didn't need to download the other css. They did download it, because I haven't done this to our own site yet (sorry everyone), but they didn't need to!

Comment by Rikki Bochow

Dated

Not sure if this is still a problem or not for you, sorry for my late reply. Is it a library or a single CSS file? If it's from your own theme why can't you just remove it from your libraries.yml file rather than trying to unset it from the info.yml file?

Comment by rcaracaus

Dated

People think, "Oh, it is only 100kb of CSS, some of our images are bigger than that." -- But they forget that the CSS render blocks.
Webpagetest.org's filmstrip view has been my best friend recently.

Also, Rikki, check this out https://jakearchibald.com/2016/link-in-body/#a-simpler-better-way

This will mean that we want to put the 's to the stylesheets in the actual twig templates themselves as performance best-practice? I think they are doing above in prep for Web Components. With ES6 JS modules will allow us to avoid to stop using the asset library system altogether?

Comment by Rikki Bochow

Dated

Hey Rob! Hope you're good :)

That's very interesting, perhaps the attach_library function could potentially have an option to add the link tag right where it's called... Certainly a headless site with inline styles could bi-pass libraries entirely. Though I'm still using them with ES6 modules, just loading one Webpack bundled JS file (with the dependecy file imported) per component instead of referencing the other library as a dependency. 

Comment by Alejandro Soto

Dated

First of all, thanks for a great article!
Now here my question :D. Did you know how to load a library defined in theme.libraries.yml before a library defined in mymodule.libraries.yml?. What I see in the response HTML is that libraries defined in theme go after libraries defined in modules and changing the category (base, theme, component...) or weight of the libraries doesn't change that order (module libraries first and theme libraries after).

Thanks!!!

Comment by Rikki Bochow

Dated

I honestly have never tried! I usually go the opposite where I want the theme to override a module. If the theme is broken down like this you might be able to override one of the themes libraries in the module? Just a guess though

Pagination

Add new comment

Restricted HTML

  • Allowed HTML tags: <a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd> <h2> <h3> <h4> <h5> <h6>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
Not sure where to start? Try typing "hello" or "help" if you get stuck.