Logo

dev-resources.site

for different kinds of informations.

How We Reduced 700mb of Images Load

Published at
12/1/2024
Categories
web
frontend
performance
webp
Author
Sol Lee
Categories
4 categories in total
web
open
frontend
open
performance
open
webp
open
How We Reduced 700mb of Images Load

We'll See How To

  1. Reduce image load size with IntersectionObserver API
  2. Use state to conditionally load images
  3. And other useful techniques (webp, lazy loading, ...)

When my co-worker sent me a message reporting the image loading took long, I didn't realize the magnitude of its importance.

In fact, I just guessed it must be a WI-FI issue or something similar.

However, after investigating the dev tools, the images were total 700mb! This was way larger than expected.

The service that downloads these images is a feed in which users can see the actual images of the food from local restaurants and reviews.

My first guess was the hidden images from the screen that were in HTML getting unnecessarily loaded.

Image description

The guess was correct. The gray squares from above screenshot were also being loaded, even though not yet visible for the user.

To improve this, we used the IntersectionObserver API so that they get loaded only they get exposed to the screen:

import React, { useEffect, useRef, useState } from 'react';

interface ObserverImageProps {
  src: string;
  alt: string;
  className?: string;
  width?: number | string;
  height?: number | string;
  /** 이미지가 λ‘œλ“œλ˜κΈ° 전에 ν‘œμ‹œν•  ν”Œλ ˆμ΄μŠ€ν™€λ” 이미지 */
  placeholderSrc?: string;
  /** 이미지가 λ·°ν¬νŠΈμ— 듀어왔을 λ•Œ 싀행될 콜백 */
  onIntersect?: () => void;
}

const ObserverImage = ({
  src,
  alt,
  className = '',
  width,
  height,
  placeholderSrc = 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==', // 1x1 투λͺ… GIF
  onIntersect
}: ObserverImageProps) => {
  const [imageSrc, setImageSrc] = useState<string>(placeholderSrc);
  const [isLoaded, setIsLoaded] = useState<boolean>(false);
  const imgRef = useRef<HTMLImageElement>(null);

  useEffect(() => {
    // IntersectionObserver μΈμŠ€ν„΄μŠ€ 생성
    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          // 이미지가 λ·°ν¬νŠΈμ— 듀어왔을 λ•Œ
          if (entry.isIntersecting) {
            setImageSrc(src);
            onIntersect?.();
            // ν•œ 번 λ‘œλ“œλœ ν›„μ—λŠ” 더 이상 κ΄€μ°°ν•  ν•„μš”κ°€ μ—†μœΌλ―€λ‘œ ν•΄μ œ
            observer.unobserve(entry.target);
          }
        });
      },
      {
        // μ˜΅μ €λ²„ μ˜΅μ…˜
        rootMargin: '50px', // 뷰포트 κΈ°μ€€ 50px μ—¬μœ λ₯Ό 두고 미리 λ‘œλ“œ
        threshold: 0.1 // 10%만 보여도 λ‘œλ“œ μ‹œμž‘
      }
    );

    // ν˜„μž¬ 이미지 μš”μ†Œ κ΄€μ°° μ‹œμž‘
    if (imgRef.current) {
      observer.observe(imgRef.current);
    }

    // μ»΄ν¬λ„ŒνŠΈ μ–Έλ§ˆμš΄νŠΈ μ‹œ μ˜΅μ €λ²„ ν•΄μ œ
    return () => {
      observer.disconnect();
    };
  }, [src, onIntersect]);

  return (
    <img
      ref={imgRef}
      src={imageSrc}
      alt={alt}
      className={`${className} ${!isLoaded ? 'opacity-0' : 'opacity-100'}`}
      width={width}
      height={height}
      onLoad={() => setIsLoaded(true)}
      style={{
        transition: 'opacity 0.3s ease-in-out',
        ...(!isLoaded ? { filter: 'blur(10px)' } : {})
      }}
    />
  );
};

export default ObserverImage;

But We Needed To Take More Action

I thought the amount of download would decrease by large percentage, but it did not. So I had to further investigate.

In the feed, there are 5 "themes" and each one of them displayed 3 contents, which had maximum 10 images. Therefore, we would have 5 x 3 x 10 = 150 images maximum. Even supposing each image is 1mb, we would have 150mb, not the absurd amount of 700mb.

In the end, it turned out that ALL the images in the feed were added to HTML, including those at the bottom of the feed.

Also, the UI for the detailed images was a "swipe" to the left and right. Thise swipe could have maximum 30 images. All of them were being loaded (unnecessarily!).

Result of Improvement: 90% Decrease in Download Size

Now that we spotted the reasons, let's fix them. First of all, we added an open state in order to only load the images when user sees them:

{open === true && <Images />}

Also, we *applied the above-mentioned IntersectionObserver API to make the image get loaded when it appears on the screen. *

These improved approximately 90% of images load. A huge improvement, in fact.

Image description

This Is Not The End. Let's Reduce Hidden Image Sizes

A few days later, the images were heavier again. We were getting total 60mb of images alone. Now the reason was the increased amount of contents, which naturally increased the total of images size. We had to find more ways to reduce it. The following is a list of technique we took:

  • From the fourth image load the images when they appear on the screen
  • Load the restaurant's image when user clicks its details page
  • Load the profile image when it appears on the screen

These techniques led to 5 aditional mb saving πŸŽ‰

Still More Room For Improvements?

Currently, the images uploaded by the users get through a resizing process. In the feed we may use smaller images.

Also, if we convert the image format to webp we are expected to reduce the image size by 25 to 35%.

In summary,

  • Use smaller image size for thumbnails
  • use webp format

Details for Better User Experience

I was able to get some lessons from this performance improvement experience.

  1. The data usage amount is a very important element in user experience, especially for mobile environments.
  2. In order to detect performance issues in invisible areas we need to keep looking at developer tools.
  3. Sometimes simple solutions work better (like lazy loading in this case)

Featured ones: