Frontend & Hyvä 12/16/2019

The integer_net solution to Magento 2 JavaScript bundling

Update from Nov 19, 2020: Please note that due to a Google PageSpeed update the guide found here still works, but no longer has the desired effect for Lighthouse. If you are looking for an alternative to improve the performance of your shop, Hyvä Themes might be of interest to you.

Table of Contents


Part One: The “ultimate guide” to Magento 2 JavaScript Bundling
  • About Lighthouse Audits
  • Benchmarks
  • How JavaScript influences the performance
Part Two: Magento 2 front end performance settings – Magento 2 bundling patches
  • The Basis — default Magento 2 setup & Patches
  • Default Magento Bundling
Part Three: How to bundle Magento 2 with Baler or M2 DevTools extension
  • Baler
  • Magento 2 DevTools browser extension
  • Devtools bundling
Part Four: The integer_net solution to JavaScript bundling
→ You are here
  • integer_net Bundling
  • A real-life example
  • Conclusions

integer_net Bundling

A lot of these steps are similar to the DevTools Bundling section. The big difference is that we make some changes to the requirejs-bundle-config file and get rid of the Magento_BundleConfig module.

 

Getting started

Base requirements:

  • All requirements listed at the 'The Basis — default Magento 2 setup' section, except for JavaScript minification
  • For Magento versions up until 2.3.4: apply the patches listed before: Mixins (<=2.3.4) and the Shims+mage-init fixes (<=2.3.3)
  • The requirejs-bundle-config.js file created with the Magento 2 DevTools toolbar, as described in a previous section

 

Optionally remove Baler:

If you just tested the results with Baler, you might want to clean that up first.

 

bin/magento module:uninstall Magento_Baler
composer remove magento/module-baler

 

Optionally remove Magento_BundleConfig:

If you just tested the results with M2 DevTools Bundling, you should clean that up first.

The BundleConfig module will inject your bundles into the head of Magento, and we don't want that. So remove or disable the module first.

 

bin/magento module:uninstall Magento_BundleConfig
composer remove magento/module-bundle-config

 

- Or -

 

bin/magento module:disable Magento_BundleConfig

 

Set Magento configuration

Disable all core JavaScript options. Remove --lock-config if you want it set in database instead of app/etc/config.php

 

bin/magento config:set dev/js/enable_js_bundling 0 --lock-config
bin/magento config:set dev/js/merge_files 0 --lock-config
bin/magento config:set dev/js/minify_files 0 --lock-config

 

We obviously don't want default Magento Bundling, and we'll do minification with Terser, which gets better results and works with modern JavaScript (ES6).

 

Install r.js, the RequireJS optimizer

You can either download r.js from the requirejs.org website or, better, install it with npm: npm install -g requirejs

You should now be able to run r.js from command line: r.js

> See https://github.com/requirejs/r.js for usage.

 

Install Terser

If you haven't already, install Terser globally.

npm install -g terser

 

Compile your themes

I usually run setup:upgrade to remove all compiled assets, and then compile DI and static content:

bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento setup:static-content:deploy en_US

 

By the way, did you know there is an excellent feature of Symfony Console, on which the Magento console is based, that allows you to abbreviate those commands?

This one liner performs all three commands above:

bin/magento set:up && bin/magento set:di:co && bin/magento set:sta:dep en_US

 

THE MAGIC

We need to make a few changes to our requirejs-bundle-config.js.

First of all, set optimize to none: "optimize": "none"

Then, inside the "modules: []" add this new bundle config:

Code snippet for require bundle config

Exclude requirejs/require from the shared bundle:

Code snippet for bundle-exclude

Exclude requirejs/require from all the other bundles:

Code snippet for bundles-exclude-example

Finally, at the end of the file, remove the last bracket } just before the last }) and add this

Code snippet for onModuleBundleComplete

You can view these changes in this single commit on Github: https://github.com/integer-net/magento2-requirejs-bundling/commit/883962adf013efd5ee0551ea2700987e3a78d1a1#diff-4333a40a1784ff0986cd218e1e965bd9

 

