WooCommerce Store Migration & Performance Overhaul

Pexels diverzant

A specialty e-commerce store selling sheet music and digital downloads had been converted from a legacy e-commerce platform by an outside agency. The agency handled everything: hosting, plugin updates, custom development work, backups. The client paid monthly and trusted that the technical side was running smoothly.

Except that it wasn’t. Issues from the initial launch (which, was almost a year late) lingered months after the site went live.

The goal was simple: take ownership. Move the site to hosting the client controls, document everything, remove the agency’s fingerprints, fix the glaring issues, and end up with a stack that any qualified WordPress developer could walk into and understand.

The Initial Look

Before touching anything, I spent time poking around the live site to get a sense of what we were dealing with. Red flags were visible right away.

The child theme’s main functions file was over 500 lines of business logic that had nothing to do with theming. WooCommerce order hooks, a third-party fulfillment API integration, a complete download-by-access-code system, email template overrides. None of it documented. The monthly bill included a $200/month search service that the agency had built custom plugins around (which, by the way, weren’t fully working). Response times felt sluggish, but nothing pointed to an obvious cause. There was clearly more going on under the hood than what the WordPress admin was showing.

The site worked, but it was a black box. The only way to really understand what was happening was to clone it somewhere I could dig without risking production.

Setting Up the Test Environment

I set up the new server as a test subdomain and rsync’d all the files over. The initial sync took several hours for 50+ GB of downloadable products, but that was fine since it ran in the background. While the files transferred, I exported the database from production, imported it on the new server, and ran the necessary search-replace operations to update URLs.

Once the clone was running, I had a safe environment to audit, break things, and fix them without affecting live orders.

What the Audit Uncovered

With the test site up, I could dig into everything. The deeper I went, the more I found.

The agency had white-labeled plugins, making them invisible in the WordPress admin. A backup plugin was active in the database but filtered itself out of the plugins screen. It was phoning home to the vendor’s servers on every single page load, invisible to anyone checking the admin. A Mailchimp integration plugin was running 18 database queries per page load unnecessarily just for option lookups. Orphaned database tables from tools that were no longer even active were sitting in MySQL.

The search system was the expensive one. The agency had built two custom plugins that rendered the entire product grid client-side via JavaScript. Every time a customer searched or filtered products, the browser made an API call to the third-party search service, received JSON, and built the product cards in JavaScript. This bypassed all of WordPress and WooCommerce’s normal hooks, which meant every plugin that needed to interact with products had to be manually re-implemented in custom JavaScript. The wishlist plugin needed custom handlers. The audio player plugin needed custom initialization. Variation selectors needed manual wiring.

I found three bugs in the search implementation just by reading the code. Wishlist states weren’t displaying correctly on page load because the JavaScript (from the agency’s custom search plugin) checked for variation IDs before the variation selector had resolved. A CSS class was being stripped during event handling, causing layout shifts. And URLs were being double-encoded, breaking some button clicks.

Performance problems stacked up. The OPcache settings had been misconfigured to a file limit of 1,000 files, way too low for a WooCommerce site with over 1,600 scripts across 40+ plugins. PHP was recompiling hundreds of files on every request instead of serving cached bytecode. The Nginx page cache wasn’t working at all because WooCommerce was setting session cookies on anonymous visitors, which triggered a cache bypass rule. An SEO plugin was making loopback AJAX requests on every admin page load, triggering a full WordPress bootstrap just to check if a preview should be rendered.

The child theme functions file had bugs too: customer names getting mangled in download emails due to a misused PHP function, access codes that could be redeemed unlimited times because nobody had written the deactivation logic, and hardcoded API credentials that should have been hidden away elsewhere.

Every one of these issues added up. The site wasn’t slow in an obvious way, but it was consistently slower than it needed to be, on every request, for every visitor.

Fixing Everything on the Test Site

With the audit complete, I worked through the issues one by one.

OPcache got bumped from 1,000 to 20,000 files, and the baseline response time dropped immediately. Page editor loads went from 1.7 seconds to 1.0 seconds per request, and that multiplied across every REST API call the block editor makes.

I wrote an mu-plugin that prevents WooCommerce from initializing sessions unless the visitor is logged in, on the cart or checkout pages, or has items in their cart. Anonymous browsing now gets fully cached responses at the Nginx layer, served in milliseconds with zero PHP execution.

