SEO Image Optimization That Doesn’t Destroy Quality
Serving Proper Image Dimensions (No, Your Retina Screen Isn’t Special)
So a friend sent me a site where all the hero images were crisp on desktop — until you scroll. The rest? Blurry. Turns out whoever built the template dumped full-size 4K images into a slider, and the lazy loader was doing a sad downscale on the fly. Looks fine at first. Looks garbage while scrolling.
Rule of thumb: serve images in the actual size they’ll be displayed at, or just slightly bigger for retina support (about 2x max). That doesn’t mean you need 3840px wide assets unless it’s spanning a huge 4K display edge to edge. Use srcset
properly and let the browser negotiate.
<img
src="image-800.jpg"
srcset="image-400.jpg 400w, image-800.jpg 800w, image-1600.jpg 1600w"
sizes="(max-width: 600px) 400px, 800px"
alt="Something descriptive">
That little srcset
gymnastics? That’s how you feed different device pixel ratios without torching quality or bloating bandwidth. But don’t go overboard. I once tried a nine-entry srcset
on a WordPress blog — Chrome choked and Googlebot threw warnings for slow rendering.
TinyPNG Isn’t Magic — Do Your Preview Homework
TinyPNG and ImageOptim are great, but they’re not perfect. Once, TinyPNG obliterated the whites in a product image — enough that it passed WCAG contrast but failed the human eye test. Background went from warm white to ghost gray. Best practice? Always compare the optimized versus original images in context, not just side by side. Preview *in layout* — preferably across devices.
Also: never trust the WordPress media library preview. The way WordPress handles thumbnails is a whole thing — has bitten me on the ass more than once. It’ll show you a compressed version of a compressed file if your theme filters sizes. That’s like checking printer ink levels by licking the page.
Keep original versions of everything. Not just for rollback, but because I’ve had ImageMagick plugins fry alpha channels when optimizing PNGs in batch. Undocumented randomness? You bet. Fail gracefully by versioning manually, or at minimum, hashing your assets before batch-transforming.
Alt Text That Doesn’t Sound Like a Bot Wrote It
If your image alt tags read like you’re trying to brute-force every niche keyword variant into a stock photo of spaghetti, step back. This isn’t 2010. Also: Googlebot actively ignores most alt content when it smells keyword stuffing — and yes, it prioritizes page context now. Not just the tag.
- Avoid filenames like
seo-strategy-best-guide-2023.jpg
unless you enjoy looking like a link farm. - Use natural phrasing, e.g., “Close-up of a server rack under red ambient light.”
- If it’s decorative, leave it blank
alt=""
and setrole="presentation"
.
Had someone tag every CTA button icon with detailed alt descriptions, which made screen readers lose their minds: “Star icon for quality, underline icon representing a call to action…” Imagine hearing that each time your focus hits a new link. Don’t do this to users with screen readers.
WebP Isn’t a Set-and-Forget Win
WebP is great. It’s also still quietly broken in some edge cases. The biggest problem I ran into recently? Safari 13 on older macOS builds rendering transparencies as black boxes. Client had a translucent overlay behind product images. Looked fine on Chrome. Dead-zoned on older Safari builds. Took us way too long to realize it wasn’t z-index — it was the file format.
WebP does reduce size dramatically, but it’s not always worth it. If you’re modern but serving enterprise weirdos (read: government, insurance, or schools still on IE forks), sanity check with Accept
headers. We use NGINX to conditionally serve WebP assets based on user-agent + header combo. Cloudflare also does this upstream with zero-config — their Polish feature just rewrites silently where compatible.
Cloudflare makes it a bit too easy though — it’ll happily serve you WebP versions even when your custom JS thumbnail renderer can’t handle it, and that breaks preview loading real fast. We had to nuke Polish on one subdomain just to restore media previews in an Angular widget. Nobody documents that.
Image Delivery via CDN: Race Conditions DO Happen
One week our bounce rate spiked. Devs swore nothing had changed. Turns out the image CDN had quietly switched to an origin access policy that delayed cold cache fetches by nearly two seconds in EU regions — only for JPGs uploaded via the new API. This was technically within SLA, but UX death in practice.
The fix wasn’t documented. We had to force preload headers for LCP images and use a staging pipe that warmed CDN edges hourly. Also enabled stale-while-revalidate so subsequent users weren’t penalized. But it never showed in Lighthouse until cache expired for that asset. We only caught it by throttling edge fetches manually in DevTools.
Five dumb little tweaks that helped:
- Serve LCP images directly via root path URLs; reduce redirect time.
- Add
preload
hints for hero banners or anything above the fold. - Avoid using JS to insert image tags — delays parsing and indexing.
- Double-check
cache-control
headers are overridable per asset. - Don’t serve next-gen formats blindly — run CanIUse checks monthly.
Honestly, having multiple CDNs sounds smart on paper until you spend 40 minutes explaining to a client why images are fine in South Carolina but busted in Belgium.
Lazy Loading Conflicts with Third-Party Scripts
Google pushes lazy loading like it’s a religion now. And yeah, it’s great for largest contentful paint and keeping initial payload light. What they don’t mention? Some third-party scripts (I’m glaring at older Pinterest taggers and some chat widgets) race against IntersectionObserver
and fire off before the image even exists in DOM.
I had social sharing plugins trying to grab open graph images from placeholders. Result: shares looked broken on Facebook. Had to add a noscript fallback *and* insert dummy meta tags that updated via JS — total hack, but it worked. Also, note this:
Chrome considers the image inside a carousel not visible unless the entire swiper container is in viewport — even if image itself is partially shown. That broke our expectations for lazy loading behavior.
There’s also a weird Chromium behavior where lazy loading + loading="lazy"
can delay LCP count if decoding="async"
is used on huge JPEGs inside shadow DOM mounts. That one took me days to spot because I didn’t think to flatten Web Components during testing.
Filenames Are Metadata — And Google Uses Them
Look, it’s not about humans. Google legitimately uses image filenames to understand image context in image search rankings. We’ll never know how weighty the signal is, but I’ve seen ranking shifts after renaming generic files (like “IMG_49013.jpg”) to product-relevant names. Uploading “blue-canvas-sneakers-side.jpg” pushed that image to the sidebar of Google Images within days — after months of nothing.
But the gotcha? If your CMS renames uploaded files (yes, Shopify, I’m looking at you), your original filename gets zero SEO love unless it also lands as the alt
or in a nearby caption. WordPress users: watch for duplicate file URLs with weird numeric suffixes. That’s a quietly penalized practice, and Google may exclude one of them entirely from indexing.
Structured Data Helps — But It’s Finicky as Hell
Add ImageObject
to your schema markup and you’ll see a lift — if, and only if, the image is also rendered visibly and is not excluded by robots.txt. (Yes, this includes the /wp-content/uploads/
folder if you’re blocking it via .htaccess to save on bandwidth. That’ll kneecap your rich results real fast.)
I learned this the hard way when we had schema implemented correctly, but Google’s Search Console kept marking images as “missing”. Turned out the CDN rewriter plugin was obfuscating the path, so the schema URL didn’t match the real final URL after client-side render. Resolution? Include a real-world img tag somewhere on-page referencing the canonical image path. Theory meets practice, Google-side.
{
"@type": "ImageObject",
"url": "https://example.com/images/widget-hero.jpg",
"width": 1200,
"height": 675
}
If you’re using JSON-LD, keep image path canonical, not CDN alias unless your CDN supports canonical rewrites on edge headers. Most don’t.
EXIF Data: Strip It Unless You Actually Need It
This one’s easy: EXIF data bloats files and can leak stuff. One time I uploaded a test shot from my DSLR without stripping EXIF, and the client freaked out because internal location metadata was included — that image ended up online. We scrambled to purge the cache across three CDNs and Google index. Clumsy, preventable mess.
Use tools like exiftool or batch scripts via ImageMagick to nuke metadata pre-upload. Basic bash one-liner on Mac:
exiftool -all= *.jpg
Only keep EXIF on photography portfolios where camera/lens/ISO info actually matters. Don’t trust your CMS to strip it automatically. WordPress sometimes retains EXIF locations even if you crop and re-upload the image.