Web Development

Our early approach to images on the first responsive builds

DEV

A couple of our recent client sites needed to work reasonably on the growing number of visitors browsing on phones and tablets, which meant rethinking how images behave, since nothing about a typical WordPress theme's image handling was built with a narrow viewport in mind.

The baseline fix

The baseline is just max-width: 100%; height: auto; on every content image, which stops layout breakage even if it doesn't reduce the amount of data downloaded. Without it, an image dropped into a post at its native pixel width will happily overflow a narrow container and break the whole page layout, forcing horizontal scroll on a phone, which is about the worst first impression a site can make on a mobile visitor.

That one CSS rule alone fixed the most visible breakage across every client site we applied it to, and it costs nothing, no JavaScript, no build step, just two lines in the stylesheet.

Going further for image-heavy projects

For a couple of image-heavy projects we went further and used a small JavaScript check on window width to decide which of two image sizes to request, a smaller version for anything under a certain viewport width, the full version otherwise. It's a rough workaround rather than a proper solution, there's no built-in browser mechanism for this yet as far as we've found, but it's noticeably better than serving a 1200px-wide photo to a phone and calling it done, both for how long the page takes to load and for how much data it costs a visitor on a mobile connection.

  • Detect window.innerWidth (or a jQuery equivalent for slightly older browser support) on page load.
  • Compare against a breakpoint, roughly matching where our CSS layout itself changes.
  • Swap the image src to a pre-generated smaller version if under that breakpoint, generated ahead of time rather than resized on the fly.

Why this feels unfinished

We're calling this a rough workaround deliberately, because it is one. Checking window width once on page load doesn't account for a visitor who resizes their browser or rotates their device after the page has already loaded, which means someone who loads a page in portrait and rotates to landscape can end up with an image sized for the narrower orientation even though they now have room for the larger one. We considered listening for a resize event and re-checking, but re-fetching a full-size image after the smaller one has already loaded felt like it could cost more in wasted bytes than it saves, so for now we're leaving that edge case unaddressed.

There's also no way to account for a device's actual pixel density with this approach, a viewport width check tells us nothing about whether we're dealing with a screen that would benefit from a higher-resolution image despite being physically narrow. We're aware of the tradeoff and living with it for now rather than trying to solve a problem we don't have good tools to solve yet.

What we're watching for

We keep hearing rumblings that browser vendors and standards groups are aware responsive images are an unsolved problem, and that something more built-in is likely coming eventually, some way to describe multiple image sources and let the browser pick the appropriate one based on its own knowledge of the viewport and pixel density, rather than us reimplementing that logic in JavaScript on every project. Until then, our JavaScript width check, imperfect as it is, remains the best tool we have for the image-heavy projects where bandwidth actually matters to the client's audience.

Deciding when it's worth the added complexity

We're not applying this JavaScript approach to every project, just the ones where it clearly matters, a photography portfolio, a real estate site with a lot of property photos, anything where images are a meaningful fraction of total page weight. For a typical brochure site with a handful of images, the max-width fix alone is enough, and adding the JavaScript layer would be complexity without a proportional benefit. Matching the amount of engineering to how much the problem actually costs a specific client's visitors has been a theme across a lot of what we've built this year, not just images specifically.

A practical note on generating the smaller versions

The smaller image versions aren't generated on the fly, we're pre-generating them at upload time using WordPress's built-in image sizes feature, registering an extra custom size specifically for this purpose alongside the standard thumbnail and medium sizes WordPress creates automatically. That keeps the actual runtime logic simple, just picking between two already-existing files rather than resizing anything dynamically, which would add real server load and complexity we don't want for what's ultimately a fairly modest problem.

What we'd tell someone building this today

If we were starting from scratch on a new project right now, we'd still reach for the same two-tier approach rather than trying to build something more sophisticated with several breakpoints and image sizes, since the added complexity of managing five or six generated sizes per image, and the JavaScript logic to pick between them, hasn't so far seemed worth it for the modest gain over a simple two-tier split on the projects we've tried it on. Two sizes, one breakpoint, has been enough to meaningfully cut mobile page weight without turning image handling into its own subsystem that needs ongoing maintenance.

Measuring whether any of this actually mattered

We ran a rough before-and-after comparison on one of the image-heavy projects, total page weight on a typical gallery page before the JavaScript swap versus after, on a connection profile meant to approximate a mid-range mobile visitor. The swap cut total image weight on that page by more than half, which translated into a meaningfully faster time-to-usable-page on the throttled connection, even though the desktop experience on a fast connection looked and felt identical either way, exactly as intended.

That kind of before-and-after measurement, however informal, is something we're trying to build into more of our decisions this year rather than assuming a technique works because it sounds reasonable in theory. It's easy to add complexity in the name of performance and never actually confirm the complexity bought anything.

What this means for how we scope future projects

We're now asking, as part of our initial client questionnaire, roughly how image-heavy a proposed site will be and whether the client's own audience is likely to include a meaningful share of mobile visitors on slower connections, rather than treating image handling as an afterthought decided during development. Surfacing that question early means the decision about whether to invest in this kind of responsive image handling gets made deliberately and priced into the project, rather than getting bolted on halfway through once someone notices a gallery page feels sluggish on a phone.

What we'd change if we were starting today

If we were setting this up fresh rather than adding it partway through an existing project, we'd build the breakpoint check as a small reusable snippet from day one, rather than the slightly ad-hoc, per-project version we wrote the first time and have since copy-pasted with minor tweaks into every subsequent project that needed it. It works fine as is, but the small inconsistencies between each copy, one project checks 600px, another checks 640px, purely because whoever wrote that particular version picked a slightly different number, are exactly the kind of drift that a shared snippet would prevent. That's a small housekeeping item on our list rather than an urgent one, since the inconsistency costs us nothing functionally, it just makes the codebase between projects slightly less predictable than it could be, and predictability across projects is exactly the kind of thing that pays off later when either of us is the one debugging a project the other one built, rather than the one who wrote it originally. We've put "write the shared snippet" on the list for the next slow week, filed alongside a handful of other small consistency cleanups that never feel urgent enough to bump an actual client deadline. We would rather ship the imperfect two-tier version now and revisit it once a client project actually demands finer-grained control, than delay shipping any improvement while waiting for a more elegant solution to arrive. It is a small enough gap in the current approach that we are comfortable living with it for another few projects before deciding whether it is worth the rebuild.

← Back to the journal

Have a project in mind?
Let’s talk.

Tell us where you are and where you want to go. We'll map the fastest route between the two.

Currently accepting new clients