Archive

리액트 웹 성능 최적화

|

리액트 웹 성능 최적화


웹 성능 결정 요소

  • 로딩 성능 : 리소스를 불러오는 성능

  • 렌더링 성능 : 불러온 리소스를 화면에 그려주는 성능


1. 브라우저의 렌더링 원리

  1. HTML => DOM, CSS => CSSOM 가공하여 tree 구조로 세팅

  2. tree를 조합하여 Render Tree 생성

  3. Layout 연산 (width, height 등)

  4. Layout 위에 Paint (color, shadow 등)

  5. Composite 단계에서 각 레이어를 합성

  • 이 전체 과정을 Critical Rendering Path 혹은 Pixel Pipeline이라 한다
  • width, height 등 크기 변경 시, 1~5 전 과정을 실행 => Reflow
  • color, background-color 등 색깔 변경 시, 3(Layout 연산) 스킵 => Repaint
  • Reflow
    • position, width, height, left, top, right, bottom, margin, padding, border, display, float, font-family, font-size, font-weight, line-height, overflow, min-height, text-align, vertical-align…
  • Repaint
    • background, background-*, border-radius, border-style, box-shadow, color, line-style, outline, outline-*, text-decoration…

2. Performance 패널을 이용한 분석

  • 개발자도구의 Performance 탭에서 분석이 가능
  • Frame Chart를 보며 시간이 오래 걸리는 컴포넌트, 함수를 리팩토링

3. Lighthouse 패널을 이용한 분석

  • 개발자도구의 Lighthouse로 퍼포먼스 점수 분석이 가능
  • Opportunities나 Diagnostics에 적힌 내용들을 수정하여 최적화

4. Network 패널을 이용한 분석

  • 개발자도구의 Network 패널을 이용해 network 통신 분석이 가능

5. Coverage 패널을 이용한 분석

  • 개발자도구에서 Coverage 패널로 리소스의 사용량을 체크해볼 수 있다

6. webpack-bundle-analyzer를 이용한 분석

  • webpack-bundle-analyzer 패키지 설치 후 사용
  • CRA는 cra-bundle-analyzer 사용
  • npm 패키지가 불필요한 페이지에서 사용될 경우 분리시킬 필요가 있다
  • code splitting 적용

7. 텍스트 압축

  • 로딩 성능 최적화
  • 서버에서 받는 리소스를 압축해서 받고 풀어서 클라에게 전송
  • Response Header에 Content-Encoding 항목이 있으면 압축된 것
  • 리소스를 제공하는 서버나 Router 서버에서 Compression을 적용
  • 무분별하게 압축하면 오히려 성능 저하가 올 수도 있음
  • 기본적으로 2KB 이상일 경우 compression 실행

8. 이미지/동영상 사이즈 최적화

  • 로딩 성능 최적화
  • 이미지 사이즈 최적화
    • 애초에 이미지 사이즈를 줄이거나 코드를 통해 이미지를 줄여라
    • 구글에서 제공하는 WEBP도 있음, 지원 브라우저 확인 필요
    • JPG보다 용량, 화질면에서 뛰어남
    • squoosh.app 구글에서 제공하는 컨버터
    • 실제 태그 크기가 300px 이라면 보통 2배로 해서 이미지를 600px로 리사이징
    • webp 지원하는 브라우저 분기 처리 방법
<!-- source 태그가 안먹히면 다음으로 img 태그가 먹힌다 -->

<picture>
	<source srcset="logo.webp" type="image/webp">
    <img src="logo.png" alt="logo">
</picture>
  • 비디오 사이즈 최적화
    • media.io 등 동영상 압축기를 사용
    • 구글에서 제공하는 WEBM 확장자 사용, 지원 브라우저 확인 필여
    • webm 지원하는 브라우저 분기 처리 방법
<!-- webm이 안먹히면 그 다음으로 mp4가 먹힌다 -->

<video>
	<source src="video.webm" type="video/webm">
    <source src="video.mp4" type="video/mp4">
</video>

9. 이미지 CDN을 통한 최적화

  • 로딩 성능 최적화

  • 이미지를 불러올 때, CDN으로 호출하여 직접 구현하거나 imgix 등 툴을 이용하여 필요한 크기로 줄여서 불러옴

