개발서적/프론트엔드 성능 최적화 가이드

프론트엔드 성능 최적화 가이드 [작성중]

journey-dev 2023. 12. 25. 16:11

프론트엔드 성능 최적화 가이드

 

1. 이미지 최적화 - 이미지 CDN

화면에 렌더될 이미지의 크기가 100px인데 다운받은 사진이 1000px일 경우 리소스 낭비됨.

때문에 CDN을 이용해 처음에 다운받을 때 부터 사이즈를 조절해서 다운받거나

미국서버가 아닌 좀 더 가까운 한국 서버에서 다운 받도록 하여 다운 속도를 향상시킬 수 있음

 

※ CDN 사이트 예제 : https://developers.cloudflare.com/images/image-resizing/url-format/'

100px로 렌더될 이미지라면 2배수인 200 사이즈로 줄이고, 품질을 낮춰서 다운받는다

<img src="/cdn-cgi/image/width=200,quality=75/uploads/avatar1.jpg" />
// https://<ZONE>/cdn-cgi/image/<OPTIONS>/<SOURCE-IMAGE>

2. 병목코드 최적화

performance 탭에서 컴포넌트가 렌더링 될 때 오래걸리는 부분을 파악 -> 그 부분의 코드 수정하여 최적화하기


3. 코드 분할(code splitting) , 지연로딩(lazy loading)

- 하나의 번들 파일을 여러 개로 쪼개서(코드 분할) 필요한 시점에 로드되어 실행됨(지연 로딩)
: 장) 페이지를 로드할 때 당장 필요 없는 코드는 번들에 포함되지 않아 로드할 파일이 작아지고, 초기 로딩 속도 빨라짐
: 단) 해당 페이지에 들어갈 때 새로 로드해야 되서 지연 발생 (->사전 로딩으로 해결가능함)

 

- 동적 import 사용
: import 문을 빌드할 때가 아닌, 런타임에 로드하는 것

: 동적 import는 Promise를 반환함. Promise안에 컴포넌트가 감싸진 형태로
때문에 React.lazy로 로드된 컴포넌트를 프로미스 밖으로 빼내는 것임.

(※ 동적으로 모듈 가져오기 :  https://ko.javascript.info/modules-dynamic-imports )

 

- React.Suspense

: lazy로 import하면 컴포넌트를 로딩하는 시간이 걸리는데, 그 시간동안 화면이 보여지도록 하는 역할

: 컴포넌트 로드되기 전엔 fallback이 보여진다.

function App() {
  const ListPage = React.lazy(() => import("./pages/ListPage/index"));
  const ViewPage = React.lazy(() => import("./pages/ViewPage/index"));

  return (
    <div className="App">
      <React.Suspense fallback={<div>loading...</div>}>
        <Switch>
          <Route path="/" component={ListPage} />
          <Route path="/view/:id" component={ViewPage} />
        </Switch>
      </React.Suspense>
    </div>
  );
}

4.사전로딩

✔️ 컴포넌트 사전 로딩
: 미리 코드를 로드해 두어 컴포넌트를 빠르게 띄울 수 있음

[컴포넌트 사전로딩 시점]

1) 버튼 위에 마우스 올려놓았을 때 사전로딩 하기
: mouseEnter 이벤트를 통해 버튼 누르기 전에 미리 로드하여 준비시켜 놓기

function App() {
 const handlerMouseEnter = () => {
    const component = import("./components/ImageModal");
  };

    return (
        <ButtonModal onMouseEnter={handlerMouseEnter}>
            사진 보기
        </ButtonModal>
}


2) 최초에 페이지가 로드되고 모든 컴포넌트 마운트 끝났을 때
: 미리 준비해야 할 컴포넌트 크기가 큰 경우 사용함. mouseEnter하기도 전에 미리 파일을 준비하게 됨.

: 마운트 완료 시점 파악  
   React - componentDidMount, useEffect , VUE - mounted에서 $nextTick

 

✔️이미지 사전로딩
: js의 new Image를 통해 이미지를 직접 로드해놓음

: 브라우저가 해당 이미지를 로드하고, 로드된 이미지는 브라우저 캐시에 저장됨.
동일 이미지에 대한 후속 요청이 있을 때 다시 다운하지 않고 캐시된 이미지를 보여주어 사전로딩이 가능한 것임

  useEffect(() => { 
    const img = new Image();
    img.src = "https://photos/...";
  }, []);

5. 리플로우, 리페인트 지양

[리렌더링(리플로우,리페인트)]
브라우저 리소스를 많이 잡아먹어, 화면을 새로 그리는 것이 느려진다.

  • 리플로우 : cssom , render Tree > 리레이아웃 > 리페인트 > composition
  • 리페인트 : cssom , render Tree > x > 리페인트 > composition

