Skip to content
31MB → 1.2MB실무 경험

Image transfer 31MB → 1.2MB: optimizing images with a CDN and srcset

6 min readWorkMoved from my Tistory blogRead the Korean original

I applied Akamai Image Manager to the image resources of WiniLogis and Autowini, so that image format conversion and resizing are now handled by the CDN. It follows earlier work where I split out the React core libraries and applied CDN caching to static resources, and this time the work was about reducing image transfer size.

Before the change, I checked the WiniLogis blog list page. A single PNG was 1,240kB, and the larger ones were 2,326kB. About 20 of these images came down, so opening the page once transferred 31MB in images alone. The images were much larger than the cards they were actually shown in.

Network tab before applying Akamai Image Manager, with the original PNGs served as is on the WiniLogis blog list
Figure 1. Network tab before applying Akamai Image Manager, with the original PNGs served as is on the WiniLogis blog list

Automatic format conversion

With Image Manager applied, the CDN reads the browser's Accept header and converts the image to the best format the browser supports. This happens automatically once it is applied, so I did not need to change any code. When I requested the same URL, the 1,240kB PNG above came back as a 35.5kB AVIF.

Across the whole page, image transfer dropped from 31MB to 1.2MB, and the load finish time went from 812ms to 531ms.

Network tab after applying Akamai Image Manager, with the same URL served as AVIF
Figure 2. Network tab after applying Akamai Image Manager, with the same URL served as AVIF

This was the biggest saving in this work, and nothing was done on the frontend for it. The resizing I did afterward is an additional optimization on top of this.

Requesting resizing and choosing widths

Unlike format conversion, resizing did not work just by applying it. The widths had to be registered in the CDN policy in advance, so I listed the widths we needed and sent a request to Megazone (the partner that manages our CDN settings). Once 160, 320, 480, 640, and 720 were registered, I could get derivatives at those widths.

Which of those widths to request was up to the frontend. At first I thought it was simple. The project already had isMobile (a value that says whether the viewport is 599px wide or less), so I figured I could use a small width on mobile and a large width on desktop. The blog list card images are shown at 400px on desktop, so I decided 480, which is close to 400, would be enough.

While I was working on it, though, Claude Code suggested using srcset and sizes to let the browser pick the width. When I looked into why that was better, I found something the isMobile branch misses. Even at the same display width, the image size you need depends on the screen's pixel density, and that is where I first learned about DPR.

DPR(Device Pixel Ratio)

DPR is the number of physical screen pixels used to draw one CSS pixel. High-density displays pack pixels more tightly into the same physical size. If they drew 1 CSS px with a single physical pixel, text and UI would become too small, so each CSS pixel is drawn using several physical pixels.

A typical office monitor usually has a DPR of 1, a MacBook or iMac has 2, and smartphones have 2 to 3. Non-integer values like 1.5 or 2.4 are also common, and most fall between 1.0 and 4.0. You can check it in the browser console with window.devicePixelRatio.

So an image displayed at 400px did not need 400 pixels. On a DPR 2 screen it is drawn across 800 physical pixels, so if you put in a 480px image, the missing pixels are filled by stretching it, and it gets blurrier by that much.

The right image width comes down to display width × DPR. Whether the device is mobile or desktop was not enough to decide it. Even on desktop, a regular monitor is fine with 400px but a MacBook needs 800px, and a 375px mobile viewport at DPR 2 needs 750px.

srcset: listing image candidates

A branch like isMobile cannot take DPR into account, so I looked for a way to leave this calculation to the browser. If you list several widths of the same image in the srcset attribute of an <img> tag, the browser picks the one that fits its environment.

html
<img
  src="blog_480.jpg"
  srcset="blog_320.jpg 320w,
          blog_480.jpg 480w,
          blog_720.jpg 720w"
/>

Each entry is written as {URL} {width}w, where w is the actual pixel width of that file. The important part is that it is not the display size.

sizes: declaring the display width

With only srcset, I got a larger image than I expected. For performance, the browser starts downloading images before CSS calculation is finished, and at that point it cannot know whether this image goes into a 400px card or fills the whole screen.

So you have to tell it the display width in advance with sizes. If you leave it out, the default is 100vw, so even for a 400px card the browser calculates from the full screen width and downloads a needlessly large file.

html
<img
  src="blog_480.jpg"
  srcset="blog_320.jpg 320w,
          blog_480.jpg 480w,
          blog_720.jpg 720w"
  sizes="(max-width: 599px) 100vw, 400px"
/>
<!-- Full viewport width on mobile (599px or less), 400px otherwise -->

The values have to match the actual CSS layout, and for the media query conditions you can use the project's breakpoints as they are.

How the browser picks a width

The browser gets the display width from sizes, multiplies it by its own DPR to calculate the pixels it needs, and then picks the smallest srcset candidate that is equal to or larger than that value.

If no candidate is at least the needed width, it uses the largest one. For example, a 375px mobile viewport at DPR 2 needs 750px, but the largest candidate is 720, so it gets 720.

Results

I applied the same approach to the freight rate cards on the main page as well as the blog list. The country background images in those cards showed a clear difference in file size by width, which made the effect easy to check. A DPR 2 desktop got the 720px version (129.7kB) and a DPR 1 monitor got the 480px version (58.8kB), while the original served without resizing was 222.4kB. Each screen now downloads only as much as it needs.

The same image varies a lot in file size by width, and if it is smaller than the width needed for display, it gets scaled up and looks blurry.
Figure 3. The same image varies a lot in file size by width, and if it is smaller than the width needed for display, it gets scaled up and looks blurry.

You can see which width the browser actually chose from the requested image URL in the Network tab of the developer tools.

Checking the chosen width in the Network tab
Figure 4. Checking the chosen width in the Network tab

srcset was especially important for images uploaded by users. When I checked the original widths of review images, they ranged anywhere from 607px to 5712px. With a fixed width, some images were wasteful, and for others there were even cases of receiving a derivative larger than the original. Leaving the width choice to the browser was the safer option.

What I learned

  • Once the browser has downloaded a large file, it does not download a smaller one again when you shrink the window. When checking different widths, you need to reload.
  • You cannot use srcset with CSS background-image. In that case I had to pick a fixed width based on the display width, or branch when the display widths on web and mobile differed a lot.
  • The CDN does not scale images up beyond the original width, so there was no point in setting a width on an image whose original was already smaller than its display size.

Looking back

I first tried to solve this with an isMobile branch, but in the end the answer was not to detect the device. It was to declare only the display width and hand the decision to the browser. srcset and sizes have been supported by most browsers since around 2015, but I only properly learned them this time. What I especially liked is that I won't have to change the code when new devices come out.

Learning about DPR also explained something I ran into on a personal project. When I applied Cloudflare's image resizing, I picked the width that best matched the display size, yet the images still looked blurry on screen. I couldn't find the cause at the time and moved on, but it was because I only looked at the display width and didn't consider the screen's pixel density.

Since this was work where CDN settings and frontend code had to fit together, I think it was once again a chance to learn new concepts and grow a step further.

  • #AVIF
  • #devicePixelRatio
  • #DPR
  • #LCP improvement
  • #sizes
  • #srcset
  • #responsive images
  • #image CDN
  • #image optimization
  • #frontend performance

More in Work