The hidden backup plugin got deactivated, dropping page load time by about 200ms. GridPane (the new hosting platform) handles onsite and offsite backups natively, so there was no reason to keep a third-party backup service that they had no access to, running queries on every page load. The Mailchimp integration got deactivated too, saving another 370ms; the client was already planning to switch to a different email marketing service, so this was a good time to cut it loose.

The SEO plugin’s loopback requests got short-circuited at the mu-plugin level: if the request has the specific action parameter, return a 400 immediately before WordPress even loads. That turned a 4-second request into 0.007 seconds.

The wishlist plugin was loading its JavaScript in the admin, making REST API calls that had no business running in that context. Dequeuing those scripts on admin pages eliminated another unnecessary request per page load.

I applied all pending plugin updates and let their database migrations run. I documented the child theme functions file, flagging it for eventual migration into a proper plugin. All the fixes went into mu-plugins with clear names and comments explaining why they exist.

Throughout this process, production kept running. Live orders kept flowing. The test site was days ahead in terms of plugin versions and fixes, which created a problem for the final cutover: the database schemas had diverged.

The Cutover

I couldn’t just pull a fresh database copy from production on cutover day because that would revert all the schema changes from the plugin updates. Instead, I wrote a custom export/import script pair that worked through WooCommerce’s data layer. Export orders from production after a cutoff date, transfer the JSON, import into the test environment. The scripts tracked what had been imported using custom meta fields, so running them multiple times was safe. Modified orders got updated, new orders came in with ID offsets to avoid collisions, and download permissions synced correctly across the board.

On cutover day, I scheduled the work for late Friday night when traffic was at its weekly low. I put production in maintenance mode, ran one final order sync, verified everything on the test environment, and swapped DNS. The whole process took about an hour from maintenance mode on to the new server going live.

The Search System Still Needs Replacing

The $200/month search service is still running. Replacing it is the immediate next project, but it couldn’t happen during the initial migration without blowing the scope and timeline. Sometimes you have to get the foundation solid before you can rebuild on top of it. The client needed to be on hosting they control with a stable, documented codebase before we could start ripping out the search system and replacing it with something better.

The good news is that I’ve already done the benchmarking. MySQL’s FULLTEXT search can do what the external service does, and it can do it fast. A keyword search against ~900 products runs in about 2.5 milliseconds. Add a taxonomy filter and it’s 11ms. Add multiple filters and it’s still under 70ms in the worst case. Facet counts for all seven filter dimensions together: about 110ms. Those results can be cached in Redis, bringing subsequent lookups down to 0.3ms.

The architecture I’ve spec’d out makes WooCommerce’s built-in rendering fast instead of bypassing it entirely. A custom REST endpoint receives the search query and filters, checks Redis for cached results, hits MySQL FULLTEXT on a miss, and renders the results through WooCommerce’s standard product loop. Because it goes through the normal loop, all plugins work natively with zero custom JavaScript overrides and no external API dependency.

That project is next on the list. When it ships, the client drops a $2,400/year recurring cost and gets better plugin compatibility in the process.

Taking Ownership

The client now has a WooCommerce store on hosting they control, with full documentation of every custom piece. The previous agency’s hidden plugins are gone. The monolithic functions file is documented and flagged for migration into a proper plugin. Performance issues that were silently costing response time on every request have been fixed.

Page loads that were running through PHP on every visit now get served from the Nginx cache in single-digit milliseconds. Admin page edits that were spawning 11 concurrent PHP requests at 1.7 seconds each now run 8 requests at 1.0 second each. The order sync scripts that I built for the migration are reusable tooling if they ever need to do something similar again.

There are two things worth noting from this project. First, agency relationships can create hidden dependency. When someone else controls your hosting, your backups, your codebase, and your vendor integrations, you don’t always know what you’re paying for or what’s actually running on your site. An audit is worth the time. Second, performance issues often aren’t a single cause. This site had at least six separate issues that each cost tens to hundreds of milliseconds per request. Individually they’re easy to dismiss. Together they’re the difference between a site that feels snappy and one that feels sluggish. Fixing them required looking at every layer: PHP configuration, database queries, plugin behavior, cookie handling, and caching infrastructure.

The site is faster, cleaner, and most importantly, on hosting owned by the people who should own it.