Lighthouse로 프론트엔드 성능 개선하기

JJ·2024년 8월 7일
1
post-thumbnail

개선 결과

문제점

1. 첫 화면 컨텐츠 렌더링시 버벅거림

첫 화면에서 aos 라이브러리를 사용하여 이미지를 flip시키는 애니메이션으로 렌더링되는 컴포넌트가 있다.
해당 컴포넌트는 이미지가 커튼처럼 렌더링되며, aos의 flip 애니메이션은 엄청 버벅거리고 깜빡거리는 문제점이 있었다.

해당 이슈를 아래와 같은 순서로 해결했다.

AOS로 애니메이션 사용시 will-change 속성 사용

CSS작업에서 스타일 계산, 레이아웃 처리, 애니메이션 프레임 계산 등을 CPU가 담당하는데, CPU가 과도하게 사용되는 경우 버벅거리고 깜빡이는 문제가 발생할 수 있다.

CSS의 will-change 속성은 브라우저에게 요소의 예상 변경 사항을 미리 알려주는 역할을 한다.
이 속성을 사용하면 브라우저는 요소가 실제로 스타일 변형이 필요할 때 필요한 리소스를 최적화 하여 요소의 스타일 변경과 렌더링을 더 빠르고 최적화된 방식으로 처리할 수 있다.

will-change를 사용하면 GPU에서 해당 요소를 처리하도록 최적화 할 수 있고, 브라우저에게 특정 요소에 대해 하드웨어 가속을 미리 준비하도록 지시할 수 있다.

.image-container{
	will-change: transform
}

이미지 사이즈 최적화

우선 첫 화면에 보여지는 이미지 사이즈가 너무나도 컸다.

실제 보여지는 이미지 크기는 435x435였는데 사진 크기는 4000x4000이여서 해당 이미지의 사이즈를 줄였다.

이미지 확장자 변경

webp는 무손실 및 손실 압축을 제공하는 최신 이미지 파일 포멧으로 무손실 압축의 경우 동일 화질에서 png대비 26%, jpg대비 25~34% 더 작은 이미지를 만들 수 있다.

모든 이미지가 jpg,png로 사용되고 있었는데 이 이미지들을 webp로 변환하였다.

2. 이미지 lazy load 적용

여러장의 이미지를 넘기면서 볼 수 있는 이미지 슬라이더를 react-slick으로 구현했고 해당 컴포넌트는 createPortal로 띄우고 있는 상황이였다. 그리고 현재 모달의 상태는 display:none이다.

문제점

img 태그에 lazy load를 적용해놓았음에도 불구하고 웹페이지가 로드될 때 모든 이미지가 받아와지면서 로딩이 느려지는 이슈가 생겼다.

원인은 아래와 같다.

  1. modal이 display:none이지만 position:fixed 상태여서 뷰포트 내부에 들어왔다고 인식
  2. 뷰포트 내부에 들어와있으니 <img loading="lazy"/> 속성이 먹히질 않음.

따라서 createPortal로 렌더링을 하지 않고 해당 모달을 사용하고 있는 section 내부에서 렌더링 하도록 변경하였다.

그 결과 더이상 웹페이지 첫 로드 시 뷰포트 내부에 들어와있지 않아서 이미지들을 받아오지 않았고, 이후 모달이 있는 섹션이 뷰포트 내부에 들어올 때 이미지가 받아와지는걸 확인했다.

display:none인 DOM은 가시성이 없어서 렌더 트리에 제외되지만, 해당 DOM 내부에 있는 리소스들은 DOM을 파싱할 때 리소스를 발견하고 로드를 시작하게 된다.

3. 자바스크립트로 적용한 애니메이션을 삭제하고 CSS keyframe을 활용

fadeout 애니메이션을 구현하기 위해 자바스크립트 애니메이션을 사용하고 있었고 아래와 같은 문제점이 발생했다.

