govuk-frontend: Slow compilation of SCSS files in Rails apps

Hello šŸ‘‹ from GOV.UK Publishing. Weā€™ve been having some problems with speed of CSS compilation of GOV.UK apps for a while and after doing some detective work weā€™ve traced that a large volume of the time is due to importing files from govuk-frontend.

Backstory

GOV.UK apps use the Rails asset pipline to transpile SCSS to CSS. They mostly continue to use the deprecated Ruby SASS library, but are likely to start switching to sassc in the forseeable future. Nearly all GOV.UK apps use the govuk_publishing_components gem which pulls in the govuk-frontend node module and imports all components.

Gradually over the last few months or years weā€™ve been seeing the time that is needed to compile assets for GOV.UK has increased and once we switched to a docker based development environment we started to investigate as it seemed abnormally poor. Developers are finding that frequently an app wonā€™t seem to work the first time you try run it locally as the CSS could be taking more than a minute to compile. We did start to wonder why other Rails devs werenā€™t going crazy about the slowness of CSS precompilation as it felt increasingly broken.

Looking into this further we found that most of the time was spent around the process of using @import to include files. Looking at a particular app, Finder Frontend, we could see that 7054 files were being imported, despite less than 400 distinct files being used, and every one of these files requires processing which seemed to be where the majority of the time was spent. Following this further we could see that from the point where we import all govuk-frontend components was where we were seeing the big increase in imported files.

Some basic benchmarks from my MBP 2015 on time to generate Finder Frontendā€™s application.css file:

Environment SASS Cache Time (s)
govuk-docker Ruby SASS Cold 59.983
govuk-docker Ruby SASS Warm 0.228
govuk-docker sassc Cold 32.693
govuk-docker sassc Warm 17.478
Local machine Ruby SASS Cold 31.926
Local machine Ruby SASS Warm 0.063
Local machine sassc Cold 11.805
Local machine sassc Warm 2.413

Cause and effect

The problem of slow CSS compilation for GOV.UK seems to stem from the @import approach used in govuk-frontend and how this can affect Rails apps.

If we follow govuk/all.scss we import ā€œsettings/allā€ (>= 13 files), ā€œtools/allā€ (>= 9 files), ā€œhelpers/allā€ (>= 12 files). Then for each component the same files are then imported. As sass executes each @import whether or not itā€™s been included before this can exponentially increase the amount of work done. Some basic maths would have the 30 components adding up to at least 384 files imported but I think the number ends up being quite significantly greater.

Weā€™re theorising that the sheer quantity of imported files affects the Rails asset pipeline particularly badly as everyone one of these files is checked to see if it uses any Rails specific functions (asset-url, asset-path). It could well be paired with IO operation pain too, as it probably searches a whole bunch of directories for every single file.

I put together some isolated test cases to show the effects in a Rails app free from the rest of the GOV.UK infrastructure. As a basis for comparison I used a version of govuk-frontend I put together that was optimised for the all.scss file where all other imports were removed.

Some benchmarks were:

SASS Cache Time (s)
sassc Cold 7.494180
sassc Warm 1.667473
Ruby SASS Cold 15.556202
Ruby SASS Warm 0.007234

Where we can see this performs relatively consistently with Finder Frontend app, just clearly has a lot less to do being isolated.

And compared to the version with duplicate @import calls removed:

SASS Cache Time (s) Decrease
sassc Cold 0.552520 92.6 %
sassc Warm 0.007781 99.5 %
Ruby SASS Cold 2.913921 81.3 %
Ruby SASS Warm 0.005758 20.4 %

Here we can see quite how dramatic the effects of @import can be.

I did ponder whether things would be quicker using webpacker with Rails. There I saw it take ~5s to compile with @imports duplicated and ~2.5s with them removed. So it seemed difference is still significant here but as the scale is much reduced the impact is less. Iā€™ll also add that I really didnā€™t put much time into setting up webpacker so these numbers could be flawed.

Finally, I tried running the sassc command line utility to see if this is slow I found that it took 0.27s to compile with @imports duplicated against 0.21s with them removed. Which isnā€™t a very significant difference.

Impact on GOV.UK

Some of the effects this has had on GOV.UK have been:

  • Apps never seem to work on first request for local dev, as Rails hangs waiting for assets to preload. Unaccustomed developers assume the app is broken, abort it and ask for help
  • First test was frequently failing on apps due to timeout. We switched to running a manual pre-compile before tests to resolve it.
  • Switching to sassc is concerning because of the heavy performance impact on even a warm cache.

In general we seem to have ended up with a perfect storm on GOV.UK by having some apps needing to precompile multiple files with the same contents, importing all of govuk-frontend and then switching to the slower docker environment for development.

Weā€™re currently pondering about switching away from the approach of including all govuk-frontend components which would likely offer some performance improvement. Though weā€™d still be paying a reasonable performance penalty for duplicate @import calls.

Although we could see performance improve a reasonable amount by switching to use webpacker with Rails this seems like a long way off for GOV.UK given the quantity of apps and the amount of existing asset-pipeline based code we have. Weā€™re still a reasonable distance from being able to migrate one app to webpacker

Things weā€™re wondering about

  • Whether something is broken in sassc-rails for the warm cache performance to perform so badly
  • Quite what is going on with each import that impacts time so badly
  • @use seems the ideal solution for resolving this problem but itā€™s not available on libsass.
  • Whether other teams on different stacks are experiencing same performance issues
  • Whether the govuk-frontend style of @import usage is common or not - I notice itā€™s not an approach tachyons or bootstrap use - as to whether wider world is affected.

Itā€™d be great if we can try work out if there are things that can be done to reduce this problem. Although aspects of this seem quite specific Rails it does seem to be a problem that is likely to affect other environments (as multiple execution is a property of @import) and is likely to only increase in severity as more components are added.

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Comments: 27 (24 by maintainers)

Commits related to this issue

Most upvoted comments

I have a WIP branch that tries to provide a non breaking-change way of reducing the number of imports when importing all:

https://github.com/alphagov/govuk-frontend/tree/sass-perf

@kevindew weā€™ve published a pre-release of the changes in #1752 ā€“ would you be able to benchmark it and see how it compares?

npm install --save alphagov/govuk-frontend#6735d43a

I remember when I worked on Finder-frontend almost a year ago it was painfully slow. It was the first time that Iā€™ve had noticeable issues with sass compilation.

@kevindew have you tried removing the @import ā€˜govuk_publishing_components/all_componentsā€™ to see if itā€™s any quicker?

Yes, we tried that a long time ago and found it made a huge difference. We hadnā€™t quite traced the issues down to govuk-frontend at that point so havenā€™t tried it in a while.

As govuk_publishing_components pulls in govuk-frontend dependencies at various points (for example: https://github.com/alphagov/govuk_publishing_components/blob/cd014428a873588701c05e25d93f57d9bed46f80/app/assets/stylesheets/govuk_publishing_components/components/_back-link.scss#L1) we likely compound the performance issue further by causing further cascades of @import