- Originally from : https://techblog.woowahan.com/20228/
- This post is a translation from above article, no AI-translation used
We'll See How To
- Reduce image load size with IntersectionObserver API
- Use state to conditionally load images
- 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.
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.
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.
- The data usage amount is a very important element in user experience, especially for mobile environments.
- In order to detect performance issues in invisible areas we need to keep looking at developer tools.
- Sometimes simple solutions work better (like lazy loading in this case)
Top comments (0)