TIL. 웹 사이트 성능 최적화하기

Yang⭐·2023년 3월 1일
0

리소스 용량 줄이기

불필요한 코드 줄이기

  1. 간결한 셀렉터 사용
  2. 공통 스타일은 cssclass 로 정의해서 사용
  3. 트리 쉐이킹
    import 시 필요한 부분만 import해서 사용
    import _ from 'lodash'; // bad :(
    import array from 'lodash/array'; // good :)

이미지 확장자 선택

촬영된 이미지의 경우 png보다 jpg, jpeg 의 이미지크기가 더 작고, 만들어진 이미지의 경우 jpg보다 png의 사이즈가 더 작기에 여러방면으로 확장자 선택 시 고려

애니메이션 요소의 경우 gif보다 video 태그로 mp4파일을 사용하는것이 더 적은 용량의 리소스 사용.

이미지 스프라이트

이미지를 하나로 묶어 한번의 리소스 요청을 통해 가져와 background-position 속성으로 원하는 부분만 표시

이미지 지연로딩

  1. img tag loading="lazy" 속성
  2. Intersection Observer API(교차 관찰자 API)
    1. Intersection Observer의 options 객체 생성

      • root에 타겟 요소의 가시성을 확인할 뷰포트 요소
      • rootMargin: root의 각 측면의 영역을 수측 또는 증가
      • threshold: observer의 callback이 실행될 타겟 요소의 가시성 퍼센티지
    2. Intersection Observer의 callback 함수 생성

      • 타겟 요소와 root가 교차된 상태인지의 여부 확인
        - 교차된 타겟 요소의 dataset에 등록된 이미지 주소를 src에 할당하여 이미지 로딩
      • 이미지 로딩이 완료된 타겟 요소는 관측 요소에서 제외한다.
    3. IntersectionObserver(callback, options) 인스턴스 생성

    4. IntersectionObserver 인스턴스에 타겟 요소들을 등록

      // Custom hook
      function useLazyImageObserver(target) {
        useEffect(() => {
          if (!target.current) {
            return;
          }
      
          let observer = new window.IntersectionObserver(entries => {
            entries.forEach(entry => {
              if (entry.isIntersecting) {
                const lazyImage = entry.target;
      
                lazyImage.src = lazyImage.dataset.src;
                observer.unobserve(lazyImage);
              }
            });
          });
      
          observer.observe(target.current);
      
          return () => {
            if (observer) {
              observer.disconnect();
              observer = null;
            }
          };
        }, [target]);
      }
      
      // Component
      function App() {
        const target = useRef(null);
      
        useLazyImageObserver(target);
      
        return (
          <section>
            <div style={{ height: "2000px" }} />
            <img ref={target} data-src="https://placeimg.com/320/100/any" alt="" />
          </section>
        );
      }

페이지 렌더링 최적화

목표는 레이아웃을 최대한 빠르게, 최대한 적게 발생하는것.

강제 동기 레이아웃 피하기

계산된 값을 반환하기 전에 변경된 스타일이 계산 결과에 적용되어 있지 않으면 변경 이전 값을 반환하기 때문에 브라우저는 동기로 레이아웃을 해야만 한다

    const tabBtn = document.getElementById("tab_btn");
    
    tabBtn.style.fontSize = "24px";
    console.log(testBlock.offsetTop); // offsetTop 호출 직전 브라우저 내부에서는 동기 레이아웃이 발생한다.
    tabBtn.style.margin = "10px";

레이아웃 스래싱 피하기

한 프레임 내에서 강제 동기 레이아웃이 연속적으로 발생하면 성능이 더욱 저하된다.

    // before
    function resizeAllParagraphs() {
      const box = document.getElementById("box");
      const paragraphs = document.querySelectorAll(".paragraph");
    
      for (let i = 0; i < paragraphs.length; i += 1) {
        paragraphs[i].style.width = box.offsetWidth + "px";
      }
    }
    
    // after
    function resizeAllParagraphs() {
      const box = document.getElementById("box");
      const paragraphs = document.querySelectorAll(".paragraph");
      const width = box.offsetWidth;
    
      for (let i = 0; i < paragraphs.length; i += 1) {
        paragraphs[i].style.width = width + "px";
      }
    }

상위 DOM요소보다 하위 DOM요소를 사용하기

상위 DOM요소를 사용하면 내부 하위 DOM요소에도 영향을 미치므로 가급적 하위 DOM요소를 변경한다.

