Skip to content
crawlforgeEspañol
It broke6 August 20269 min readLeer en español

Your audit tool is not seeing your images

A news site with 18,000 pages of photos reported zero heavy images. The table was empty. The cause is a lazy-loading plugin that half of WordPress runs.

We were going through the report of a 20,000-URL crawl of a news site and something did not add up. ASSET-IMG-HEAVY, the rule that flags images over 200 KB, returned zero findings.

A digital newspaper. Fifteen years of publishing articles with photos. Zero heavy images.

We went to the database directly:

SELECT COUNT(*) FROM images;
-- 0

Not one row. In the same crawl, 20,000 URLs and 18,000 HTML pages, and the images table empty.

What was happening

The site runs LiteSpeed Cache, one of the most widely installed WordPress optimisation plugins. Among other things it lazy-loads images, and to do that it rewrites the HTML like this:

<img data-lazyloaded="1"
     src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDgwMCI+PC9zdmc+"
     data-src="https://example.com/wp-content/uploads/2026/07/photo.jpg"
     alt="Article caption">

The src attribute, which is where everyone looks, holds a transparent SVG encoded in base64. The real URL sits in data-src, and the browser moves it into src when the image approaches the viewport.

Our parser read src, found a data: URI, discarded it as not being a crawlable URL, and moved on. Correctly, in fact: a data: URI is not an image you can request from a server. The problem is that it did not look for a real URL sitting next to it either.

What gets lost

Everything that depends on knowing the image:

  • Weight. You do not know you are serving a 1.9 MB JPEG on the home page.
  • Status. An image returning 404 shows up nowhere.
  • Format. You cannot say how many are still JPEG when they could be WebP.
  • Count. The report says the site has no images.

And the part that annoyed us most: we were reading alt correctly, because the plugin leaves that attribute where it was. So we were dutifully reporting images with no alternative text, on a site we believed had no images. The report contradicted itself and nobody had noticed.

The fix

When src is a data: URI, look for the real URL in the attributes lazy-loading plugins use. In order of how common they are:

  1. data-src — LiteSpeed Cache, lazysizes, a3 Lazy Load, Smush, EWWW
  2. data-lazy-src — WP Rocket, Jetpack Lazy Images
  3. data-original — the classic jQuery lazyload, still alive on older sites
  4. The first candidate in data-srcset

We did not take that list on faith and went to check: we downloaded the home page and an article from the real site. LiteSpeed emitted data-lazyloaded="1" with data-src and data-srcset on 45 of the 51 images on the home page.

We also decided not to read <noscript>. Many plugins leave a copy of the original <img> in there as a fallback for browsers without JavaScript, and it looked like the more reliable source. Two reasons against it: the pages we measured had none, and on sites that do emit it, reading it would duplicate every image and force us to deduplicate afterwards.

The result

Same site, a 300-URL crawl with the fix in place:

SELECT COUNT(*) FROM images;
-- 5945

And on a full crawl, the rules that had been silent for months:

high    ASSET-IMG-BROKEN     2,193
medium  ASSET-IMG-HEAVY     43,980

Forty-three thousand images over the threshold and two thousand returning errors. On a news site that is probably the highest-impact finding in the whole audit, and it had been invisible from the start.

One of the ones that turned up:

https://example.com/wp-content/uploads/2026/07/segunda-rfef-grupo-v.png
  1,927,124 bytes  ·  limit 204,800  ·  used on 1 page

Almost two megabytes in a PNG that could be a 150 KB WebP.

What to do when your images are heavy

Finding them is half of it. The other half is the report telling you where to start, and there the count of pages using each image matters more than its weight:

crawlforge report crawl.sqlite --rule ASSET-IMG-HEAVY

A 1.9 MB image used on one post from three years ago is not urgent. The same image in the template header, served on every page, is the first thing to touch. Our detail includes used_by_pages for exactly that reason.

The order we follow: template images first, then home and category pages, and the archive last, which rarely pays off unless you have an automated process.

How to tell if this is happening to you

Whatever tool you use. Crawl a site of yours that lazy-loads images and look at the image count. If it comes back suspiciously low, or zero, you have the same problem.

The quick check against your own HTML:

curl -s https://yoursite.com/ | grep -c 'data-src='

If that returns a high number and your audit says the site has few images, you know where to look.

What we took from it

No unit test would have caught this, and none did: we had 810 of them passing. The image fixtures we had written used <img src="/photo.jpg">, like the examples in the books.

Real HTML does not look like the examples in the books. It has plugins on top.

There is a second part we found afterwards, and it is expensive: once we started reading the images, the table went from zero to 4.4 million rows, and that uncovered a missing index in the engine. The final pass of a large crawl went to over eight hours. That deserves its own article.

Built in the open

Every two weeks: measurements, defects, worked examples. Nothing else.

← Back to the devlog