We helped migrate 47,000 images on a WordPress website to improve performance
In our experience, some of the biggest performance problems on WordPress sites are not dramatic bugs. They are architectural choices made years ago, decisions lost by changing developers, and only discovered during auditing – quietly impacting sales, conversions or producing a poor user experience.
In a recent WordPress improvements project, we found exactly that: a website with more than 47,000 images, where the pages looked “fine” on desktop, but mobile load times were heavy, Core Web Vitals were quite terrible, and the experience was starting to affect engagement. What we uncovered was surprisingly simple.
WordPress is excellent at serving the right image size to the right device, but only when it can correctly identify an image as a Media Library attachment.
On a healthy setup, images are rendered using attachment IDs. That allows WordPress to:
- generate
srcsetandsizesautomatically - serve smaller images to mobile devices
- lazy-load consistently
- add alt text descriptions for accessibility
On this site, the previous implementation had stored full image URLs directly in the database instead of attachment IDs.
That small choice caused a big consequence:
WordPress could not reliably associate those URLs with Media Library attachments, so it could not output responsive image markup and we lost all of the benefits listed above.
The result was that mobile visitors were often downloading full-size images even when smaller versions existed. Accessibility was poorer as the client had no ability to add alt text.
The site looked fine, you wouldn’t have known until you looked at the code.
Oversized images on mobile create a cascade of knock-on effects:
- Worse Core Web Vitals, especially slower Largest Contentful Paint (LCP)
- More data downloaded, which increases page weight and load time
- Lower conversion rates, as delays increase the likelihood of abandonment
- Higher energy usage, because bigger transfers require more resource to deliver
If you have ever tried to load a heavy website while travelling across London on an inconsistent 4G signal, you will know how quickly patience runs out. Performance is user experience, and user experience affects outcomes – lower sales, fewer leads, people abandoning your website after a few seconds.
So the goal was clear – migrate URL-based image references to proper attachment IDs, so WordPress could do its job properly again.
Even at 30 seconds per image, manual migration would have been completely unrealistic and error-prone.
More importantly, bulk updates done poorly can:
- time out mid-run
- spike CPU and database load
- break content on edge cases
- disrupt visitors during peak usage
This is where good WordPress maintenance practices matter. The safest solution is usually the one that is controlled, resumable, observable, and reversible.
For this type of work, we typically choose WP-CLI. We wrote a custom command tailored to the site’s content structure and business logic, designed to run in controlled batches.
At a high level, the process was:
- Scan post content and custom fields for image URLs
- Resolve each URL to an attachment ID
- If the attachment did not exist, sideload it into the Media Library
- Update content/meta to store IDs instead of URLs
- Record progress so the migration can resume safely
Background processing with Action Scheduler
The client already used WooCommerce, so Action Scheduler was available and proven in their environment. That made it a strong fit for running work in batches without impacting visitors.
It also gave us operational visibility:
- which posts were being processed
- what succeeded, what failed
- retries and error handling where appropriate
In WordPress, attachment_url_to_postid() is the obvious first step, but real sites rarely match perfectly.
We had to handle variations like:
- resized filenames (e.g.
image-300x200.jpg) - query strings from CDNs
- historical GUID mismatches
- inconsistent hostnames across environments
So the migration used a layered approach:
- normalise URLs (including removing query strings when needed)
- detect resized variants and prefer the original full-size file
- fall back to a direct lookup when necessary (carefully, and only when required)
We spent a few days testing and refining the script as we found edge-cases that broke the process.
Some images were referenced in the database but were not registered as Media Library attachments at all – they were stored in a directory outside of WordPress too.
To make WordPress generate intermediate sizes and metadata (so responsive images can work), sideloading is the correct approach.
Here’s the core pattern (trimmed to what matters):
That step matters because it ensures WordPress generates all required sizes and stores attachment metadata. Without that, you do not get proper responsive image output.
With migrations like this, “it worked on staging” is not enough.
We designed the migration to be:
- batched (fixed number of records per run)
- resumable (progress recorded per post/item)
- idempotent (avoid duplicate work)
- observable (logging and failure reporting)
We started with a tiny batch (5 posts), manually reviewed results, and checked that:
- images migrated correctly
- galleries retained the correct order
- no layouts were affected
Once verified, we increased batch size gradually while watching server metrics. The goal was simple: finish the job without the user ever noticing it was happening.
After migrating URL-based references to attachment IDs, the site could render images via core functions like wp_get_attachment_image(), which immediately unlocked:
- responsive
srcsetoutput - appropriate sizes for mobile
- better LCP characteristics
- cleaner long-term maintainability
Most importantly, the client gained a stronger foundation for ongoing WordPress improvements and WordPress maintenance, rather than carrying hidden performance debt forward.
If you suspect a similar issue, look for these indicators:
- mobile pages downloading unusually large image files
- images output without
srcset/sizesattributes - custom fields storing raw image URLs rather than IDs
- page builders or legacy tooling bypassing WordPress media functions
A targeted audit can confirm this quickly, often in under a day.
How Branch helps
At Branch Agency, we deliver proactive, expert-led WordPress improvements for business-critical websites, alongside ongoing WordPress maintenance and WordPress support.
Our aim is simple: reduce risk, improve performance, and make your site easier to evolve without breaking things.
If you suspect hidden performance debt, this is exactly the kind of problem we solve properly.
Find out if images are hurting your Core Web Vitals
If your site is serving oversized images on mobile, performance and conversions suffer. We’ll confirm it and fix it safely.
Book a discovery call