Front-End/성능개선

[웹 성능 최적화] - 애니메이션 최적화, 컴포넌트 Lazy loading, Preloading(3/3)

딸기케잌🍓 2023. 2. 8. 16:44

먼저 브라우저 렌더링 개념을 알아보겠습니다.

Critical Rendering 또는 Path Pixel Pipeline이라는 과정을 통해 브라우저는 렌더링을 하게 되는데 

DOM 트리 + CSSOM -> Render Tree -> Layout -> Paint -> Composite 단계로 이루어집니다.

 

DOM 트리 + CSSOM

먼저 서버로부터 화면을 그릴 때 필요한 HTML, HTML, JS를 다운로드 받습니다.

HTML과 CSS 로 DOM트리, CSS object model 2가지 데이터 형태를 만들고 

DOM 트리와 CSSOM(css object model)을 조합하여 Render Tree를 만듭니다.

 

Layout

요소의 위치나 크기를 계산합니다. 화면의 레이아웃을 잡는 과정입니다.

Paint

색을 채워넣는 과정입니다.

 

Composite

각 레이어를 합성하는 과정으로 최종적으로 화면을 그립니다.

 

 

애니메이션 최적화

애니메이션이 부드럽게 보이려면 1초에 60 프레임 정도가 적절합니다. 이는 0.016초에 1번씩 화면을 그려야 하는데 짧은 시간에 앞서 설명한 Critical Rendering 과정을 거치려니 브라우저가 중간중간 단계를 드롭하다보면 매끄럽지 않고 조금씩 버벅거리게 보입니다. 

 

해결책은 무엇일까용!?

비용이 많이드는 Layout, Paint 과정을 건너띄는 것입니다.

width, height등의 요소들의 위치와 크기가 변경되면 Critical Rendering의 모든 과정이 재실행이 되는데 이를 Reflow라고 합니다.

color, background-color 등 색상을 변경 할 때는 Layout 과정만 생략 가능한데 이를 Repaint 라고 합니다.

 

GPU의 도움을 받아서 Reflow나 Repaint를 모두 피할 수 있는 방법도 있습니다!!두둥

transform, opacity(GPU가 관여할 수 있는 속성)를 변경하는 것인데 스타일의 변경이므로 렌더 트리는 생성하지만 layout, paint 과정은 거치지 않습니다.

 

 

다음은 애니메이션을 최적화할 바 게이지의 움직임입니다.

Bar를 클릭하면 조금 버벅이면서 게이지가 올라가는 것이 보입니다.

요소 검사쪽을 보시면 클릭에 따라 width가 변하고 있습니다.

width가 변경되므로 reflow가 발생하고 있다고 짐작할 수 있습니다.

코드를 보면,

기존에는 다음과 같이 width로 Bar의 가로길이를 잡아주고 있었습니다.

const BarGraph = styled.div`
    position: absolute;
    left: 0;
    top: 0;
    width: ${({width}) => width}%;
    transition: width 1.5s ease;
    height: 100%;
    background: ${({isSelected}) => isSelected ? 'rgba(126, 198, 81, 0.7)' : 'rgb(198, 198, 198)'};
    z-index: 1;
`
d

Performance 탭에서 녹화를 하고 Bar를 클릭했을 때의 그래프 입니다.

빈번하게 Rendering과 Painting이 일어나는것을 알 수 있습니다.

 

 

그럼 버벅이는 애니메이션 최적화를 해볼까요?

제일 처음 언급한대로 GPU의 도움을 받을 수 있는 transform을 이용해보겠습니다.

 

애니메이션 최적화를 위해 Bar의 styled component를 다음처럼 수정했습니다.

transform을 이용해서 reflow, repaint가 일어나지 않게 했고 scaleX를 이용해서 가로 길이만 width 길이만큼 늘려주었습니다.

transform-origin은 transform의 기준이 되는 축인데 왼쪽 끝에 바가 붙어야 하므로 세로는 센터, 가로는 left를 주었습니다.

transition에는 width를 transform으로 수정해줍니다.

const BarGraph = styled.div`
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  transform: scaleX(${({ width }) => width / 100});
  transform-origin: center left;
  transition: transform 1.5s ease;
  height: 100%;
  background: ${({ isSelected }) =>
    isSelected ? "rgba(126, 198, 81, 0.7)" : "rgb(198, 198, 198)"};
  z-index: 1;
`;

먼저 육안으로 확인해봅니다.

조금 더 빨라진게 느껴지시나요!?🤓        

 

다시 성능을 측정해보면,

WOW🤭 눈에 띄게 그래프가 깨끗해진 것을 확인할 수 있네요.우와아아앙

정말 코드 몇줄 바꿨다고 극적인 변화네요.

 

 

 

다음으로는 심심하니까(?)

npx cra-bundle-analyzer

위의 명령어로 cra-bundle-analyzer를 이용해서 번들 분석을 해봅니다.

image-gallery.js 리액트 모듈이 58.65KB로 그렇게 크기가 큰건 아니지만 

당장 첫 화면에서부터는 필요 없고 "올림픽 사진 보기" 버튼 클릭시에 로드가 되면 더 좋아서 최적화를 해보겠습니다.

 

 

 

