As much as I like working with Magento 2 I have to admit it’s never been known for its blazing performance, page loading speeds in both frontend and backend have been consistently slower than its own 1.9 version and that’s been a problem most Magento developers have had to deal with in some way or another.
Don’t get me wrong, there are a lot of changes Magento’s done to improve development experience: using composer, the plugin system, dependency injection, just to name a few, but page loading speeds (specially on the frontend) have suffered because the frontend stack that Magento decided to go with hasn’t scaled very well and it’s so tied to the core that making drastic changes to it involves a lot of work.
That’s why we’ve seen a push for PWA solutions like Vue Storefront or Magento’s own PWA Studio to bring a more modern approach to the e-commerce experience but what can should we do with all the Magento 2 stores running now? Should we rewrite them all from scratch as a PWA?
Well… Yes! If that’s an option, however there are some things we can do to improve page loading speeds using the current Magento 2 stack that I don’t see being discussed that often.
I’ll be using Google’s Page Speed Insights score to measure the impact of these improvements because it’s “the standard” for these sort of things these days and Magento is not known to score very well out of the box but first we need to understand how the score works so we know what we should be focusing on.
Understanding the Page Speed Score
Google’s Page Speed Insights performance score is a summary of 6 main metrics:
- First Contentful Paint: marks the time at which the first text or image is painted.
- First Meaningful Paint: measures when the primary content of a page is visible.
- Speed Index: shows how quickly the contents of a page are visibly populated.
- Time to Interactive: the amount of time it takes for the page to become fully interactive.
- First CPU Idle: marks the first time at which the page’s main thread is quiet enough to handle input.
- Max Potential First Input Delay: The maximum potential First Input Delay that your users could experience is the duration, in milliseconds, of the longest task.
These scores however are not treated equally, Google has a lot of data about how people in the real works use the web in their Chrome User Experience Report so they can recommend best practices based on the analysis of millions of users across millions of websites.
Here’s how the metrics are scored as of December 2019:
- Time to Interactive — 33.3%
- Speed Index — 26.7%
- First Contentful Paint — 20.0%
- First CPU Idle — 13.3%
- First Meaningful Paint — 6.7%
- Max Potential First Input Delay — 0%
Based on this we can conclude that not only we have to display content on the screen quickly, we also need to make sure the content can be interacted with quickly, that means: as soon as something can be seen users should be able to click it, tap it, scroll it, etc., and the page should respond accordingly.
Let’s see how this all looks in a base installation of Magento 2.2.10 (I’ll come back to the version number later) using the Luma theme and sample data:
For convenience, I’ll be using Chrome’s built-in Lighthouse audits to measure performance.
As you can see we get decent FCP and FMP times but everything else is lacking. This makes sense when we understand that Magento adds hundreds of scripts on each page load and they are included for the most part using RequireJS, that means they get loaded in sequence not in parallel so the browser has to wait a while until all of them have been downloaded, parsed, compiled and executed.
Setting up our goal
So now we know that Google’s Page Speed score gives priority to sites that can be interacted with quickly, however there are so many scripts included by default in Magento 2 that no matter how minified, merged or bundled they are they still need a lot of time to load.
Instead of coming up with some fancy new way of optimising all this code, I think we should try to:
Remove as many scripts as possible…
And I don’t mean getting rid of a couple JS files here and there, I mean going for a deep clean and see how far that takes us.
Let the purge begin
Let’s start with the easy stuff, we can disable all the modules that we know we won’t use in our Magento 2 website. This might not remove any JS files since not all modules have frontend assets however it will potentially reduce the server response time which will improve all the Page Speed Score metrics proportionally.
- Are you not using MSI? Kill it ☠️
- Don’t care about swatches? Kill it ☠️
- Amazon Payments? Kill it ☠️
- Klarna? Kill it ☠️
- Temando Shipping? WTF is that!? Bye ☠️
Worst case scenario it will declutter our admin area and make DI compilation quicker so there aren’t any downsides that I can think of, just make sure not to disable modules that are being dependent on.
Integer_net has a nice article on how to remove unused core modules with composer but I found that just disabling them works well enough.
Upgrade to Magento ≥ 2.3.3
Earlier in the article I ran the initial Page Speed measurements on an “old” 2.2.10 version of Magento on purpose to showcase one of the main features introduced in 2.3.3.
Earlier versions of Magento used to include the whole jQuery UI library even though they only used some of its components. Starting on 2.3.3 jQuery UI has been split into smaller parts so only the code that’s necessary gets loaded when it’s needed, this gives us some performance benefits for free.
As you can see we managed to improve our score by 10%, decreased our JS request size by ~10% and JS request count by ~10%. Nothing extraordinary but keep in mind we haven’t written a single line of code yet.
Using 2.3.3 comes with its own set of issues but if you can afford the upgrade it can give you instant performance wins.
Removing unused scripts
Now comes the fun part, out of all those 169 JS files how many of those is our website actually using?
We need to find out what files are not being used and delete them to oblivion, this sounds simple but it’s can be very tricky because:
- Just because the script is not used somewhere it doesn’t mean it’s not used somewhere else.
- Scripts can have very obscure dependencies, even not so obvious ones.
One of the easiest ways I’ve found to spot unused code is by using Chrome’s Code Coverage Report, so try switching it on, reload the page and interact with it for a while to see how much of the code it’s actually being used.
Here we can see some obvious and not so obvious suspects just by looking at the “Unused Bytes” column, keep in mind that even if they seem like they’re being used (30–40% usage) there’s a good chance that it’s coming from the initialisation code that runs when the file is included not from actual usage on the page so make sure to go into all of them and review the report.
I’ll go through the files I ended up removing with my reasoning behind it but don’t follow my advise blindly as it might not apply to your project in the same way:
- range.js: Looks like some sort of range slider, maybe used as a price slider somewhere but not even the layered navigation uses it by default, so bye! ☠️
- colorpicker.js: I can’t remember the last time I used a colorpicker in an e-commerce site. ☠️
- datepicker.js: Same as above, I can think of more use cases for this but definetly not needed everywhere. ☠️
- tooltip.js: This is used to render some information tooltips around the site, we use CSS for this functionality so no need. ☠️
- polyfill.js: Looks like a polyfill for localStorage and sessionStorage for older browsers, this is also useful in some modern browsers (like Safari on iOS) where there’s no localStorage when using Incognito / Privacy Mode so it falls back to using cookies but in our case we don’t care about this feature. ☠
- decorate.js: Used to add classes to lists, presumably for styling (first, odd, even, last). Maybe a remnant of the Magento 1 days when the CSS
:nth-childselector wasn’t as widely supported. ☠️
There are some files that need more digging, for example I could see some draggable.js and resizable.js files being loaded but I couldn’t think of anywhere where I would care about these features. Luckily you can use Chrome to hunt down these dependencies:
Using this feature you can search within loaded assets and see which files are trying to load unwanted dependencies. In this case it was used by the dialog.js file to allow you to resize and drag the modal windows around (eg: “remove from minicart” modals), a very core e-commerce feature I know but something I’m willing to live without.
Stripping down the minicart
While I was looking at the minicart component I realised there’s a lot of stuff there I wasn’t very emotionally attached to:
- confirm.js and alert.js: Only used when you want to confirm a removal from cart or display an error. We don’t mind removing items from the cart without confirmation and for errors we can display them using the regular Magento Ui messages. ☠️
- effects-fade and effects.js: Used to fade out items when removed from the cart, again not that bothered to lose that, if needed we can have it cheaper with a timeout + CSS transitions. ☠️
- authentication-popup.js: Used to display a popup if you require login to checkout, case by case dependant but we can get rid of it for now, if needed maybe a redirect with a message might be enough. ☠️
We could argue that by removing this code we’re left with a “poorer” user experience but when making decisions like this try to take this line of though:
Is the user going to be bothered by items not fading out when removed from the cart? Maybe not. Will they be bothered by the extra second it takes to load every single page because of this code? Probably.
Misc template changes
️Some scripts might need to be removed from .phtml template files, for example:
- menu.js: We usually use CSS for any interactivity with the menu and any JS we use will be way less than what’s included by default. ☠️
- form-mini.js: We rarely use the default Magento search so no need to include this to handle autocompletion. ☠️
Some third-party modules are known for extending this file so make sure it’s not being used.
There are some less than obvious ones as well:
- swatches-render.js: Even though we do use swatches occasionally we rarely render them on product lists so we don’t need this. If you do, be ready to deal with other DB performance issues as a result. ☠
- t️ranslate-inline.js: Used to render the UI to translate strings directly on the frontend, we never use this as all translations are done in the code level. ☠️
So far we’ve been removing unused script files but we can also replace some of the biggest libraries with some alternatives that will make the total request size smaller.
To help with this you can use Bundlephobia to have an idea of the impact a given library will have on your site and even get some recommendations on alternatives you can use to replace it, eg:
- moment.js: Luckily Magento 2 doesn’t come bundled with the whole moment.js library but it’s still quite big (~20KB). As far as I could see it’s only used for some logging functionality and date validations so we can replace it with something like dayjs (~2KB) and remove ~95% of that code. ☠️
- jquery.js: This is a bit scarier because jQuery is used EVERYWHERE but upgrading to jQuery 2.2.4 will reduce the library size by ~10%. This major version only dropped support for older browsers while keeping backwards compatibility so in theory (and in my limited testing) it should still work without major issues. ☠️
- underscore.js: I didn’t actually change this one but I still thought it would be worth mentioning. In theory this library could get the same treatment as jQuery UI and split it into separate components (using something like lodash) since only ~40% is used at a time but it will require a lot of work and I don’t know if it’s worth for the sake of saving 5–6KBs. ⚠️
Going the extra mile
We’ve been focusing on JS since by know we know that’s what the browser struggles with the most, but we’re on a warpath here so we might as well get rid of other assets while we’re at it, eg:
- CSS: For this I don’t mean performing any kind of Critical Path optimisations, I mean removing styles we won’t use. Since we already removed the date-picker stuff it makes sense to get rid of the calendar.css styles as well, etc. ☠️
- Fonts: Sounds be obvious but let’s get rid of all the custom fonts and variations we’re not using. ☠️
- Icons: We use SVG icons for these so we can get rid of the Luma/Blank icon font.
Let’s see how we look after doing all that work:
Still nothing formidable however if we compare it with our initial numbers we can see a:
- 42% increase in Page Speed Score. 🎉
- 42% decrease in request count. 🎉
- 42% decrease in JS bundle size. 🎉
You can probably spot a trend now, removing code proportionally benefits our Page Speed Score. Keep in mind we haven’t written any code, just removed or replaced so that’s code that we’ll never have to worry about maintaining, updating, patch or concern if it becomes vulnerable.
On top of that I’ve been running these audits on a less than ideal environment by using:
- Local development environment.
- Developer mode.
- No composer optimisations.
- No minification or caching.
- No image lazy loading.
- No CDN.
So other than removing JS I haven’t focused on any of the more well known “tricks” to improve frontend performance, in fact the Audits show a lot of room for improvement:
So I went ahead and took care of most of these, the only one I didn’t bother with for now was “Removing Unused CSS” and “Optimising for the Critical Path” because:
- It’s more time consuming than removing JS.
- In my limited testing it hasn’t improved load times that much. I think people underestimate how good browsers are at painting pixels on the screen.
However I did:
- Enable JS, CSS and HTML minification.
- Add our own image lazy loading extension.
- Switch Magento to production mode and optimise composer autoloader.
So let’s see, how are we looking?
Much better now, yet there’s still more we can do…
Even though we’ve removed a lot of JS code we’re still loading hundreds of files per request, which wouldn’t be a massive issue if they were prefetched and loaded in parallel thanks to HTTP/2, but they’re not…
Luckily there are tools that can be incorporated in our deployment process that can help optimise JS beyond a simple merge and minification like:
The article I linked to above describes in more detail how to use each of them, I’ll be using the “Magento 2 DevTools” approach with the changes Willem recommended in his article because, even though these are no longer supported, I’ve got the best results with them but “Baler” looks very promising so far.
So after optimising our JS bundles with the “Magento 2 DevTools” we’re looking like this:
So close, yet so far… But it’s a very good score nevertheless, even one that some PWAs struggle to accomplish these days.
On top of optimising JS bundles I made sure to add
<link rel="preload" href="..." as="script"> in the HTML
<head> to make sure the scripts are loaded as soon as possible. This increases the FCP and FMP times a bit but reduces TTI by quite a lot which is the most important metric.
I’m sure there are ways to keep improving this score even more, I’ll keep working on this to get it closer to that sweet “100” but there are many areas of improvement. So for we only focused on JS and although it’s the biggest piece of the puzzle there are other ones that could use more attention (CSS, images, fonts, HTML).
Also, these kinds of optimisations are not the ones you only worry about at the end of the project, I’d argue that it might be to late then. You need to keep them in the back of your head every time you work on a new feature, perform and upgrade or install any new extension. Try to keep the amount of code as small as possible, the code that’s more optimised is the one that’s not there in the first place!