[대책]

CPU에서 해야할 작업을 GPU(그래픽 처리 장치)로 전가

: GPU는 애초에 그래픽 작업을 처리하기 위해 만들어진 것이여서 화면은 빠르게 그려줌
: GPU를 활용하여 그래픽 작업이 이뤄져, CPU에서 리레이아웃,리페인트 거칠 필요 없이 스타일 변경 가능해짐

 

✔️ transform , opacity 사용
transform , opacity은 브라우저에서 합성 계층으로 취급되어 다른 계층과 격리되어 있어,

해당 계층이 변화되도 다른 요소에 영향X

  • left, right, width, height 보단 transform사용
    : transform은 실제 레이아웃에 영향을 주지 않음. 요소의 크기나 위치가 변경되지 않아 리플로우X
  • visibility, display보단 opacity 사용
    : opacity는 픽셀의 변화를 일으키지 않음. 따라서 리페인트X

✔️ 적용

만약 progress bar를 width 속성으로 채운다면 width가 변할 때마다 리플로우,리페인트 가 발생한다.

그러나 transform:scaleX 을 이용한다면 리플로우를 막을 수 있다. (> composite만 일어남)

width로 구현, 리렌더링이 발생하여 깜빡거림
transform으로 구현, 리렌더링이 일어나지 않는다

※ 추가 자료

https://wit.nts-corp.com/2017/06/05/4571

https://boxfoxs.tistory.com/408


6. 이미지 지연로딩 - Intersection Observer

당장 보여지지 않는 이미지는, 스크롤하여 뷰포트에 요소가 들어온 때 이미지를 로드하는 방식 (요소를 관찰하는 것)

페이지 스크롤시 요소가 화면에 들어왔는지 감시하여 로직이 실행되게 됨.

 

- scroll 이벤트를 사용하면 스크롤 할 때마다 계속 로직이 실행되는 문제가 있어 성능적으로 좋지 않음

import React, { useEffect, useRef } from "react";

function Card(props) {
  const imgRef = useRef(null);

  useEffect(() => {
    const option = {};
    const callback = (entreis, observer) => {
      entreis.forEach((entry) => {
        if (entry.isIntersecting) {
          entry.target.src = entry.target.dataset.src;
          observer.unobserve(entry.target); // 요소에 대한 관찰을 제거 (이미지 한번 로드 후 다시 호출할 필요가 없으므로 관찰 해제)
        }
      });
    };

    const observerInstance = new IntersectionObserver(callback, option);
    observerInstance.observe(imgRef.current);

    return () => observerInstance.disconnect(); // cleanUp funtion - 인스턴스 제거 (리소스 낭비 방지)
  }, []);

  return (
    <div>
      <img data-src={props.image} ref={imgRef} />
    </div>
  );
}

7. 이미지 사이즈 최적화

✔️ 이미지 포맷 종류

1) PNG : 무손실 압축(원본 회손 없이 압축), 투명도 표현 ㅇ

 

2) JPG : 손실 압축(원본 정보에 손실 발생), 투명도 표현X
: 일반적으로 웹에서 이미지 사용시 고화질이거나 투명도 정보가 필요한게 아니라면 jpg 사용함

 

3) WEBP : 손실,무손실 압축 모두 제공

webp

 

webp만으로 이미지 렌더링 할 경우

최신 이미지 포맷이라 지원되지 않는 브라우저 있을 수 있음

=> picture, source 태그를 사용하여 이미지 렌더하여 해결 가능

 

✔️ picture  태그

picture 태그는 다양한 타입의 이미지를 렌더링하는 컨테이너로 사용됨

 

1. 브라우저별 지원되는 타입의 이미지를 찾아 렌더

- source태그가 여러개 있으면 위에서 부터 적용됨 (webp가 지원되지 않는 브라우저라면 > jpg로 적용)

- source에 지원되는 이미지 형식이 없다면, 기본 img태그를 기본 이미지로 사용한다.

<picture>
  <source srcset="./assets/main1.webp" type="image/webp">
  <source srcset="./assets/main1.jpg" type="image/jpeg">
  <img src="./assets/main1.jpg" alt="Main Item"> 
</picture>

 

2. 브라우저 사이즈에 따라 지정된 이미지 렌더

<picture>
  <source srcset="./assets/main1-large.jpg" media="(min-width: 800px)">  <!-- 800px 이상인 경우 사진 바뀜 -->
  <source srcset="./assets/main1-medium.jpg" media="(min-width: 600px)">  <!-- 600px 이상인 경우 사진 바뀜 -->
  <img src="./assets/main1-small.jpg" alt="Main Item">   <!-- 기본 이미지 -->
