The problem

WordPress could not generate responsive images

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 srcset and sizes automatically
  • 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.

Why this matters for performance and Core Web Vitals

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.

Migrating 47,000 images is not a manual task

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.

Our approach

A WP-CLI migration that runs safely in batches

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:

  1. Scan post content and custom fields for image URLs
  2. Resolve each URL to an attachment ID
  3. If the attachment did not exist, sideload it into the Media Library
  4. Update content/meta to store IDs instead of URLs
  5. 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

Resolving URLs to attachment IDs

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.

When the image did not exist as an attachment

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):

// Download the image to a temp file
$tmp = downloadUrl($url);
$file_array = [
‘name’ => sanitize_file_name(wp_basename(parse_url($url, PHP_URL_PATH))),
‘tmp_name’ => $tmp,
];

// Create attachment and generate sizes/metadata
$attachmentId = media_handle_sideload($file_array, $postId);

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.

How we kept it safe on production

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.

The outcome

Faster pages and a healthier media foundation

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 srcset output
  • 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.

What to check on your own site

If you suspect a similar issue, look for these indicators:

  • mobile pages downloading unusually large image files
  • images output without srcset / sizes attributes
  • 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.

Next step

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