10. 캐시 최적화

  • Lighthouse 검사 시, Serve static assets with an efficient cache policy 경고문
  • 브라우저의 캐싱방법은 메모리 캐시, 디스크 캐시가 있음
    • 메모리 캐시 : RAM에 정보를 저장하여 읽어들임
    • 디스크 캐시 : 파일 형태로 정보를 저장하여 꺼내서 읽어들임
  • 서버에서 리소스를 줄 때, cache-control을 헤더에 넣어서 캐시에 대한 설정이 가능
    • no-cache : 캐시를 사용하기 전, 서버에 검사 요청 후 사용 결정
    • no-store: 캐시 사용 안함
    • public : 모든 환경에서 캐시 사용 가능
    • private : 브라우저 환경에서만 캐시 사용, 외부 캐시 서버에서는 사용 불가
    • max-age : 캐시의 유효시간 (max-age=0은 no-cache와 같다, 만료가 되면 바로 지워버리는게 아니라 서버에 검사 요청하여 사용을 결정하기 때문)
  • 서버에서는 리소스가 만료된 것인지 어떻게 알까?
    • 리소스마다 ETag의 해시값이 있어서 이 값을 비교하여 확인한다

11. 이미지 preload

  • 로딩 성능 최적화
  • javascript의 Image Object를 이용하여 preload를 구현할 수 있다
import React, { useEffect } from 'react';

function App() {
    useEffect(() => {
        const img = new Image();
        img.src = 'https://경로';
    }, []);
    
    retrun (/* ... */)
};
  • img.src가 실행되는 순간 네트워크 통신을 하기 때문에 캐싱이 필요하다
  • 이미지의 cache-control 설정은 따로 찾아보도록..
  • 모든 이미지를 캐싱하여 보관하면 오히려 다른 성능에 영향일 미칠 수 있으므로, 제일 중요하고 큼지막한 이미지를 위주로 선정하자

12. 컴포넌트 preload

  • 로딩 성능 최적화
  • 사용자의 상호작용이 필요한 컴포넌트를 미리 load하여 가지고 있는다
  • 하지만 그 시점이 애매하다
  • 버튼의 경우, 버튼을 누를지 안누를지 모르기 때문
    • 버튼 위에 마우스를 올렸을 때 적용 가능
    • 최초 페이지 로드 후 모든 컴포넌트의 마운트 후에 적용 가능
  • 팩토리 패턴을 이용한 preload 방법
import React, { useEffect, Suspense, lazy } from 'react';

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

const LazyModal = lazyWithPreload(() => import('./components/Modal'));

function App() {
    
    useEffect(() => {
        LazyModal.preload();
    }, []);
    
    return (
    	<Suspsnse fallback={null}>
        	<LazyModal />
        </Suspsnse>
    );
};
  • 컴포넌트의 마운트가 모두 완료된 후, lazy를 먹인 import를 실행하는 방법

13. Lazy Load

  • 로딩 성능 최적화
  • React의 suspense, lazy 사용
  • Next.js Dynamic import 사용
  • 최초 로딩 속도는 향상시킬 수 있으나 그 후에는 성능이 오히려 떨어질 수도 있다
  • 따라서, preload를 적용하여 이를 보완한다

14. Code Splitting

  • 로딩 성능 최적화
  • 불필요, 중복되는 코드가 없이 적절한 사이즈의 코드가 적절한 타이밍에 로드되도록

15. 이미지 Lazy Load

  • 로딩 성능 최적화

  • Intersection Observer를 사용하여 이미지 지연로딩 처리가 가능하다
  • 스크롤 이벤트는 매 스크롤 마다 함수가 실행되어 성능 최적화에 맞지 않다
import React, { useEffect, useRef } from 'react';