So, what did we do here?

  • We took out minification since we'll do it with Terser after the bundling step
  • We inserted "mage/common", "jquery/jquery.cookie" and "jquery/jquery-storageapi" into "requirejs/require". This file already exists and is loaded in the head of your page synchronously.
  • We tell r.js not to include files from "requirejs/require" into other bundles, so that we're sure they will be loaded once and only from "requirejs/require". This way we are also sure these JavaScript modules are loaded before our page is being rendered in the frontend.
  • We added onBundleComplete which injects all our bundle configuration into requirejs-config.js. This bundle config helps RequireJS to find the appropriate bundle instead of requesting a single file in the frontend.

 

Or, to keep it simple, we reduced the absolute minimum of JavaScript we need for a functioning page into requirejs/require and made all other JavaScript available through async bundles.

This means we're blocking page render as little as possible, while moving all other JavaScript into bundles that are fully optimized and only loaded when needed on the page.

 

Why did we add those modules to requirejs/require?

We need "mage/common", "jquery/jquery.cookie" and "jquery/jquery-storageapi" in that main, synchronous, bundle because the browser needs those scripts when parsing the body of the page. Mage/common will for example make x-mage-init work and storageapi is needed for references to localStorage.getItem().

In our tests, all other scripts could be moved to asynchronous bundles without issues.

Create the bundles

We can now go to the root of our project, where you should have saved your improved requirejs-bundle-config.js

 

Now we are going to perform a couple of commands to first copy the generated themes from pub/static/frontend/ to a backup directory and then run the r.js optimizer which will copy the theme back into the original directory with the optimizations and generated bundles.

Code snippet for integer_net-bundle-bash-script

You should see a long list of output for bundles that are being created. Then all bundles and `require.js` and `require-config.js` are minified by Terser.

We don't minify the entire theme directory because in theory all files should be in the bundles already, and the minification through Terser otherwise takes extremely long.

That's it

To be sure that your new files are being loaded, flush your cache (and maybe even force clean your Varnish).

Now visit your store's frontend and open inspector. You should see bundles/shared.js loaded on any page and for any page that has a custom bundle you should see the bundle with the layout handle e.q. bundles/catalog-product-view

 

Troubleshooting


Bundling fails because certain files cannot be found.

The toolbar will pick up AMD files that might be inside a minified JavaScript file that are not in your RequireJS config. I found it easiest to simply remove those from the bundle configuration and try again.

 

Errors in the frontend

Did you apply the patches for bundling?

Is there any JavaScript loaded directly without the needed require([], function(){}) around it?

 

Some things don't work in the checkout

Mixins might not be working, and the first place you usually find out is in the checkout process.

Again, did you apply those patches?

 

Terser throws a "file read" error

If you changed the Terser command above, check whether you haven't placed spaces between the `reserved` list items. You would see this error if you did: "ERROR: ENOENT: no such file or directory, open 'jQuery,"

 

Other issues

You might have errors in your requirejs-config.js files, mixins or shims or any other issue. This is unfortunately a case by case issue and you'll have to debug.

 

Profile

You can now start profiling. Results really depend on your project, but we got the best scores using this approach. Hopefully Baler will be even better when it's finished.

 

Results

Table of results for JavaScript Bundling without and with integer_net Bundling.
Result of Lighthouse Perfomance Audit with and without integer_net Bundling

Homepage

Result of Lighthouse Perfomance Audits for integer_net bundling on the homepage of the Luma Demostore

Checkout

Result of Lighthouse Perfomance Audits with integer_net bundling for the checkout of the Luma Demostore

Implementation

We've managed to get this into our deployment pipeline installing `requirejs` locally in the project during deployment. This means you run `npm install requirejs` and execute `node_modules/bin/r.js` instead. This works well.

The same goes for Terser, it can be run from node_modules/bin/terser instead.

