프론트엔드 성능 최적화

Dan·2023년 8월 3일
0

실무공부

목록 보기
10/12
post-thumbnail

이미지 사이즈 최적화

  • 이미지 사이즈를 불러올 때 레티나 디스플레이 같은 해상도를 대응하기 위해 기본 이미지 사이즈의 2배가 사용자가 사용하기 편하다.

CDN(Contents Delivery Network)

물리적 거리의 한계를 극복하기 위해 소비자(사용자)와 가까운 곳에 컨텐츠 서버를 두는 기술

Image CDN (Image processing CDN)

  • 이미지 CDN 프로세스를 활용해서 받아올 이미지 사이즈를 미리 줄여서 받아올 수 있도록 한다.

  • 대표적으로 imgix 같은 서비스를 활용하는 방법도 있고 아래와 같은 함수로 조정하는 방식도 존재한다. 하지만 아래와 같은 방식으로 적용시킬려면 서버와 함께 어떤 옵션을 추가할지를 정의해야하는 사전 작업이 필요하다.

/* 파라미터 참고: https://unsplash.com/documentation#supported-parameters */
function getParametersForUnsplash({width, height, quality, format}) {
  return `?w=${width}&h=${height}&q=${quality}&fm=${format}&fit=crop`
}

Bottleneck

버틀넥(병목현상) 이란 엄청난 양의 데이터를 순식간에 처리하다보니 메모리가 이를 제대로 소화하지 못해 성능 저하를 나타내는 현상이다.

  • 아주 긴 길이의 문자열을 받아와 특수문자를 2중 for문을 돌려서 제거한다고 치자, 해당 연산은 굉장히 오래 걸리고 그러므로 로딩시간이 길어지게 될 것이다.

Bottleneck 해결방안

특수 문자를 효율적으로 제거하기

  • replace 함수와 정규식을 사용
  • 마크다운의 특수문자를 지워주는 라이브러리 사용 (remove-markdown)