function App() {
    const ref = useRef(null);
    
    useEffect(() => {
  		const opts = {};
        const cb = (entries, observer) => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    entry.target.src = entry.target.dataset.src;
                    observer.unobserve(entry.target);
                }
            });
        };
        const observer = new IntersectionObserver(cb, opts);
        
        observer.observe(ref.current);
    }, []);
    
    return (
    	<div>
        	<img data-src={"https://경로"} ref={ref} />
        </div>
    )
};
  • 스크롤을 내리다 이미지가 observe되는 순간 data 속성으로 박아놓은 경로를 src에 넣어준다
  • 나중에 로드해도 되는 이미지를 지연로딩 함으로서, 초기 로딩 속도를 개선할 수 있음

  • react-lazyload 패키지를 이용한 방법
import Lazyload from 'react-lazyload';

function App() {
    return (
    	<div>
        	<Lazyload>
            	<img src="" />
            </Lazyload>
        </div>
    );
}
  • react-lazyload 패키지는 Scroll Event를 기반으로 작성되어있다

16. 병목 코드 제거

  • 렌더링 성능 최적화
  • 로직의 연산 자체가 오래걸린다면 구조를 변경하는 최적화가 필요하다

17. repaint, reflow 줄이기

  • 렌더링 성능 최적화
  • GPU의 도움으로 repaint, reflow를 줄일 수 있다
  • transform, opacity 등 GPU가 관여할 수 있는 속성을 사용하여 Layout, paint 과정을 생략할 수 있다

18. 불필요한 CSS 제거 방법

  • Lighthouse의 Remove unused CSS 경고문
  • PurgeCSS를 사용하면 jsx태그와 css파일을 비교하여 사용하지 않은 className들을 필터링해준다
  • config 파일로 커스텀이 가능

19. Layout Shift 피하는 방법

  • 렌더링 성능 최적화
  • Layout Shift : 엘리먼트가 렌더링되면서 영역이 밀리는 현상
  • 개발자도구의 Performance 패널에서 Layout Shift 확인 가능
  • Lighthouse로 검사 시, Cumulative Layout Shift 수치를 0 ~ 1로 나타내어 준다\
  • 이미지 사이즈가 정해져있지 않거나, 동적으로 삽입된 콘텐츠, 웹폰트 등에 의해 발생
  • 이미지 사이즈를 정하는 방법
    • 이미지 태그 위에 부모태그를 감싸고 padding-bottom을 비율대로 설정
    • 이미지 태그를 width, height: 100%로 맞추고 position: absolute로 조정
    • 영역이 미리 잡혀있으므로 Layout Shift가 발생하지 않는다

20. useSelector 렌더링 문제 해결법

  • 렌더링 성능 최적화
function App() {
    const { a, b } = useSelector((state) => ({
        a: state.a,
        b: state.b,
    }));
}
  • 위처럼 useSelector 안에 Object로 Redux의 값을 가져오게 되면 useSelector 안의 콜백함수는 매번 다른 Object를 생성하게 되므로 a, b 이외의 전혀 상관없는 state가 바뀌었을때에도 불필요한 리렌더링이 발생하게 된다
  • Object 쪼개기
    • a, b가 string, bool 등이라면 이전 값과 비교했을때 같은 값이면 리렌더링이 안됨
function App() {
    const a = useSelector((state) => state.a);
    const b = useSelector((state) => state.b);
}
  • Equality Function 사용하기
    • react-redux에서 제공하는 shallowEqual을 두 번째 인자로 전달한다
    • shallowEqual은 a와 b 처럼 object의 최상위에 있는 값만 비교하여 값이 바뀌었을때에만 리렌더링이 일어난다
    • 주의할점은 a와 b가 object일 경우, 그 안의 값들까지 deep하게 비교하지 않는다는것
import { useSelector, shallowEqual } from 'react-redux';

function App() {
    const { a, b } = useSelector((state) => ({
        a: state.a,
        b: state.b,
    }), shallowEqual);
}

21. Redux Reselect를 통한 렌더링 최적화

  • 렌더링 성능 최적화
  • reselect 패키지로 selector를 만들어서 사용
  • input에 대한 리턴값을 캐싱해두고 state가 바뀌지 않으면 캐싱된 값을 재사용한다
import { createSelector } from 'reselect';

