리액트 웹 성능 최적화
25 Oct 2021 | React리액트 웹 성능 최적화
웹 성능 결정 요소
-
로딩 성능 : 리소스를 불러오는 성능
-
렌더링 성능 : 불러온 리소스를 화면에 그려주는 성능
1. 브라우저의 렌더링 원리
-
HTML => DOM, CSS => CSSOM 가공하여 tree 구조로 세팅
-
tree를 조합하여 Render Tree 생성
-
Layout 연산 (width, height 등)
-
Layout 위에 Paint (color, shadow 등)
-
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)