다음은 App.js의 기존 코드입니다. ImageModal 컴포넌트를 임포트해서 showModal 변수 유무에 따라 모달을 보여주고 있습니다.

import ImageModal from './components/ImageModal'

function App() {
    const [showModal, setShowModal] = useState(false)

    return (
        <div className="App">
            <Header />
            <InfoTable />
            <ButtonModal onClick={() => { setShowModal(true) }}>올림픽 사진 보기</ButtonModal>
            <SurveyChart />
            <Footer />
            {showModal ? <ImageModal closeModal={() => { setShowModal(false) }} /> : null}
        </div>
    )
}

 

컴포넌트 lazy loading을 활용하여 다음처럼 최적화 했습니다.

동적 지연 로딩을 하면 해당 컴포넌트가 렌더링 되어야 할 시점에 관련된 파일들을 서버로부터 받아오기 때문에 성능을 최적화할 수 있습니다.

간단하게 모달 컴포넌트만 lazy loading을 해봤는데 더욱 복잡한 시스템(이 예에서는 모달)일수록 꽤 의미 있는 기법이 될 수 있습니다.

const LazyImageModal = lazy(() => import("./components/ImageModal")); //lazy loading
function App() {
  const [showModal, setShowModal] = useState(false);

  return (
    <div className="App">
      <Header />
      <InfoTable />
      <ButtonModal
        onClick={() => {
          setShowModal(true);
        }}
      >
        올림픽 사진 보기
      </ButtonModal>
      <SurveyChart />
      <Footer />
      <Suspense fallback={null}> //Suspense 추가
        {showModal ? (
          <LazyImageModal
            closeModal={() => {
              setShowModal(false);
            }}
          />
        ) : null}
      </Suspense>
    </div>
  );
}

 

다시 번들 분석을 해보면 image-gallery 모듈이 분할된 것을 볼 수 있습니다.

 

Preloading은 뭔가요!?

모달 컴포넌트가 필요한 시점에 로드하는 것은 좋은데,, 단점이 하나 있습니다.

클릭하고 모달 소스들을 받아오고 그 스크립트를 읽은 후에 모달이 오픈될텐데 이는 사용자에게 '느리네'라고 느낄 수 있기 때문이죠.

이와 같은 단점을 보완하기 위해 Preloading을 합니다.

프리로드 전

 

 

적당한 시점에 미리 모달 소스들을 로드한 후 사용자가 버튼을 클릭하여 모달 컴포넌트가 열릴 때 빠른 사용자 경험을 제공하는 것입니다.

 

프리로딩

프리 로드하는 '적당한 시점'은

  • 마우스를 버튼 위에 올렸을 때 
  • 최초 페이지가 로드 되고 모든 컴포넌트가 마운트 된 후 (componentDidMount)

이렇게 생각해 볼 수 있습니다.

마우스를 버튼 위에 올리고 클릭하기 까지 보통 0.2~0.5초 정도가 소요된다고 하는데 로드할 파일이 너무 커서 로드에 걸리는 시간이 1초 이상이라면 적합한 방법이 될 수 없을겁니다.

그래서 후자가 프리로드 시점이 더 적절할 수 있습니다.

 

모든 컴포넌트가 마운트 된 후 useEffect을 사용하여 프리로드를 해줍니다.

  useEffect(() => {
    const component = import("./components/ImageModal");
  }, []);

 

더 많은 컴포넌트들을 프리로드 해야할 때는 다음과 같이 작성할 수도 있습니다.

function lazyWithPreload(importFunction) {
  const Component = React.lazy(importFunction);
  Component.preload = importFunction;
  return Component;
}

const LazyImageModal = lazyWithPreload(() => import("./components/ImageModal"));

function App() {
...
  useEffect(() => {
    LazyImageModal.preload();
  }, []);
...
}

모든 컴포넌트 마운트 후에 모달컴포넌트가 로드된 모습을 Network 탭에서 확인할 수 있습니다.

 

 

 

이미지도 프리로드 해볼까?

모달에서 보여주는 이미지 중 가장 첫 번째 이미지도 프리로드해서 모달창이 뜰 때 바로 보이도록 하면 더 좋을 것 같습니다.

앞에서 작성한 useEffect에 다음의 코드를 작성해줍니다.

  useEffect(() => {
    LazyImageModal.preload();

    const img = new Image();
    img.src =
      "https://stillmed.olympic.org/media/Photos/2016/08/20/part-1/20-08-2016-Football-Men-01.jpg?interpolation=lanczos-none&resize=*:800";
  }, []);

위와 같이 이미지가 프리 로드 된것을 확인할 수 있습니다!! 이미지 로드는 꽤 간단했네요.

 

 

 

공부 후기

웹 성능 최적화는 왠지 막연하게 어려울거야..하는 느낌이 있었는데 막상 이렇게 공부해보니 분석하는 과정들도 재밌고 조금 더 성능면에서 효율적인 코드를 작성할 수 있다는 점에서 모든 프론트 개발자들이 알아야하는 필수 개념인 것 같아요.

강의에서도 말씀하셨지만 개념을 아는것도 중요한데 직접 해보면서 최적화 해보는 훈련이 제일 중요할 것 같네요.

이상 나름 길었던..?포스팅 마칩니다ㅎㅎㅎ,ㅎ,,,,