JavaScript is one of the biggest culprits behind slow Magento 2 storefronts. A default installation can easily generate 100+ individual JS file requests per page. Each one adds latency, blocks rendering, and hammers your Time to First Byte (TTFB). In this guide, we'll walk through Magento's built-in bundling system, how RequireJS works under the hood, and the concrete steps you can take to dramatically reduce JS overhead.
Why Magento 2 JS is Slow by Default
Magento 2 uses RequireJS (AMD module format) to load JavaScript dependencies on demand. This is a flexible system, but it comes with a significant downside in production: the browser has to make a waterfall of individual HTTP requests to resolve the dependency graph at runtime.
On a typical category page you'll see:
requirejs/require.jsrequirejs-config.js- Dozens of individual
*.jsmodules loaded on demand
Even with HTTP/2 multiplexing, this many round trips add up — especially on mobile or high-latency connections.
Step 1: Enable Production Mode
Before any JS optimisation makes sense, make sure you're running in production mode. Development mode intentionally skips most optimizations.
bin/magento deploy:mode:set production
In production mode Magento automatically:
- Merges and minifies CSS
- Deploys static assets to
pub/static/ - Enables RequireJS optimizer output
Run the full static content deploy after switching:
bin/magento setup:static-content:deploy -f en_US nl_NL
Step 2: Enable JavaScript Merging and Minification
In the Admin under Stores → Configuration → Advanced → Developer → JavaScript Settings, enable:
| Setting | Value |
|---|---|
| Merge JavaScript Files | Yes |
| Enable JavaScript Bundling | Yes |
| Minify JavaScript Files | Yes |
Or via CLI:
bin/magento config:set dev/js/merge_files 1
bin/magento config:set dev/js/enable_js_bundling 1
bin/magento config:set dev/js/minify_files 1
bin/magento cache:flush
⚠️ Note: Magento's built-in bundling creates a single large bundle per page type (homepage, category, product, etc.). This reduces request count dramatically but can increase initial payload size. Measure the impact with Lighthouse before and after.
Step 3: Understand the RequireJS Build System
Magento ships with an r.js optimizer workflow. When you run setup:static-content:deploy, Magento processes your requirejs-config.js files and can produce optimised bundles.
The key file is requirejs-config.js — each module can contribute to it via view/frontend/requirejs-config.js. Badly written third-party configs are often the source of JS bloat.
Check for conflicts or redundant map entries:
# Inspect the merged requirejs config for a theme
cat pub/static/frontend/Magento/luma/en_US/requirejs-config.js | python3 -m json.tool | grep -i "map\|paths\|shim" | wc -l
A healthy store should have a focused, clean config. If you're seeing hundreds of entries, a third-party module is likely over-declaring dependencies.
Step 4: Identify Your Heaviest JavaScript Files
Before blindly bundling, know what you're dealing with:
# Find the largest JS files in your static deploy
find pub/static/frontend -name "*.js" -not -name "*.min.js" \
| xargs wc -c \
| sort -rn \
| head -20
Common heavy hitters:
-
jquery.js/jquery-ui.js— often loaded even when not needed on a page -
mage/validation.js— heavy, only needed on checkout/forms -
Magento_Catalog/js/product/view/bundle — large on PDPs
Step 5: Use Advanced JS Bundling (Grunt-based)
Magento's default bundling is coarse-grained. For finer control, use the official Advanced JS Bundling approach with Grunt.
Install dependencies in the Magento root:
npm install
npm install -g grunt-cli
Create a bundle config targeting page types:
// Example: split bundles by page type
bundles: {
'bundles/default': {
include: [
'jquery',
'jquery/ui',
'mage/apply/main',
'mage/apply/scripts',
'mage/common',
'mage/dataPost',
'mage/bootstrap'
],
exclude: [],
create: true
},
'bundles/catalog': {
include: [
'Magento_Catalog/js/view/compare',
'Magento_Catalog/js/price-box',
'Magento_Catalog/js/product/view/product.types.config'
],
exclude: ['bundles/default'],
create: true
}
}
Run the bundling:
grunt exec:mage-build
This produces smaller, page-type-specific bundles so visitors only download what they need per page type.
Step 6: Defer Non-Critical JavaScript
Not all JS needs to block rendering. Use Magento's layout XML to defer scripts that don't need to run at parse time.
In your theme's default.xml:
<block class="Magento\Framework\View\Element\Html\Head\Script" name="my.deferred.script">
<arguments>
<argument name="file" xsi:type="string">js/my-script.js</argument>
<argument name="attributes" xsi:type="string">defer</argument>
</arguments>
</block>
For third-party blocks loaded in the footer, adding defer or async can shave hundreds of milliseconds off your Largest Contentful Paint (LCP).
Step 7: Lazy-Load Heavy Widgets
RequireJS's require() call is already lazy by nature, but make sure you're not eagerly loading heavy modules on every page. Audit your requirejs-config.js for deps entries — these are loaded eagerly on every page load regardless of whether the functionality is used.
// BAD — loads on every page
var config = {
deps: ['Magento_SomeModule/js/heavy-widget']
};
// GOOD — only load when the DOM element exists
var config = {
map: {
'*': {
'heavyWidget': 'Magento_SomeModule/js/heavy-widget'
}
}
};
Then trigger the load conditionally in your template:
require(['heavyWidget'], function(widget) {
// Only runs when explicitly required
});
Step 8: Remove Unused Modules' JS
If you've disabled frontend modules (checkout steps, wishlists, product comparison), make sure their JavaScript isn't still being loaded. Use layout XML to remove blocks:
<referenceBlock name="some.js.block" remove="true"/>
Or disable the output of entire modules:
bin/magento module:disable Magento_Wishlist
Every module you disable removes its RequireJS config contributions and associated JS files from the dependency graph.
Measuring the Results
Use these tools to validate your work:
# Count JS requests on a page
curl -s https://yourstore.com/ | grep -o 'src="[^"]*\.js"' | wc -l
# Or use Lighthouse CI
npm install -g @lhci/cli
lhci autorun --collect.url=https://yourstore.com
Target metrics after optimization:
- JS request count: < 10 (down from 80–120)
- Total JS transfer size: < 300KB gzipped for above-the-fold
- Time to Interactive (TTI): < 3.5s on 4G
Common Pitfalls
Bundling breaks checkout: Magento's checkout is notoriously sensitive. Always test the full checkout flow after enabling bundling. If you see JavaScript errors, a module is likely missing from the bundle config.
Cache must be flushed after every config change:
bin/magento cache:flush config full_page block_html
Static deploy must be re-run after any requirejs-config.js change — the merged config is written to pub/static/ at deploy time, not runtime.
Summary
| Optimization | Impact | Effort |
|---|---|---|
| Production mode | High | Low |
| Merge + minify | High | Low |
| Built-in bundling | High | Low |
| Advanced Grunt bundling | Very High | Medium |
| Defer non-critical scripts | Medium | Low |
| Remove unused module JS | Medium | Medium |
| Lazy-load heavy widgets | Medium | Medium |
JavaScript performance in Magento 2 isn't about one silver bullet — it's about layering these optimizations. Start with production mode and built-in bundling for immediate wins, then invest in advanced Grunt-based page-type bundling for the biggest long-term payoff.
Every millisecond you shave off JS execution translates directly to better conversion rates. Google's own data shows a 0.1s improvement in load time correlates with an 8% improvement in conversion. That's not a rounding error — that's real revenue.
Top comments (0)