Why is Magento 2 LESS compilation so slow?
It has been the frustration of Magento 2 frontend developers for years already. And it has surprised me that so little has been written about it: Compiling the default LESS code of Magento 2 is slow, easily taking up to 8 seconds. But why? Here's a summary of an experiment I've run to find out what is the actual problem. Plus some conclusions (and semi-solutions).
Instead of writing CSS directly, CSS preprocessors like SASS and LESS have become popular over the years. Magento 2 adopted LESS at an early stage. Nowaways, the
Magento/blank theme and the
Magento/luma theme ship with numerous LESS files that are boiling down to 2 main files:
These 2 main LESS files are either compiled using a PHP library (
oyejorge) or Grunt. It is theoretically possible to compile the LESS with other tools but then you will need to solve the issue of
@magento_import statements (that support theme inheritance and module status) yourself.
When you try to compile the LESS files
styles-l.less, the compilation time takes up to 6 seconds.
When this is part of static content deployment (which is again part of the deployment of code to a live site), nobody really cares. However, a modern frontend developer workflow includes a Grunt task
watch that keeps monitoring LESS files for changes. And when this task picks up on a change, the frontend developer will wait for this task to complete before seeing the effect of that change on screen in a browser. Tools like LiveReload come in handy.
Slow loading time stops you in the flow
But the problem is the waiting time. When a frontend developer is in a flow, it should only take a maximum of about 1 second before LESS compiles into CSS. This is about the time that it takes to refresh a browser or (if the browser is refreshed automatically) to shift your eye focus from your editor to the browser screen.
To compare, I've been using LESS (and SASS) for custom projects and it usually takes about 400-600 ms to compile with Grunt and 300-500 ms with Gulp. That's what we want to see. So something is wrong in the Magento setup.
What is eating?
While talking about this with other developers, I've often heard that it was Magento to blame. I've been repeating that same thing as well. But saying that Magento is the problem is saying that the computer doesn't work. If you don't dive into the details of the problem, you are not helping to solve it.
Let's go through the stages of LESS compilation to see what happens where:
Stage 1: Preprocessing the preprocessor
When Grunt is used for LESS compilation, Grunt is checking with Magento which Magento modules are enabled, then copies all of the LESS files per Magento module to a temporary folder
var/view_preprocessed, thus resolving the
@magento_import statement and translating it into regular
Stage 2: Compiling LESS
Whatever ends up in the
var/view_preprocessed folder is plain LESS. You can compile it with any LESS compiler. Magento uses either the PHP library or a Grunt plugin.
Stage 3: Copy to /pub
Finally, the CSS that is the result of LESS compilation is copied to
/pub/static/frontend and we're done.
Reviewing the stages
I've spent a long weekend (about half a year ago) to hack my way through stage 1, focusing on the way that Grunt was gathering LESS files but also benchmarking the call to Magento to see which Magento modules are enabled. I started this experiment thinking that Magento was to blame for the speed. But the call to Magento took only 60-80 ms in my case, the gathering of Grunt files only about 200 ms (with a SSD disk).
It is not Magento
The conclusion of this first experiment was kind of baffling: It was not Magento - not its complex PHP architecture, not its database, not its huge file structure. It was also not Grunt, while many have complained about Grunt being so much slower than Gulp. It was not even the file copy process dealing with 100s of LESS files. Stage 1 took place in only 200-300 ms (maybe still slow, but not leading to 8 seconds).
It was the LESS compilation itself (stage 2).
Proof that LESS itself is to be blamed
This blog is the result of that old experiment plus a new experiment I did in the last couple of days. And the first thing that I did was find an easy way to show that LESS itself is to be blamed.
Make sure Grunt is working and then run
grunt less. Next, navigate into the
var/view_preprocessed and then further down to the
pub/static/frontend/Magento/luma/en_US/ folder. Here in this folder, I used the regular NPM
lessc command (
npm install -g less) to compile the main 2 LESS files. And I also used the
time command to measure how much time this took:
time lessc css/styles-m.less > /dev/null time lessc css/styles-l.less > /dev/null
Compiling the file
styles-m.less took almost 6 seconds and compiling the file
styles-l.less took over 2 seconds. Together 8 seconds compilation time, without Grunt or Magento being involved at all.
In my first experiment, I came to this conclusion already. But when hacking through the LESS code itself, I removed
@import rules, saw an improvement in performance and made a premature conclusion that the LESS
@import rules themselves might be responsible for the speed. I didn't blog about this and am now glad I didn't: This was a wrong conclusion.
Now, I dived more deeply into this: If
@import rules were to blamed, the speed of LESS compilation would go up if all
@import rules were resolved first. For this, I created a little PHP script
less_import_parser.php (gist.github.com/27a1406586b4e5bba0cfdb05ba2ada0a) that simply reads all
@import rules and replaces them with a dump of that corresponding LESS file.
No more @import, one huge LESS blob
The result of this PHP script are a couple of single LESS files - each corresponding the original main LESS file - without any
@import rules. I named these files
test-m.less. Next, I used the same approach as earlier to measure compilation time:
time lessc css/test-l.less > /dev/null time lessc css/test-m.less > /dev/null
Result: No real difference in compilation time. Conclusion: The LESS
@import rules (so also the number of LESS files) do not affect the compilation process.
Rectification: The number of LESS files has no impact
In the last 2 years, I've been saying people that LESS compilation is something that could theoretically be improved by reducing the number of LESS files involved. (There is still some truth in that, because the less LESS-code you have, the faster it becomes.)
After the first experiment, I've been telling people also that there is a magical limit of 80 LESS files above which LESS suddenly becomes a burden. This limit is extremely weird but real: I've experienced in a custom project a better performance by reducing 120 LESS files to 60 LESS files, without reducing the LESS code itself.
However, this does not seem the problem with Magento.
Narrowing down the real issue
So if it is not Magento, not Grunt, not LESS
@import, there are only 2 areas left that could be causing the problem: The amount of LESS code involved. Or the specific syntax of LESS used by Magento. Well, to be quick about the first possibility: I've been using Bootstrap 3 for years now and the amount of LESS code I'm using there is larger than with Magento, while compilation time is around 800 ms. It's not the amount of LESS code.
It's the syntax. I'm not a LESS expert. But somehow the way that the
Magento/blank theme (and Luma) uses LESS variables in mixins and calculations causes LESS compilation to take more time.
To solve this, you will need to heavily rewrite the LESS code. Or ditch it. Have fun either way. Conclusions are still towards the end of this article.
Switching from LESS to SASS?
I'm just a simple Dutch boy. So I decided that maybe LESS was just efficient enough in its logic. So perhaps SASS did a better job. I installed a Ruby gem
less2sass to convert all of the LESS code to SASS. This failed. I also used some web-based converters but the resulting SCSS files never compiled to CSS properly.
I found no way to easily convert LESS to SASS. And I don't know whether this would have solved the issue anyway.
Switching from Grunt to Gulp
While it might be that
lessc is the most barebones compiler to test with, it is not the most performant. Gulp uses Node streams to compile resources and thus handles files more efficiently. Simply by using Gulp you can shave off some seconds. By using subodha/magento-2-gulp you can combine Gulp and LESS and compile the code quicker than with the Magento Grunt / LESS mix:
In my case, running
grunt less:luma took about 4 seconds. With
gulp less --luma, this becomes 2.5 seconds. Still slow, but better.
RAM disks or tmpfs have no effect
I tried to copy all of the LESS files to
tmpfs and a RAM-disk, effectively making sure that zero calls to disk where needed. With a modern Linux filesystem (which I have) memory is used efficiently anyway, but I wanted to make sure. Zero impact. Adding RAM to this doesn't solve anything.
(I thought about maybe adding more CPUs to the compilation process, but haven't found a multi-threaded compiler.)
This does work: Disable modules
Reducing the amount of LESS code involved does have an impact on performance: The less LESS code needs to be compiled, the less time it consumes. So reducing the number of modules is definitely something to consider.
I've written a blog post on this earlier (Disabling annoying 3rd party modules) but the blogs of Integer.net offer a much cooler
composer solution: Removing unused core modules from Magento 2 – the right way
Reducing the number of Magento modules in your setup is good anyway. But it doesn't take the fact that something is wrong with the LESS code of Magento.
The complexity of the LESS code written by Magento itself is to blame. I've run bigger LESS projects myself, where more code is compiled in about 10% of the time that Magento takes. As I see it, the main reason why my LESS code is faster is because it only includes simple variables, mixins behaving like pure functions and that's it.
Waiting for LESS code to compile in Magento 2 takes a long time, which is a problem. Let's now look at some possible solutions.
Solution 1: Deal with it
Suck it up. It only takes 8 seconds to compile code and see that change in your browser. It doesn't mean that you need to stare out the window in those 8 seconds. Find a way to make that time productive. In those seconds, I usually think about my life as a Magento developer.
Solution 2: Replace LESS with something else
The dirty conclusion is that the complexity of the LESS code is to be blamed. Get rid of it. Rewrite the LESS code into something you understand. Or write new LESS code from scratch. And while you're at it, perhaps switch from LESS to SASS. Or use CSS variables or Flexbox. Once you are free from the restraints of Magento LESS, you can build whatever you want to build. For instance, Snowdogs Frontools is a popular approach.
Solution 3: Add your own CSS on top of Magento CSS
If the first 2 solutions are too frustrating, there is also a less perfect solution: Forget about extending the Magento LESS. Instead, use XML layout to add your own
theme.css to the page and extend upon that file. And you can use LESS or SASS or whatever to generate that file. Performance-wise, this is not the best solution but it will get rid of the frustration without too much effort.
Solution 4: Use subodha/magento-2-gulp
By swapping Grunt with Gulp, you get the benefit of Node streams which handles file contents more efficiently. With the setup of subodha/magento-2-gulp, you can bring compilation down from 4-6s to about 2-3 seconds which is awesome. Thanks Martijn Cuijten for pointing this out.
Feel free to comment
I hope you liked this article. For me, it was interesting to go through the benchmarking and see what was eating where. Unfortunately, it doesn't offer a solid solution to fix everything. But hopefully, it helps you decide upon the best solution for you.
Feel free to leave a comment below. I'm eager to hear any feedback on the complexity of LESS code structuring in Magento. Likewise, if you have alternative solutions, please do let everybody know.