웹성능 최적화(2)

웹성능 최적화2

유동균님의 강의를 보며 정리한 글 입니다.


1. 이미지 지연(lazy) 로딩

post image

예제에서 네트워크를 6메가비트로 맞춰놓았다.

제일 먼저 사용자에게 보여져야할 동영상이 이미지 보다 더 늦게 다운로드 되는 부분을 동영상이 먼저 다운로드 되도록 수정해야한다.

  • 이미지를 빠르게 다운로드
  • 이미지를 나중에 다운로드, 동영상 먼저 다운로드

두번째 방법을 사용해 수정(image lazy)

이미지를 필요할때(나중에, 보여지기 직전에) 로드되도록 해야한다.

스크롤이 이미지가 있는 곳에 도달하면 이미지를 로드하고, 그렇지 않으면 보이지 않도록 해야하는데,

이 방법은 사용자가 매번 스크롤을 할 때마다 이벤트함수가 호출되는 단점이 있다.

이 문제를 해결하려면 IntersectionObserver로 해결할 수 있다. 즉 화면에 특정 이미지가 들어올 때만 함수를 호출하게 된다.

https://developer.mozilla.org/ko/docs/Web/API/Intersection_Observer_API

const observer = new IntersectionObserver(callback, options)
observer.observe(element객체)
    const imgRef = useRef(null)

   useEffect(() => {
      const options = {}
      const callback = () => {
         console.log('callback')
      }
      const observer = new IntersectionObserver(callback, options)

      observer.observe(imgRef.current)
   }, [])


   return (
      <div className="Card text-center">
         <img src={props.image} ref={imgRef} />
         <div className="p-5 font-semibold text-gray-700 text-xl md:text-lg lg:text-xl keep-all">
            {props.children}
         </div>
      </div>
   )
}
post image

스크롤을 내려 이미지가 보이거나 사라질때 콘솔이 찍힌다.

이미지가 보이는 그 순간에만 로드하려면 callback 함수에 entries와 observer객체를 넘겨주면 된다.

useEffect(() => {
   const options = {}
   const callback = (entries, observer) => {
      entries.forEach(entry => {
         if(entry.isIntersecting){
            console.log('is Intersecting')
         }
      })
   }
   const observer = new IntersectionObserver(callback, options)

   observer.observe(imgRef.current)
}, [])
post image

위와는 다르게 화면안에 이미지가 보일 때만 콘솔이 찍힌다.

    const imgRef = useRef(null)

    useEffect(() => {
        const options = {}
        const callback = (entries, observer) => {
            entries.forEach(entry => {
                if(entry.isIntersecting){
                    console.log('is Intersecting', entry.target.dataset.src)
                    entry.target.src = entry.target.dataset.src
                    // 이미지가 들어오면 더이상 감시하지 않음
                    observer.unobserve(entry.target)
                }
            })
        }
        const observer = new IntersectionObserver(callback, options)
        // 이미지를 넣음
        observer.observe(imgRef.current)
    }, [])


    return (
        <div className="Card text-center">
            <img data-src={props.image} ref={imgRef}/>
            <div className="p-5 font-semibold text-gray-700 text-xl md:text-lg lg:text-xl keep-all">
                {props.children}
            </div>
        </div>
    )
}
post image

동영상을 먼저 로드하고, 이미지를 불러옴


2. 이미지 사이즈 최적화

위에서 지연로딩을 사용해 이미지를 불러오는 타이밍을 조절했지만 그래도 이미지 자체의 용량이 크다면 불러오는 속도가 느려질 수 밖에 없다.

이미지 포맷, 확장자 종류

  • JPG를 WEBP(구글에서 나온 이미지 포맷, JPG에 비해서 화질이높고 용량이 낮음)로 변경,
  • 꼭 WEBP로 할필요는 없다 (WEBP, WEBM은 현재 잘 쓰지 않는다) 현재 지원하지않는 브라우저도 있음

picture 태그로 이미지 분기(나중에 자세하게 배워보자)

<picture>
   <source  data-srcset={props.webp} type='image/webp'/>
   <img data-src={props.image} ref={imgRef}/>
</picture>

이미지 포맷은 https://squoosh.app/ 를 사용했다.

post image

예제에서 브라우저에 보일 크기는 300 x 300 이므로 x2를해 600 x 600으로 포맷

이미지가 9.74MB에서 21.9KB로 줄었다.


3. 동영상 사이즈 최적화

동영상이 메인 컨텐츠가 아닌 경우 적합

동영상도 WEBM으로 포맷

<video
   className="absolute translateX--1/2 h-screen max-w-none min-w-screen -z-1 bg-black min-w-full min-h-screen"
   autoPlay
   loop
   muted
>
   <source src={video_webm} type='video/webm' />
   <source src={video} type='video/mp4' />
</video>

4. 폰트 최적화

