Slow compilation of SCSS files in Rails apps
See original GitHub issueHello š 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.
Issue Analytics
- State:
- Created 4 years ago
- Comments:27 (24 by maintainers)
Top GitHub 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?