문제점

  1. 한 번만 사용되는 애니메이션을 위해 isShow라는 상태가 사용되었고 이로 인해 불필요한 리렌더링이 발생하였다.
  2. CSS 애니메이션은 초기로딩 때 즉시 적용되지만 자바스크립트를 사용한 애니메이션은 컴포넌트 마운트 이후에 실행되기 때문에 약간의 지연이 발생했다.
  3. 코드적으로 CSS에 비해 자바스크립트는 복잡하고 선언적이며, 또한 setTimeout을 사용하고 있어 정확한 타이밍 제어가 어렵다.

자바스크립트 사용시

function App(){
  const [isShow, setIsShow] = useState(false);

  useEffect(() => {
    const showTimeout = setTimeout(() => {
      setIsShow(() => true);
    }, 400);

    const hideTimeout = setTimeout(() => {
      setIsShow(() => false);
    }, 2200);

    return () => {
      clearTimeout(showTimeout);
      clearTimeout(hideTimeout);
    };
  }, []);

return(
  
   <StyledNav className={isShow ? "active" : ""}>배경음악이 준비되어 있습니다.</StyledNav>
  )

}

const StyledNav = styled.div`
  @media (max-width: 360px) {
    max-width: 360px;
  }

  width: 100%;
  max-width: 435px;
  height: 48px;
  position: absolute;
  top: 0;

  transform: translateY(-48px);

  transition: all 0.3s ease-in-out;
  display: flex;
  justify-content: center;
  align-items: center;

  font-size: 1.4rem;

  background-color: #f0ede6;
  color: #606060;
  &.active {
    transform: translateY(0);
  }
`;
  

css 사용시


function App() {
  return (
      <StyledNav>배경음악이 준비되어 있습니다.</StyledNav>
  );
}


const slideInOut = keyframes`
  0% {
    transform: translateY(-48px);
  }
  10%, 90% {
    transform: translateY(0);
  }
  100% {
    transform: translateY(-48px);
  }
`;

const StyledNav = styled.div`
  @media (max-width: 360px) {
    max-width: 360px;
  }

  width: 100%;
  max-width: 435px;
  height: 48px;
  position: absolute;
  top: 0;

  transition: all 0.3s ease-in-out;
  display: flex;
  justify-content: center;
  align-items: center;

  font-size: 1.4rem;

  background-color: #f0ede6;
  color: #606060;

  /* 애니메이션 적용 */
  animation: ${slideInOut} 3.5s ease-in-out;
  animation-fill-mode: forwards;
`;

4. preload와 이미지 메모이제이션

preloadImage함수를 이용해서 이미지를 미리 로드해오도록 수정하였다.

이미지를 미리 로드함으로써 사용자가 실제로 이미지를 보게 될 때 이미 브라우저 캐시에 있어 즉시 표시되는 장점이 있다.

또한 React.memo를 사용해 이미지를 렌더링하는 컴포넌트를 최적화하고 불필요한 리렌더링을 방지하게끔 했다.

const preloadImage = (src) => {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = resolve;
    img.onerror = reject;
    img.src = src;
  });
};

const OptimizedImage = React.memo(({ src, alt, ...props }) => {
  useEffect(() => {
    preloadImage(src);
  }, [src]);

  return <StyledImage src={src} alt={alt} {...props} />;
});



export default function Starting() {
  const imageUrl = useMemo(() => `${process.env.PUBLIC_URL}/image/main-card.webp`, []);

  useEffect(() => {
    // Preload the main image as soon as the component mounts
    preloadImage(imageUrl);
  }, [imageUrl]);

  return (
    <Container as="section" aria-label="시작">
      <ImgWrapper data-aos="flip-left" data-aos-delay="300" data-aos-duration="1200">
        <OptimizedImage src={imageUrl} alt="메인 사진" />
      </ImgWrapper>
    </Container>
  );
}

5. 번들 사이즈 줄이기

react-mui의 버튼을 사용하고 있었고 import할때 모듈 전체를 import했기 때문에 불필요한 코드도 같이 번들에 포함되었다.

import {Button} from '@mui/material

따라서 아래와 같이 필요한 코드만 import했다.

import MuiButton from "@mui/material/Button/Button";

개선 후

0개의 댓글