Hengxi's 개발 블로그

[React] 웹 성능 최적화하기 2 본문

개발/React

[React] 웹 성능 최적화하기 2

HENGXI 2022. 11. 3. 14:31

웹 성능 최적화 방법 2

  • 애니메이션 최적화(Reflow, Repaint)
  • 컴포넌트 Preloading
  • 이미지 Preloading

1. 애니메이션 최적화 (Reflow, Repaint)

애니메이션 원리

연결된 동작을 하는 그림을 빠르게 보여주면 이전 그림이 뇌에서 사라지기 전에 다음 그림을 보면서 연결된 것처럼 받아들이는 원리

일반적인 모니터의 경우에는 1초에 60 FPS(1초에 이미지를 표현할 수 있는 개수)로 이미지를 표할 할 수 있는데, 브라우저도 이에 맞춰 초당 60 FPS으로 렌더링을 하려고 한다. 요즘 좋은 모니터의 경우에는 144 FPS까지 가능하다.

브라우저가 렌더링을 하는 과정에서 특정한 이유를 통해서 한 번의 이미지를 표현할 수 있는 프레임이 작아진다면(30 FPS나 20 FPS가 된다면) 애니메이션이 버벅거리는 쟁크 현상이 발생하게 된다.

그렇다면 왜 브라우저는 60 FPS를 못 그리는 것일까?

브라우저에서 쟁크 현상이 발생하는 이유

브라우저에서 쟁크현상이 발생하는 이유를 알려면 브라우저의 렌더링 과정에 대해서 알아야 한다.

 

브라우저 렌더링 과정

1. 우선 HTML과 CSS를 파싱 하여 각각의 모델로 만들어준다. (DOM, CSSOM)

DOM, CSSOM

2. DOM 트리와 CSSOM 모델을 조합해서 최종적으로 Render Tree라는 데이터 구조를 만들어준다.

Render Tree

3. Layout 단계에서는 위치나 요소에 대한 크기를 실제 계산을 한다.

위치, 크기 계산

4. Layout에 색을 채워 넣는다.

색 채워 넣기

5. 각 레이어 합치는 과정(Composite)을  통해 최종적인 화면을 그려준다.

레이어 합치기

이 전체적인 과정을 Critical Rendering 또는 Path Pixel Pipeline라고 부른다. 만약 완성된 화면에서 일부 스타일이 바뀌게 된다면 변화된 내용을 가지고 렌더링 과정을 다시 거치게 된다.

초당 60FPS에 화면을 빠르게 보여줘야 하는데 짧은 시간 동안 많은 렌더링 과정을 다시 거치게 되면서 브라우저가 중간중간에 화면들을 생략하게 되고 결국에는 쟁크 현상이 발생하게 된다.

 

아래는 변경될 때 렌더링을 일으키는 요소들이며 조금씩 다른 특성을 가지고 있다.

 

Reflow

Width, Height가 변경되면 위의 모든 과정이 다시 실행된다. Rendering 과정 중에서 특히나 Layout이나 Paint가 다시 실행되는데 이러면 컴퓨터의 자원을 많이 소비하게 된다. 

Repaint

color나 background-color 등이 변경되면 위의 과정 중 Layout을 제외하고 실행된다.

Reflow, Repaint를 제외하고 애니메이션 실행하기(GPU 도움받기)

css요소 중 transform, opacity을 변경해주면 CPU 작업이 아닌 GPU가 작업을 대신하여 Layout과 Paint 과정을 생략해서 훨씬 더 빠르게 Rendering을 해줄 수 있다. 

 

그렇다면 아래의 코드는 어떻게 최적화를 시키는 것이 좋을까?

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

Reflow를 일으키는 width를 직접적으로 변경해주는 것이 아닌 GPU 작업을 일으키는 transoform을 활용하여 애니메이션을 실행해 주면 된다.

const Component = 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;
`;

2. 컴포넌트 Preloading

Preloading이란

필요한 리소스 자원을 서버에 요청할 때 여러 자원을 동시에 요청하게 되고, 서버에서는 요청 순서에 상관없이 준비가 되는대로 응답을 하게 된다. 이때 우선순위를 부여하여 특정 리소스를 빠르게 로딩하도록 하는 것이다.

컴포넌트 Preloading 타이밍

1. 버튼위에 마우스를 올려놨을 때

2. 최초페이지 로드 후 모든 컴포넌트의 마운트가 끝났을 때

 

아래 Modal 코드를 preloading 코드로 바꿔보자

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>
    )
}

버튼 위에 마우스를 올려놨을 때와 최초 페이지 로드 후 모든 컴포넌트의 마운트가 끝났을 때  preloading을 함께 구현했다.

파일을 로드하는데 1초 이상이 필요하다면 컴포넌트의 마운트가 끝났을 때 preloading을 하는 것이 좋다.

function lazyWithPreload(importFunction) {
  const Component = React.lazy(importFunction)
// preload라는 키로 함수를 지정하여 호출한다.
  Component.preload = importFunction
  return Component
}

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

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

// 화면이 렌더링 된 후 특정함수를 실행하는 hook
// 미리 import하여 화면이 미리 렌더링 될 수 있도록 한다.
    useEffect(() => {
      LazyImageModal.preload()
    }, [])

// 마우스가 올라 갔을 때 preload가 필요할 시
    // const handleMouseEnter = () => {
    //     const Component = import('./components/ImageModal')
    // }
    
    return (
        <div className="App">
            <Header />
            <InfoTable />
              <ButtonModal
                onClick={() => {
                  setShowModal(true);
                }}
                /* onMouseEnter={handleMouseEnter} 마우스가 올라 갔을 때 */
              >
                올림픽 사진 보기
              </ButtonModal>
              올림픽 사진 보기
            </ButtonModal>
            <SurveyChart />
            <Footer />
            <Suspense fallback={null}>
              {showModal ? <LazyImageModal closeModal={() => { setShowModal(false) }} /> : null}
            </Suspense>
        </div>
    )
}

 2. 이미지 Preloading

이미지를 preloading 하기 위해서는 Image 객체를 만들어준 후 src에 등록해주면 자동으로 이미지를 불러오게 되는데, 불러오면서 자동으로 cache에 저장되게 된다.

js에서 new Image라는 객체를 만들어주면 html의 <img>를 불러오는데,  Image의 객체에. src로 원하는 이미지를 등록해주면 그 이미지를 미리 로드할 수 있다.

아래와 같이 useEffect 같이 모두 마운트 된 이후에 실행되는 hook을 지정하여 이미지를 미리 다운로드하는 함수를 실행한다.

  useEffect(() => {
    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";
  }, []);

cache-control 항목에서 max-age를 확인할 수 있다.

ex) max-age=1800 (=1800초 동안 캐시가 적용)

preloading 시간 비교

preloading
preloading X

 

Comments