let _str = _str.replace(/\#\_\*\~\&\;\!\[\]\n\=\-/g, "");

작업하는 양 줄이기

  • 화면에 표시되어야 할 꼭 필요한 양으로 데이터를 줄이기
let _str = str.substring(0,300)

Bundle 파일 분석 (bundle-analyzer)

번들 사이즈는 페이지의 초기 로딩 속도에 큰 영향을 끼친다. 그러므로 번들 사이즈를 줄이면 초기 로딩시간을 개선시킬 수 있다.

  • 먼저 bundle-analyzer 같은 tool를 활용해서 현재 번들이 어떤식으로 이뤄지고 있는지 파악을 해야한다.
  • 위에서는 refractor이라는 모듈이 큰 용량을 차지하는것을 볼 수 있다. 해당 모듈이 모든 페이지에서 사용되는지 확인하고 그것이 아니라면 아래와 같은 방법들로 번들 사이즈를 줄여보도록 하자.

Code Splitting

Code Splitting은 덩치가 큰 번들 파일을 쪼개서 코드를 분할 하는 것을 의미한다.
불필요한 코드 또는 중복되는 코드가 없이 적절한 사이즈의 코드가 적절한 타이밍에 로드될 수 있도록 하는것이 중요하다.

리액트의 코드 분할 방법들에 대해서는 리액트 공식 문서에서 찾아 볼 수 있다.

Route-based code splitting

기본적으로 code splitting을 적용할려면 webpack 설정이 되어있어야 하지만 Cra, Cna로 생성된 프로젝트는 자동으로 옵션이 적용이 되어 있다.

라우트 기반 코드 스플릿팅은 기본적으로 react에서 제공되고 있는 lazy 로딩과 Suspense를 사용해서 할 수 있다.

const ListPage = lazy(() => import("./pages/ListPage/index"));
const ViewPage = lazy(() => import("./pages/ViewPage/index"));

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

적용된 코드를 네트워크 탭을 활용해서 확인 해보면 결과는 아래와 같다.

  • localhost:3000/

  • localhost:3000/view/20

해당 url을 접속했을 때야 비로소 1.chunck.js라는 용량이 큰 파일을 불러오는 것을 볼 수 있다. 이처럼 라우트 상태에 따라 큰 용량을 차지하는 모듈을 필요할떄만 불러와서 사용 할 수 있다.

  • bundle-analyzer

번들 어날라이저를 통해 확인해보면 code splitting을 진행하기 전과 구조가 변한 것을 확인할 수 있다.

  • 퍼포먼스 개선

애니메이션 분석

애니메이션을 분석하기 위해서는 Reflow 와 Repain가 브라우저에서 어떻게 이루어지고 있는지를 알아야한다.

먼저 브라우저 렌더링 과정에 대해 간략하게 정리해보자.

DOM + CSSOM 생성 => Render Tree 생성 => Layout진행 => Paint진행 => Composite 과정으로 이루어져있다. 이 모든 과정을 Critical Rendering Path 아니면 Pixel Pipeline이라고 칭한다.

  1. 브라우저가 HTML, CSS, Javascript 소스를 받아온다.
  2. HTML과 CSS를 가공하여 아래와 같은 트리구조의 DOM, CSSOM을 생성한다.
  3. 가공된 DOM 트리와 CSSOM 트리를 조합하여 Render Tree를 생성한다.
  4. Layout 과정에서는 요소의 위치와 크기를 계산하여 그린다.
  5. Paint과정에서는 그려진 Layout에 색을 채워 넣는다.
  6. Composite에서는 여러개의 레이어들을 합성해서 최종적으로 화면에 그린다.

Reflow 와 Repaint

브라우저의 렌더링 과정을 배워보았다. 브라우저는 화면에 변화가 있을때마다 위와 같은 과정을 반복적으로 실행하여 새로운 화면을 그리게 되는데 그중 제일 성능적인 부분을 많이 차지하는게 Layout과 Paint하는 과정이다. 새로운 화면을 그리는 과정에서 이 두가지의 과정을 생략할 수 있게 된다면 성능적으로 크게 개선할 수 있게 된다.

Reflow

Layout 과정을 다시 진행하는걸 Reflow라고 한다. 요소의 위치나 크기를 변화 시키는 css가 변했을 시 Reflow가 발생하는 원리이다. 아래와 같은 속성들을 변경하는걸 최대한 지향하면 Reflow가 발생하지 않기 떄문에 사용을 줄여주는게 좋다.

Repaint

Layout에 색을 다시 채우는 과정을 Repaint라고 한다. 만약 색을 채우는 과정인 paint 속성에만 변화를 줬을 경우 Reflow과정을 생략하고 Repaint를 진행 할 수 있다.

Reflow, Repaint 생략

transform, opacity는 웹브라우저의 GPU가 관여할 수 있는 속성이기에 Relfow 및 Repaint 과정을 생략할 수 있는 방법이다. 그리하여 transform 및 opacity를 다른 속성을 대신해서 사용할 수 있는 경우라면 적극적으로 사용해주자.

애니메이션 최적화

위와 같은 Progressbar를 최적화해보도록 하자.

  • 최적화 전 코드

최적화 전 코드를 보면 width라는 props를 받아서 width를 조정해서 reflow를 발생시켜 animation을 보여주고 있다. 이와 같은 방법은 렌더링 과정을 처음부터 끝까지 다시 진행하므로 성능적인 부분에 영향을 미치게 된다. 좀 더 복잡한 애니메이션이 들어가게 될 경우 사용자 입장에서도 버벅임을 보이게 될 수 있다.

  width: ${({width}) => width}%;
  transition: width 1.5s ease;
  • 최적화 후 코드

Reflow와 Repaint를 생략할수 있는 transform 속성을 활용해서 아래와 같이 최적화를 했다.

  width: 100%;
  transform: scaleX(${({ width }) => width / 100});
  transform-origin: center left;
  transition: transform 1.5s ease;

수정 후 performance 변화

  • transform을 사용했을 떄 맨 위의 초록색 그래프인 fps가 상당히 일정하다는것을 알 수 있다.
  • 또한 아래 그래프인 쓰레드도 tranform 속성의 경우에는 GPU를 사용하기 떄문에 쓰레드를 거의 사용하지 않는 것을 알 수 있다.

Preloading

Preloading 기법은 화면에 보여줘야할 용량이 큰 파일이나 컴포넌트를 미리 로딩시켜놓고 필요할 떄 보여줄수 있도록하여 로딩 지연시간을 단축시켜주는 기법이다. 위에서 배운 Code Splitting 기법과 조합하여 적절하게 사용하면더 좋은 성능 최적화를 할 수 있다.

Component Preloading

기본적으로 컴포넌트를 preloading하는 기법은 두 가지가 있다. 두 가지 기법을 상황에 따라 적절하게 사용하면 된다.

  1. 이벤트가 발생하기 직전을 감지해서 Preloading
  2. 화면이 Fully Mount된 시점에 Preloading

이벤트가 발생하기 직전을 감지해서 Preloading

  • Code Splitting과 Preloading을 조합해 만든 공통함수
function lazyWithPreload(importFunction) {
  const Component = React.lazy(importFunction);
  Component.preload = importFunction;
  return Component;
}
  • mouseEnter 이벤트를 활요해서 Preloading
function App(){
    const handleMouseEnter = () => {
      LazyImageModal.preload();
   };


	return (
      <ButtonModal
        onClick={() => {
          setShowModal(true);
        }}
        onMouseEnter={handleMouseEnter}
      >
        올림픽 사진 보기
      </ButtonModal>
    )
}

화면이 Fully Mount된 시점에 Preloading

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

이미지 Preloading

이미지를 Preloading하는 방법은 new Image() 객체를 미리 생성해 놓는 방법이다. 이미지 프리로딩 같은 경우에는 꼭 첫화면에 제일 먼저 보여져야할 이미지만 해주는 것이 좋다. 예를 들어 모달창을 띄었을 때 보이는 첫번째 이미지만 프리로딩하지 않고 모달창에 들어가는 모든 이미지를 preloading 해놓는건 오히려 성능에 악영향을 끼칠 수 있다.

  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";
  }, []);
profile
만들고 싶은게 많은 개발자

0개의 댓글