Image Loading Optimization

imageperformanceoptimizationpreloadintersectionobserver

2019-05-05


1. Data URI scheme (base64 encoding)

base64 ๋กœ ์ด๋ฏธ์ง€๋ฅผ ์ธ์ฝ”๋”ฉํ•˜์—ฌ html ์— ์ธ๋ผ์ธ์œผ๋กœ ๋„ฃ๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ, ํฌ๊ธฐ๊ฐ€ ์ž‘์€ ์ด๋ฏธ์ง€๋ฅผ ๋ Œ๋”ํ•  ๋•Œ ์ ํ•ฉํ•˜๋‹ค. ์ด๋ฏธ์ง€ ํŒŒ์ผ์„ ๊ฐ€์ ธ์˜ค๋Š” http ์š”์ฒญ์„ ๋”ฐ๋กœ ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ๋กœ๋”ฉ ์†๋„๋ฅผ ์ค„์ผ ์ˆ˜ ์žˆ๋‹ค.

์ธ์ฝ”๋”ฉ๋œ ๊ฐ’์„ data URI scheme (data:[<media type>][;base64],<data>) ํ˜•์‹์œผ๋กœ src property ์— ๋„ฃ์œผ๋ฉด ๋œ๋‹ค.

<img src="data:image/png;base64,iVBORw0KGgoAAAANSU..."/>

์ด๋ฏธ์ง€ ํฌ๊ธฐ๊ฐ€ ํด ๊ฒฝ์šฐ์—๋Š” ์ธ์ฝ”๋”ฉํ•˜์—ฌ html ์— ๋“ค์–ด๊ฐ€๋Š” ์ฝ”๋“œ๋„ ์ปค์ง€๊ณ , request ๋ฅผ ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— caching ๋„ ๋˜์ง€ ์•Š๋Š”๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ๋‹ค.

Webpack ์„ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด url-loader ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ํ†ตํ•ด ์ด๋ฏธ์ง€ ๋ชจ๋“ˆ์„ ์ž๋™์œผ๋กœ ์ธ์ฝ”๋”ฉ/์ธ๋ผ์ธ ์‚ฝ์ž…ํ•  ์ˆ˜ ์žˆ๋‹ค. limit ์˜ต์…˜๋ณด๋‹ค ์ž‘์€ ์ด๋ฏธ์ง€ ๋ชจ๋“ˆ์€ ์ธ๋ผ์ธ์œผ๋กœ ์‚ฝ์ž…ํ•˜๊ณ , ํฐ ๋ชจ๋“ˆ์€ fallback ์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋‹ค. fallback ์„ ์ง€์ •ํ•˜์ง€ ์•Š์œผ๋ฉด ์ด๋ฏธ์ง€๋ฅผ ํŒŒ์ผ๋กœ ์ทจ๊ธ‰ํ•˜๋Š” file-loader๊ฐ€ ๊ธฐ๋ณธ๊ฐ’์ด ๋œ๋‹ค.

{
  test: /\.(png|jpe?g)$/i,
  use: [
    {
      loader: 'url-loader',
      options: {
        limit: 8192,
        fallback: 'file-loader' // default
      }
    }
  ]
}

2. Preload

์ด๋ฏธ์ง€๊ฐ€ ๋กœ๋“œ๋˜๋Š”๋™์•ˆ ๋ฒ„๋ฒ…๊ฑฐ๋ฆฌ๋Š” ํ˜„์ƒ์„ ํ”ผํ•˜๋ ค๋ฉด preload๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ๋‹ค๋ฅธ ์ž์›๋“ค๋ณด๋‹ค ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋†’์—ฌ์„œ ์ฐจ๋‹จ ์—†์ด(non-render-blocking) ์ด๋ฏธ์ง€๋ฅผ ๋จผ์ € ๋กœ๋”ฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.


1. <link> ์‚ฌ์šฉํ•˜๊ธฐ

link๋Š” ํ˜„์žฌ ๋ฌธ์„œ์™€ ์™ธ๋ถ€ ๋ฆฌ์†Œ์Šค ๊ฐ„์˜ ๊ด€๊ณ„๋ฅผ ๋ช…์‹œํ•œ๋‹ค. as ์†์„ฑ๊ฐ’์œผ๋กœ๋Š” ์ด๋ฏธ์ง€ ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ video, css, font ๋“ฑ๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

<head>
  <link rel="preload" as="image" href="logo.jpg"/>
</head>
...
<body>
  <img src="logo.jpg"/>
</body>
  • ์ด๋ฏธ์ง€๋ฅผ preload ํ•˜์ง€ ์•Š์•˜์„ ๋•Œ: before
  • preload ํ–ˆ์„ ๋•Œ (๋‹ค๋ฅธ ์ž์›๋ณด๋‹ค ๋จผ์ € ๋กœ๋“œ๋œ๋‹ค): after

2. Image() constructor ์‚ฌ์šฉํ•˜๊ธฐ

new Image() constructor ๋Š” HTMLImageElement ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•œ๋‹ค. ์ด๋ฅผ window๊ฐ€ ๋กœ๋“œ๋  ๋•Œ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.

window.onload = function() {
  const img = new Image();
  img.src = "assets/image.png";
};

3. Lazy loading with IntersectionObserver API

์ด๋ฏธ์ง€๊ฐ€ viewport ์— ๋“ค์–ด์˜ฌ ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ ธ๋‹ค๊ฐ€ ๋กœ๋“œํ•˜๋Š” ๊ฒƒ์œผ๋กœ, medium ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.

