All posts
Forward Development

Speeding up Magento 2 without breaking your store

A low-risk order of work for Magento 2 performance: measure first, fix caching, indexers, cron and modules one at a time, and leave checkout intact.

  • Magento 2
  • Adobe Commerce
  • Operations

Category pages take four seconds to load. Product pages are a little faster, but only after you have opened them once. Every few hours the admin hangs while someone saves a product. Somebody suggests installing a speed extension, someone else wants to turn on JS bundling, and a third person wants to upgrade the server. Any of those could help. Any of them could also break checkout on a Friday afternoon.

Most Magento 2 slowdowns come from a short list of causes: pages that miss the full-page cache, indexers left on Update on Save, a cron backlog, and too many third-party modules hooking into hot code paths. Front-end weight matters too, but it is rarely the first problem. The order you work in matters more than the list of fixes. Measure first, change one thing, measure again.

Measure before you change anything

Without a baseline you cannot tell whether a change helped, and you cannot prove it did not hurt. Before touching configuration, record:

  • Time to first byte on a few representative URLs: the home page, a large category, a configurable product, and the cart. Use curl -s -o /dev/null -w '%{time_starttransfer}\n' against each URL, twice. The second request should be much faster than the first. If it is not, the page is not being served from cache.
  • The deploy mode. bin/magento deploy:mode:show should say production. A store running in developer or default mode in production is slow, and nothing else on this list will make up for it.
  • Cache status. bin/magento cache:status should show every cache type enabled. Someone disabling a cache type while debugging and never re-enabling it is a common cause.
  • Server-side profile data. New Relic or Blackfire will show which transactions are slow and why. Without them, Magento's built-in profiler (enabled with the MAGE_PROFILER environment variable, on staging only) and the MySQL slow query log are a reasonable start.

Write the numbers down. Every later step is judged against them.

Fix full-page cache misses first

On a healthy store, almost all anonymous catalog traffic should be served by the full-page cache without PHP rendering anything. When that fails, every request rebuilds layout, loads collections and renders blocks. That one problem accounts for more slow Magento stores than anything else.

For production, use Varnish instead of the built-in file cache. The built-in cache works, but Varnish answers before the request reaches PHP. Once it is running, look for the reasons pages miss:

  • A block with cacheable="false" in layout XML. One such block makes the whole page uncacheable. Third-party modules sometimes add one to a default handle to render something customer-specific, and every page that uses that layout stops caching. Search the codebase for cacheable="false" and check where each one is used.
  • Constant invalidation. Hourly product imports, stock feeds or price updates that touch thousands of SKUs flush cache tags just as fast. The cache works, but it never stays warm.
  • Cookies or query strings that split the cache. Marketing parameters and stray cookies can turn one cached page into hundreds of variants unless the Varnish config strips or ignores them.

Fixing cacheable="false" usually means moving customer-specific content to private content (customer sections loaded over AJAX). That is a developer task, and checkout has to be tested afterwards.

Make cron reliable, then move indexers to Update by Schedule

With indexers on Update on Save, every admin save reindexes synchronously. Saving one product can lock tables, and that is why the admin hangs and front-end queries stall. bin/magento indexer:show-mode shows the current setting.

The fix is bin/magento indexer:set-mode schedule. Changes are then written to changelog tables and indexed in the background by cron. Do not switch until cron is reliable. If cron is broken, Update by Schedule means prices, stock and category assignments go stale on the storefront without any error.

Check cron first:

  • Confirm the crontab entry exists (bin/magento cron:install creates it) and runs as the same user that owns the codebase.
  • Query the cron_schedule table. Look for large numbers of missed jobs, or running jobs that started hours ago. A stuck job holds a lock and blocks everything behind it.
  • Check bin/magento indexer:status for indexers that need a reindex or have a growing backlog.

Once cron runs cleanly for a few days, switch the indexers on staging, run a normal day's imports and admin edits, check the storefront reflects them, and then make the same change in production.

One related note: flat catalog tables come up in older advice. Adobe's own guidance no longer recommends them, and on modern installs they tend to cause more indexing load than they save.

Audit third-party modules one at a time

Every module that adds a plugin around product loading, an observer on a high-traffic event, or an extra query per item in a collection adds cost to every request. bin/magento module:status lists what is enabled. Most stores have modules nobody remembers installing.

The profile data from the first step tells you where to look. Find the slow transactions, find which plugins and observers run inside them, and map those back to vendors. Then test on staging, one module at a time: disable it, run setup:upgrade and setup:di:compile, measure, and run through checkout. Never remove a module from production on a hunch. A module with its own database tables or EAV attributes can leave orphaned data or broken admin forms when you disable it, and some must be uninstalled properly rather than switched off.

The answer is often to replace two overlapping modules with one, or with a small piece of custom code that does only what the store needs.

Treat front-end weight as the last step

When time to first byte is low and stays low, look at what the browser does. Run Lighthouse on the same representative URLs and deal with the large items: product images served at full resolution, no lazy loading below the fold, and third-party scripts such as chat widgets, review widgets and tag managers loading synchronously.

Be cautious with Magento's built-in JS bundling. On many themes it produces very large bundles that block rendering for longer than the separate requests it replaced. Turn it on, measure, and turn it off again if the numbers get worse. Minification is usually safe. Merging and bundling need checking page by page, because a broken RequireJS dependency often shows up only in the minicart or checkout.

Know which fixes are safe to make yourself

A merchant or non-specialist tech lead can safely:

  • Check deploy mode, cache status and indexer mode, and record the baseline timings
  • Look at cron_schedule for missed and stuck jobs
  • List the installed modules and flag the ones nobody uses
  • Compress and resize product images, and run Lighthouse before and after

These need someone who works in Magento code and infrastructure:

  • Varnish configuration and cache-splitting rules
  • Removing cacheable="false" blocks and moving content to private sections
  • Switching indexer mode on a large catalog with live imports
  • Disabling or uninstalling modules that have their own schema
  • Database tuning, Redis setup and PHP-FPM sizing

What to do next

Take the baseline this week, even if you change nothing else. Then run the checks above in order: cache misses, then cron and indexers, then modules, then the front end. Stop at the first one that fails, because that is your bottleneck. If it falls in the developer column, our Magento 2 service covers this kind of operations work, and you can send us the baseline numbers to start the conversation.

Need this done on a real stack?

Magento 2, Adobe Commerce, migrations to Medusa.js or Vendure, enterprise Next.js, WordPress, and AI automation.

Contact us