DEV Community

Lacey Glenn
Lacey Glenn

Posted on

Optimizing Image-Heavy Travel Listings for Mobile: Lazy Loading, CDNs, and Compression

Optimizing Image-Heavy Travel Listings for Mobile: Lazy Loading, CDNs, and Compression

If you've ever built a travel app, you already know the uncomfortable truth: travel listings live and die by their photos. A hotel listing with three blurry, slow-loading images converts worse than one with a dozen crisp, fast ones — but a dozen crisp images is exactly what tanks your mobile performance if you don't handle them correctly.

This is the tension every travel app team runs into. Users want a rich, photo-heavy browsing experience — swiping through hotel rooms, destination galleries, and activity photos. But mobile users are also on inconsistent connections, limited data plans, and battery-conscious devices. Get the image pipeline wrong and you'll watch your bounce rate climb every time someone opens a listing page on 4G in an airport.

This post walks through the three levers that actually move the needle: lazy loading, CDN delivery, and compression — plus a few things travel apps specifically need to get right that generic e-commerce advice doesn't cover.

Why travel apps are a uniquely hard image problem

Most "optimize your images" articles are written with e-commerce product photos in mind — a handful of images per product, mostly static. Travel apps are a different animal:

  • Volume is higher. A single hotel listing can have 30–100+ photos across rooms, amenities, and views.
  • Galleries are the primary UI, not a secondary feature. Users expect to swipe through dozens of images per listing, not just view a thumbnail.
  • Connectivity is unpredictable by design. Your users are, definitionally, traveling — airport wifi, roaming data, spotty hotel connections, and countries with slower average bandwidth are all normal use cases, not edge cases.
  • Trust and safety intersect with images. Users are making real financial decisions (booking a stay, paying a deposit) based partly on what they see, which means image integrity and how you serve them also becomes part of how you secure a travel app — verifying that listing images haven't been tampered with, swapped, or served from a compromised source matters just as much as making them load fast.

With that context, let's get into the actual techniques.

1. Lazy loading: only load what's actually visible

The single highest-leverage change you can make is simple: stop loading every image on a page the instant it renders. If a listing has 60 photos and a user only scrolls through the first 8 before booking, you've wasted 87% of that bandwidth and load time.

Native lazy loading (the easy win)

Modern browsers support native lazy loading with a single HTML attribute:

<img
  src="hotel-room-1.jpg"
  loading="lazy"
  alt="Deluxe room with ocean view"
  width="800"
  height="600"
/>
Enter fullscreen mode Exit fullscreen mode

This defers loading any image until it's within a calculated distance of the viewport. It's supported broadly across mobile browsers at this point, and it costs you nothing to add — always include the width and height attributes so the browser can reserve layout space and avoid content jumping around as images load (this also helps your Cumulative Layout Shift score, which matters for both UX and SEO).

Intersection Observer for finer control

Native lazy loading is great, but for travel-specific UI — like horizontal swipeable galleries where "visible" doesn't map cleanly to vertical scroll position — you often want more control. The Intersection Observer API lets you trigger loading based on custom thresholds:

const galleryImages = document.querySelectorAll('.gallery-image[data-src]');

const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        const img = entry.target;
        img.src = img.dataset.src;
        img.removeAttribute('data-src');
        observer.unobserve(img);
      }
    });
  },
  { rootMargin: '200px 0px', threshold: 0.01 }
);

galleryImages.forEach((img) => observer.observe(img));
Enter fullscreen mode Exit fullscreen mode

The rootMargin: '200px 0px' is the key detail here — it starts loading images 200px before they enter the viewport, so by the time a user swipes to them, the image is already there. Tune this value based on your average swipe speed; too small and users will see loading spinners, too large and you're back to over-fetching.

Preload the next likely image, not just the current one

For gallery-style browsing specifically, a pattern that works well: eagerly load the current image and the next one in the sequence, lazy-load everything else. This gives you the perceived performance of "everything's ready" without the cost of loading all 60 photos upfront.

function preloadAdjacent(currentIndex, images) {
  const nextImage = images[currentIndex + 1];
  if (nextImage) {
    const img = new Image();
    img.src = nextImage.src;
  }
}
Enter fullscreen mode Exit fullscreen mode

2. CDNs: get the bytes closer to the user

Lazy loading solves when you fetch an image. A CDN solves how far those bytes have to travel. For a travel app specifically, this matters more than almost any other app category — your users are, by definition, geographically distributed and constantly changing location.

Why origin servers alone don't cut it

If your images are served from a single origin in, say, us-east-1, a user browsing hotel listings from Southeast Asia is going to feel that latency on every single image request. Multiply that by a gallery of 40 photos and you've got a genuinely bad experience, even if each individual image is well-compressed.