domFragment 활용하기

    // before
    const parentNode = document.getElementById("parent")
    const cnt = 10;
    
    for (let i=0;i<cnt;i++) {
      const newNode = document.createElement('li');
      newNode.innerText = `this is ${i}-content`;
      
      parentNode.appendChild(newNode);
    }
    
    // after
    const parentNode = document.getElementById("parent")
    const cnt = 10;
    
    const fragNode = document.createDocumentFragment();
    
    for (let i=0;i<cnt;i++) {
      const newNode = document.createElement('li');
      newNode.innerText = `this is ${i}-content`;
      
      fragNode.appendChild(newNode);
    }
    
    parentNode.appendChild(fragNode);

domFragment에 추가된 요소들을 parentNode에 append하면 한 번만 DOM객체에 접근하면 되므로 효율적이다. documentFragment는 실제 DOM 트리에 포함되는 요소가 아니므로 reflow나 repaint를 발생시키지 않는다.

애니메이션 최적화

한 프레임 처리가 16ms(60fps) 내로 완료되어야 렌더링 시 끊기는 현상 없이 자연스러운 렌더링을 만들어낼 수 있다. 자바스크립트 실행 시간은 10ms 이내에 수행되어야 레이아웃, 페인트 등의 과정을 포함했을 때 16ms 이내에 프레임이 완료될 수 있다. 애니메이션을 구현할 때 네이티브 자바스크립트 API를 사용하는 것보다 CSS 사용을 권장한다.

애니메이션 요소는 position 고정시키기

애니메이션이 있는 요소는 다른 요소에 영향을 미칠 수 있으니 position:absolute; , position:fixed 로 고정한다.

Reflow보다 Repaint 속성 활용하기

가장 많이 사용하는 속성인 top,left, right, bottom 이나 width, height 조정대신 transform 속성을 활용하면 레이어만 분리해 합성만 일어나게됨.

사용자 기준 성능 최적화

스켈레톤 UX활용

실제 데이터가 로드되기 전에 영역을 표현할 스켈레톤 UX를 활용하면 체감 로드속도를 향상시킬 수 있음.

React에서 React.lazy를 통해 코드 스플리팅과 동시에 Suspense의 fallback props에 스켈레톤 Component를 설정해 컴포넌트가 로드되기 전 스켈레톤 이미지를 띄울 수 있다.

    const BotItem = React.lazy((_) => import("./BotItem"));
    
    const BotList = React.memo((props) => (
      props.data.map((bot) => (
        <Suspense key={bot._id} fallback={thumbnail}>
          <BotItem {...bot} />
        </Suspense>
      ))
    )

React.memo를 사용하기

이 함수로 component를 감싸주면 이전 props와 현재 props의 각 필드를 비교해 업데이트 여부를 결정함. props의 타입에 따라 적용 방식이 달라진다.

  • 원시타입으로 구성된 경우
    각 prop의 비교 조건이 명확하고 재사용되기 좋은 component일 가능성이 높기에 가급적 적용시키는게 좋다.
  • function이 포함된경우
    부모 component에서 전달되는 함수 prop이 매번 재생성되는지 확인해야함. 재생성된 함수가 전달된다면 prop이 안바뀌어도 매번 렌더링됨. ( useCallback 또는 useReducer를 통해 해결할 수 있다.)
  • 배열 및 객체가 포함된경우
    Array method, Object.assign, spread operator 등으로 재성성된다면 참조가 바뀌기 때문에 매번 렌더링됨.
    이 때 React.memo에서 제공되는 custom 함수로 비교로직을 구성할 수있다.
        function Hotel(props) {
          const { id, rooms } = props;
          return (
            <section>
             {rooms.map(room => <HotelRoom key={room.id} room={room} />)}
            </section>
          );
        }
        function areEqual(prevProps, nextProps) {
          return prevProps.id === nextProps.id;
        }
        export default React.memo(Hotel, areEqual); 
	// rooms가 재생성된 배열이라도 id값이 변경된 경우에만 렌더링됨.	
  • children이 포함된경우
    childres prop은 매 렌더링마다 참조가 바뀌기때문에 children의 하위 component 대상으로 React.memo적용시기거나 children prop을 캐시하는 방법을 택해야함.
function App(props) {
  const { name, rooms } = props;
  const children = useMemo(() => <HotelRooms rooms={rooms} />, [rooms]);
  return (
    <section>   
      <Hotel name={name}>{children}</Hotel>
    </section>
  );
}
function Hotel(props) {
  const { name, children } = props;
  return (
    <section>
      <h1>{name}</h1>
      {children}
    </section>
  );
}
export default React.memo(Hotel);

Reference

[프론트엔드] 성능 최적화 정리

마이리얼트립 웹사이트 성능 측정 및 최적화 Part 1. 리소스 로딩

마이리얼트립 웹사이트 성능 측정 및 최적화 Part 2. 렌더링

0개의 댓글