function App() {
    const selector = createSelector(
    	[
            (state) => state.a,
            (state) => state.b,
        ], // 첫번째 인자에 store에서 꺼내올 데이터
        (data, data2) => (
        	data === 'test'
            	? data2
            	: data2.filter((item) => item.id !== data);
        ) // 위에서 가져온 데이터를 가공할 콜백함수
    );
    
    const filtered = useSelector(selector);
}

22. memoization을 이용한 최적화

  • 렌더링 성능 최적화
  • input에 대해 return 값이 나오는 함수가 있을때, input에 대한 return 값을 캐싱해두고 동일한 input이라면 캐싱되어있는 값을 return한다
  • 중요한 것은 이 함수는 Pure Function이어야한다
  • 동일한 input이 반복적으로 입력되며, 로직 자체가 복잡한 구조를 가질때 적용해볼 수 있다
  • 아무데에서나 적용하면 결국 메모리 낭비로 이어질 수 있으므로 주의
const cache = {};

export function memoization(value) {
    if (cache.hasOwnProperty(value)) {
        return cache[value];
    }
    const returnValue = {
        a: 0,
        b: '',
    };
    
    // logic..
    
    cache[value] = returnValue;   
    return returnValue;
}
  • 함수마다 매번 이 캐싱 기능을 붙이는 것은 너무 번거로우므로 팩토리 패턴을 사용해서 util을 제작한다
  • 인자가 여러개면 결국 항상 새로운 배열이 생성되므로 key값이 매번 달라지게 된다
  • 생각해볼점
    • 여러개의 인자를 키값으로 활용하는 방안
    • 인자가 Object일때 동일한 key값으로 인식하는 방법
function memoiz(fn) {
    const cache = {};
    
    return function (...args) {
        if (args.length !== 1) {
            return fn(...args);
        }
        
        if (cache.hasOwnProperty(args)) {
        	return cache[args];
    	}
        
        const result = fn(...args);
        cache[args] = result;
        
        return result;
    };
}

23. 폰트 최적화

  • FOUT (Flash of Unstyled Text) : IE, Edge 등 텍스트 먼저 보여주고 웹폰트 적용
  • FOIT (Flash of Invisible Text) : Chrome 등 웹폰트 적용 전까지 텍스트 안보여줌
  • 폰트 적용시점 컨트롤
    • font-face의 font-display 속성 사용
@font-face {
    font-family: aaa;
    src: url('/경로');
    font-display: block;
}
  • fontfaceobserver 패키지로 폰트가 로드되는 시점을 캐치하여 애니메이션을 걸어서 사용자 거부감을 줄이는 등 활용이 가능

  • 폰트 사이즈 줄이기
  • 웹폰트 포맷 사용 : TTF/OFF > WOFF > WOFF2 (IE11 미지원)
  • 변환기 사용 transfonter.org
/* 지원하지 않는 브라우저 대응 */

@font-face {
    font-family: myfont;
    src: url('./경로/font.woff2') format('woff2'),
    	 url('./경로/font.woff') format('woff'),
         url('./경로/font.ttf') format('truetype');
}
  • local 폰트 사용
@font-face {
    font-family: myfont;
    src: local('myfont'),
    	 url('./경로/font.ttf');
}
  • Subset 사용 : 폰트 전체 중 필요한 글자만 잘라서 사용
    • 변환기의 Subsets, Characters 옵션에서 원하는 글자만 선택이 가능
  • Unicode Range 사용 : 설정한 글자가 나왔을때만 폰트 적용
@font-face {
    font-family: myfont;
    src: url('./경로/font.woff2') format('woff2');
    font-display: block;
    unicode-range: u+0041; /* A */
}
  • data-uri로 변환하여 사용
    • 변환기로 인코딩 시 base64 encoding 옵션을 체크
    • 변환해서 나온 css 파일을 열어서 src: url() 부분을 똑같이 적용한다
    • 네트워크 발생 없이 폰트를 불러오는 것을 볼 수 있음
  • font preload하여 사용
    • webpack 사용 시 빌드 시점에 preload가 적용되도록 preload-webpack-plugin 패키지 등으로 config 파일에 추가해야됨
<link rel="preload" href="폰트.woff2" as="font" type="font/woff2" crossorigin>



참고 자료


프론트엔드 개발자를 위한, 실전 웹 성능 최적화(feat.React)