A CDN caches your images at edge locations around the world, so that Southeast Asia-based user is pulling from a node in Singapore or Tokyo instead of Virginia. The practical difference is often the gap between images "popping in" instantly versus a visible, frustrating load delay.

Use an image-specific CDN, not just a generic one

Generic CDNs cache and serve files. Image-specific CDN services (Cloudflare Images, Cloudinary, imgix, and similar) go further — they do on-the-fly transformation, format conversion, and resizing at the edge, based on URL parameters. This means you can serve one source image and request different variants dynamically:

https://your-cdn.com/hotel-photos/room-1.jpg?w=400&format=auto&quality=80
Enter fullscreen mode Exit fullscreen mode

That single query string can return a WebP or AVIF image (see below), sized exactly to the device requesting it, without you having to pre-generate every possible size and format combination yourself.

Signed URLs for user-uploaded content

Travel apps increasingly let hosts or users upload their own listing photos, which introduces a real security consideration. If you're serving user-generated images, use signed, time-limited URLs from your CDN rather than permanently public ones — this prevents hotlinking, unauthorized scraping of your listing photos, and reduces the attack surface if a listing's image storage bucket configuration is ever misconfigured. It's a small step, but it's part of the broader discipline required to secure a travel app end-to-end, not just its payment and booking flows.

3. Compression and modern formats: shrink the bytes themselves

Even with perfect lazy loading and CDN placement, an unnecessarily large image file is still an unnecessarily large image file. This is where format choice and compression settings do the heavy lifting.

Ditch JPEG as your default

JPEG has been the default for two decades, but modern formats beat it meaningfully on file size at equivalent visual quality:

  • WebP — roughly 25–35% smaller than JPEG at comparable quality, with broad mobile browser support at this point.
  • AVIF — often 20–30% smaller still than WebP, though encoding is slower and support, while growing quickly, is slightly less universal than WebP.

The safest approach is to serve AVIF where supported, fall back to WebP, and fall back to JPEG as a last resort — which the <picture> element handles cleanly:

<picture>
  <source srcset="room-view.avif" type="image/avif" />
  <source srcset="room-view.webp" type="image/webp" />
  <img src="room-view.jpg" alt="Room with balcony view" loading="lazy" />
</picture>
Enter fullscreen mode Exit fullscreen mode

Responsive images: stop sending desktop-sized files to phones

A shockingly common mistake in travel apps: serving the same 2400px-wide hero image to a 375px-wide phone screen. The srcset attribute lets the browser pick the right size automatically:

<img
  src="hotel-hero-800.jpg"
  srcset="
    hotel-hero-400.jpg 400w,
    hotel-hero-800.jpg 800w,
    hotel-hero-1200.jpg 1200w,
    hotel-hero-1600.jpg 1600w
  "
  sizes="(max-width: 600px) 100vw, 50vw"
  alt="Hotel exterior at sunset"
  loading="lazy"
/>
Enter fullscreen mode Exit fullscreen mode

If you're using an image CDN with on-the-fly resizing, you don't need to pre-generate all these variants manually — the CDN can do it per-request based on the same w= parameter pattern shown earlier.

Set a real quality ceiling, and test it

Most teams either over-compress (visible artifacts on hero images that matter for conversion) or under-compress (wasted bytes on thumbnails nobody zooms into). A practical rule of thumb for travel listings: quality 80–85 for hero/gallery images where visual fidelity drives booking decisions, and quality 60–70 for thumbnails and preview grids where images are small and users are scanning rather than evaluating detail.

Putting it together: a realistic pipeline

For a travel listing page, a solid end-to-end setup looks like this:

  1. Original high-resolution images are uploaded and stored in object storage (S3, GCS, etc.), never served directly to users.
  2. An image CDN sits in front of storage, generating resized, format-converted, and compressed variants on request, cached at edge locations globally.
  3. The frontend requests appropriately sized images via srcset, using <picture> for format fallback.
  4. Only the first 1–2 images in a gallery load eagerly; everything else loads via Intersection Observer with a reasonable rootMargin buffer.
  5. User-uploaded images are served via signed URLs where relevant, as part of the broader effort to secure a travel app against content tampering and unauthorized access.

Final thoughts

None of these three techniques — lazy loading, CDN delivery, and compression — is individually complicated. What makes travel apps hard is that they all have to work together, at scale, across wildly inconsistent network conditions, without degrading the rich, photo-driven browsing experience that makes travel apps worth using in the first place. Get the pipeline right once, and it pays off on every listing page you ship afterward.

If you're mid-build and want to sanity-check your setup, a quick gut check: open your busiest listing page on throttled 3G in your browser's dev tools. If it doesn't feel usable there, your users on real-world mobile networks are feeling it too.

Top comments (0)