</picture>

 

✔️ picture  태그

 

- webp 지원이 안될시 기본 img태그를 쓰도록 처리하는 picture 태그 처럼, 동영상에선 video 태그가 동일한 역할을 해준다.

- webp 처럼 비디오에선 webm이 있다.

<vidio autoPlay loop muted>
    <source src={'video_webm'} type="video/webm" />
    <source src={'video'} type="video/mp4" />
</vidio>

8. 폰트 최적화

✔️ 폰트 로드하는 방식

1) FOUT (Flash of Unstyled Text)

- 사용 브라우저 : Edge

- 폰트 다운여부와 상관없이, 일단 기본 텍스트를 여줘준 뒤 다운완료되면 그때 폰트 적용하는 방식

- 적용 : 중요한 텍스트인 경우 (글자가 빠르게 나타나는게 중요)

2) FOIT (Flash of Invisible Text)

- 사용 브라우저 : 크롬, 사파리, 파이어폭스 등

- 폰트가 완전히 다운로드 되기 전까지 텍스트를 보여주지 않다가 -> 폰트 다운 완료되면 폰트 적용된 텍스트 보여줌

- 적용 : 사용자에게 꼭 전달하지 않아도 되는 텍스트의 경우

 

✔️ 폰트 최적화 방법1.  폰트 적용 시점 제어하는 법

CSS font-display 속성으로 폰트 적용 시점을 제어할 수 있다.

fout 방식인 브라우저에서 foit 방식 적용할 수 있고, foit 방식인 브라우저에서 fout 방식 적용할 수 있다.

@font-face{
    font-display: fallback, 
    font-family : ...,
    scr : url("")
}

 

font-display 속성

1. auto : 브라우저 기본 동작 (default)

2. block : FOIT (timout = 3s)
: 글꼴 다운로드 최대 3초까지 기다렸다 -> 기본폰트 -> 커스텀 폰트 적용
: 다운된 글꼴이 적용될 때 자연스럽게 표현하기
  1) fontfaceobserver 라이브러리를 이용해 폰트가 다운되는 시점을 파악
  2) 다운된 폰트가 적용될 때 애니메이션(페이드인)을 적용하여 부드럽게 나타낼 수 있다.


3. swap : FOUT
: 글꼴 다운 완료될 때 까지 기본글꼴 적용


4. fallback : FOIT (timeout = 0.1s) :
: 0.1초 까지는 텍스트 보여주지 않음.
: 0.1초 이후엔 폴백 폰트 적용 (※ 폴백 폰트: 폰트가 로드되지 않거나 사용할 수 없을 때 대신 사용하는 대체 폰트)
: 3초 후에도 폰트 다운하지 못한 경우, 다운 완료 되더라도 폰트 적용하지 않고 캐시해둠
: 이후 페이지가 다시 로드왰을때 캐시된 폰트를 바로 적용함


5. optional : FOIT (timeout = 0.1s)
: 0.1초 까지는 텍스트 보여주지 않음.
: 0.1초 이후엔 폴백 폰트 적용
: 네트워크 상태에 따라 폰트 적용할지 여부 결정, 다운 완료 되더라도 폰트 적용하지 않고 캐시해둠
: 이후 페이지가 다시 로드됐을때 캐시된 폰트를 바로 적용함

 

✔️ 폰트 최적화 방법2.  폰트 사이즈 줄이는 법

1. 압축률이 좋은 확장자로 변경

파일 크기 : EOT > TTF/OTF > WOFF > WOFF2

 

- 폰트 압축 사이트 : transfonter  

- png,jpg보다 압축률이 좋은 webp가 있는 것 처럼, 폰트에는 TTF 보다 압축률이 좋은 WOFF가 있다.

- WOFF, WOFF2 : 브라우저 호환성 문제 있음

 - webp 지원 안될때 png로 나오게 한 원리처럼 폰트도 마찬가지

@font-face {
	font-family: BMYEONSUNG;
	src: url('./assets/fonts/BMYEONSUNG.woff2') format('woff2'), // woff2 적용
	url('./assets/fonts/BMYEONSUNG.woff') format('woff'), // 지원안되면 woff 적용
	url('./assets/fonts/BMYEONSUNG.ttf') format('truetype'); // 지원안되면 ttf 적용
	font-display: block;
}

 

2. 필요한 문자의 폰트만 로드

서브셋 폰트 사용 TODO

 

읽기!

https://d2.naver.com/helloworld/4969726