Just like with the DevTools Bundling approach, the downside to this implementation is that any time you change your shop and JavaScript files are added or removed, you need to create a new bundle config.

In case new files are added or missing from your bundle configuration, this is not a big issue because RequireJS will always fall back to loading a single file if it can't be found in the bundles.

If files are removed, however, you won't be able to create the bundles because r.js will fail and exit with an error file not found.

So keeping your configuration up to date is an ongoing process.

 

A real-life example

The reason we did such extensive research on the topic is because we were hitting walls with every approach we took. We don't really have standard cases that compare to the default Magento 2 shop with sample data.

There are either a lot of extra JavaScript libraries, or we added a lot of JavaScript, since we're really into building React components into Magento lately.

To show an example of a project where we struggled with frontend performance, here's a benchmark of one of our clients:

There are certain factors we haven't been able to optimize yet, such as the amount of CSS, because we had to take over the legacy design from the old website and it yet has to be refactored.

Like JavaScript, CSS is a page-blocking factor and hurts the performance score a lot. It's out of the scope of this article (wasn't it long enough already?), but Magento recently added a `CSS critical path` functionality that will greatly help with reducing the initial CSSload and making all other CSS load asynchronously.

Another factor is the sheer amount of third-party libraries that are loaded on the live website. We see 16 seconds of execution time on mobile devices just to load in all the tracking scripts (Analytics, Facebook, Bing, Criteo, Hotjar, TrustedShops, etc). This completely drags down the score.

The scores below are without third-party scripts. For this website we see almost a 50% decrease of Performance Score as soon as these scripts are added.


The configurations that I have tested are:

These are the exact same circumstances as tested before on default Magento with sample data.

We only left out default Magento bundling. But if you must know, it's all zeros.

  • Default: JS minification enabled, no bundling
  • Baler: JS minification and bundling disabled, Baler bundling and Terser minification via command line
  • Devtools bundling: JS minification and bundling disabled. Advanced bundling and minification via r.js using configuration generated by M2-Devtools plugin
  • Our 'integer_net' config: JS minification and bundling disabled. Advanced bundling via r.js using configuration generated by M2-Devtools plugin, with some adjustments. Minification through Terser.js

 

Results

Table of results for JavaScript Bundling with the methods: Defautl, Baler, DevTools and integer_net.

Conclusions

Magento 2 frontend optimization is not an easy task. Hopefully we gave you enough tools to cruise through this and make the biggest gains possible, which is bundling JavaScript.

You might find different outcomes than we did, and we'd be very interested to hear about it.

If you can validate our methods and send us your results, that would be very helpful too. We want to give the best advice possible.

t's understandable that Magento is moving on to a more modern tech stack (being the React based PWA Studio) that will replace the current frontend, and it will be a giant leap forward for the future.

However, we hope Magento - an Adobe company - finds it important enough to keep investing into making the current RequireJS/KnockoutJS frontend perform better.

After all, many merchants and agencies are heavily invested in the current frontend and won't switch to the new PWA suite any time soon.

Let's hope 2020 will bring us a full-featured Baler which will make this process easier.

 

Have feedback? Contact us via mail or on twitter @willemwigman.

We would really appreciate you sharing your results with us. What solution worked best and what are the before/after scores?

 

You finished reading the fourth and last part of our series.

 

Do you want to read another part?


Part One: The “ultimate guide” to Magento 2 JavaScript Bundling
  • About Lighthouse Audits
  • Benchmarks
  • How JavaScript influences the performance
Part Two: Magento 2 front end performance settings – Magento 2 bundling patches
  • The Basis — default Magento 2 setup & Patches
  • Default Magento Bundling
Part Three: How to bundle Magento 2 with Baler or M2 DevTools extension
  • Baler
  • Magento 2 DevTools browser extension
  • Devtools bundling
Part Four: The integer_net solution to JavaScript bundling
→ You are here
  • integer_net Bundling
  • A real-life example
  • Conclusions