๋จผ์ € ํ•ด์ƒ๋„๊ฐ€ ๋‚ฎ์•„์„œ ๋น ๋ฅด๊ฒŒ ๋กœ๋”ฉํ•  ์ˆ˜ ์žˆ๋Š” placeholder ์ด๋ฏธ์ง€๋ฅผ ๋ณด์—ฌ์ฃผ๊ณ , viewport ์•ˆ์— ์ด๋ฏธ์ง€๊ฐ€ ๋“ค์–ด์˜ฌ ๋•Œ ์‹ค์ œ ์ด๋ฏธ์ง€๋ฅผ ๋กœ๋”ฉํ•œ๋‹ค. ๊ธฐ์กด์— ์ด๋ฅผ ๊ตฌํ˜„ํ•˜๋ ค๋ฉด scroll ์ด๋ฒคํŠธ๋ฅผ ๊ตฌ๋…ํ•ด์„œ getBoundingClientRect()ํ•จ์ˆ˜๋กœ ์ง์ ‘ element ์˜ ํฌ๊ธฐ๋ฅผ ๋น„๊ตํ•˜๋Š” ๋“ฑ ๊ท€์ฐฎ์€ ์ž‘์—…์ด ๋งŽ์•˜๋‹ค.

ํ•˜์ง€๋งŒ IntersectionObserver API ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์›ํ•˜๋Š” element ๊ฐ€ ํ˜„์žฌ ๋ช‡ %๋‚˜ ๋ณด์ด๋Š”์ง€ ์•Œ์•„์„œ ๊ฐ์ง€ํ•˜๊ณ  ์›ํ•˜๋Š” ์ฝœ๋ฐฑํ•จ์ˆ˜(์ด๋ฏธ์ง€ ๋กœ๋”ฉ)๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด๋ฏธ์ง€ lazy loading ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๋ฌดํ•œ ์Šคํฌ๋กค, ๊ด‘๊ณ  ๋ฐฐ๋„ˆ ๋ทฐ ์ธก์ • ๋“ฑ์—์„œ๋„ ํ™œ์šฉ๋œ๋‹ค.

<img class="lazy" src="placeholder.png" data-src="image.png" data-srcset="image@2x.png 2x, image@3x.png 3x" />

๋จผ์ €, lazy loading ํ•  ์ด๋ฏธ์ง€์— class ๋“ฑ์„ ์ง€์ •ํ•˜๊ณ  src, srcset ์„ data attribute ๋กœ ์ „๋‹ฌํ•œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์•„๋ž˜์™€ ๊ฐ™์ด IntersectionObserver ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.

document.addEventListener("DOMContentLoaded", function() {
  const lazyImages = Array.from(document.querySelectorAll("img.lazy"));

  let lazyImageObserver = new IntersectionObserver(
    (entries, observer) => {
      /** entries ๋Š” ๋ณ€ํ™”๊ฐ€ ๊ฐ์ง€๋˜๋Š” ๋‹ค์–‘ํ•œ ์†์„ฑ๋“ค์ด๋‹ค. (e.g. isIntersecting, boundingClientRect, intersectionRect)  */
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          let lazyImage = entry.target;
          lazyImage.src = lazyImage.dataset.src;
          lazyImage.srcset = lazyImage.dataset.srcset;

          /** load๋ฅผ ๋งˆ์น˜๋ฉด observe๋ฅผ ๋๋‚ธ๋‹ค. */
          lazyImage.classList.remove("lazy");
          lazyImageObserver.unobserve(lazyImage);
        }
      });
    },
    { threshold: 0.8 } /** 80%๊ฐ€ ๋ณด์ด๋ฉด callback์„ ์‹คํ–‰ํ•œ๋‹ค. */
  );

  lazyImages.forEach(lazyImage => {
    /** lazy ๋กœ๋”ฉ์ด ํ•„์š”ํ•œ ๋ชจ๋“  ์ด๋ฏธ์ง€์— ๋Œ€ํ•˜์—ฌ observe ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•œ๋‹ค. */
    lazyImageObserver.observe(lazyImage);
  });
});

Chrome 51 ๋ฒ„์ „, Safari 12.1 ๋ฒ„์ „ ์ด์ƒ๋ถ€ํ„ฐ ์ง€์›ํ•˜๋ฉฐ, ์ง€์›๋˜์ง€ ์•Š๋Š” ๋ธŒ๋ผ์šฐ์ €์—์„œ๋Š” polyfill์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.


4. gzip & CDN

์ด๋ฐ–์—๋„ ์ž์›์„ gzip ์œผ๋กœ ์••์ถ•ํ•˜๊ฑฐ๋‚˜ CDN ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ตœ์ ํ™”ํ•  ์ˆ˜ ์žˆ๋‹ค. CDN(Content Delivery Network)๋Š” ๋งˆ์น˜ ์ฟ *์˜ ๋กœ์ผ“์ง๊ตฌ์ฒ˜๋Ÿผ ์ „์„ธ๊ณ„ ๊ณณ๊ณณ์˜ ์—ฃ์ง€ ๋กœ์ผ€์ด์…˜์—์„œ ์‚ฌ์šฉ์ž์—๊ฒŒ ๊ฐ€์žฅ ๋น ๋ฅด๊ฒŒ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๋Š” ๋ฃจํŠธ๋กœ ์ž์›์„ ์ „๋‹ฌํ•˜๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.


์ฐธ๊ณ