폰트도 리소스라 네트워크를 통해 받아온다.

  • 웹 폰트의 문제점

FOUT(Flash of Unstyled Text) = 폰트를 다운로드 하기 전에는 기본폰트로 컨텐츠를 보여줌

FOIT(Flash of Invisible Text) = 폰트가 다운로드 되기 전에는 컨텐츠를 보여주지 않음

  1. 폰트 적용 시점 컨트롤
  • font-display 사용 - auto : 브라우저 기본 동작 - block : FOIT (timeout = 3s) - swap : FOUT - fallback : FOIT (timeout = 0.1s) 0.1초 후에도 불러오지 못하면 기본 폰트 유지, 이후에 캐시 - optional : FOIT (timeout = 0.1s) 이후 네트워크 상태에 따라 기본폰트로 유지할지 웹폰트를 적용할지 결정, 이후에 캐시
@font-face {
	font-family: BMYEONSUNG;
	src: url('./assets/fonts/BMYEONSUNG.ttf');
	font-display: swap;
}
post image
  • fontfaceobserver 라이브러리를 사용해 시각적인 효과
@font-face {
   font-family: BMYEONSUNG;
   src: url('./assets/fonts/BMYEONSUNG.ttf');
   font-display: block;
}
const [isFontLoaded, setIsFontLoaded] = useState(false)

const font = new FontFaceObserver('BMYEONSUNG');

useEffect(() => {
   font.load().then(function () {
      console.log('BMYEONSUNG has loaded');
      setIsFontLoaded(true)
   });
}, [])

return (
    <div className="..." style={{opacity: isFontLoaded ? 1 : 0, transition: 'opacity 0.3s ease'}} >
        <div className="...">
            {...}
        </div>
    </div>
)
  1. 폰트 사이즈 줄이기

폰트 포멧 사이트 https://transfonter.org/

  • 웹폰트 포맷 사용 (파일 크기 = EOT > TTF/OTF > WOFF > WOFF2)
@font-face {
   font-family: BMYEONSUNG;
       /* 로컬에 폰트가 있으면 바로 적용*/
   src: local('BMYEONSUNG'),
       url('./assets/fonts/BMYEONSUNG.woff2') format('woff2'),
       /* 지원하지 않는 브라우저 대응 */
       url('./assets/fonts/BMYEONSUNG.woff') format('woff'),
       url('./assets/fonts/BMYEONSUNG.ttf') format('truetype');
   font-display: block;
}
  • local 폰트 사용

  • Subset 사용

필요한 글자만 가져와서 사용 "ABCDEFGHIJKLMNOPQR"

post image

변환하지 않은 폰트는 포함되지 않음

  • Unicode Range 적용
@font-face {
   font-family: BMYEONSUNG;
        /* 로컬에 폰트가 있으면 바로 적용*/
   src: url('./assets/fonts/subset-BMYEONSUNG.woff2') format('woff2'),
        /* 지원하지 않는 브라우저 대응 */
        url('./assets/fonts/subset-BMYEONSUNG.woff') format('woff'),
        url('./assets/fonts/BMYEONSUNG.ttf') format('truetype');
   font-display: block;
   unicode-range: U+0041;
}
post image

Subset은 렌더링 하는 텍스트에 폰트가 필요하지 않아도 폰트를 로드하지만 unicode-range를 사용하면 폰트가 필요하지 않으면 로드하지 않는다

  • data-uri로 변환

Base64 encode 해 불러오는 방식

  • Preload

해당하는 페이지에 폰트가 필요하다는 것을 HTML에 작성

<link rel="preload" href="BMYEONSUNG.woff2" as="font" type="font/woff2" crossorigin>
post image

CSS가 로드되기 전에 폰트가 먼저 로드됨


5. 캐시 최적화

웹 브라우저는 크게 메모리 캐시, 디스크 캐시 두가지로 캐싱한다.

메모리 캐시는 RAM에 저장, 디스크 캐시는 file로 데이터를 저장

  • Cache-Control

브라우저 이미지 요청 -> 서버

서버 -> 브라우저에 이미지(캐시) 보냄

  • 서버에서 설정이 필요
  • no-cache : 캐시를 사용하기 전에 서버에서 검사 후 사용 결정

  • no-store : 캐시 사용 안함

  • public : 모든 환경에서 사용

  • private : 브라우저 환경에서만 캐시 사용, 외부 캐시 서버에서는 사용 불가

  • max-age : 캐시의 유효시간

  • Node

const header = {
    setHeaders: (res, path) => {
    	if(path.endsWith('.html')){
        	res.setHeader('Cache-Control', no-cache)
        } else if(path.endsWith('.js') || path.endsWith('css') || path.endsWith('.webp')){
        	res.setHeader('Cache-Control', 'public, max-age=...'
        } else {
        	res.setHeader('Cache-Control', 'no-store')
        }
    },
}