프론트엔드 성능 최적화 가이드의 4장을 스터디한 내용입니다
이전 시리즈
2022.11.15 - [웹성능최적화] - 프론트엔드 성능 최적화 가이드 1장 스터디
2022.11.15 - [웹성능최적화] - 프론트엔드 성능 최적화 가이드 2장 스터디
2022.11.17 - [웹성능최적화] - 프론트엔드 성능 최적화 가이드 3장 스터디
이미지 갤러리 최적화
- 해당 서비스는 다양한 주제의 이미지를 격자 형태로 보여줌
- 헤더에는 Random, Animals, Food, Fashion, Travel이라는 버튼이 있음
- 해당 버튼을 클릭하면 그에 속하는 이미지를 필터링해 볼 수 있음
아래 이미지 중 하나를 클릭하면 클릭한 이미지가 큰 화면으로 나타남
배경 색은 이미지 색상과 비슷한 색으로 맞춰짐
모달이 뜨는 절차
- 사용자는 이미지를 클릭함
- 이미지가 클릭되면 리덕스에 SHOW_MODAL 액션을 보냄
- modalvisible 값을 true로 변경 (렌더링)
- ImageModalContainer에서 modalVisible 값을 구독.
- src, alt를 ImageModal 컴포넌트에 전달하고 모달을 띄움
- 이미지가 완전히 로드되면(onLoad) getAverageColorOfImage를 통해 이미지의 평균 색상을 구하고 해당 값을 리덕스에 저장함
- 다시 리덕스 스토어의 상태가 변하고, 최종적으로 변경된 bgColor를 ImageModal에 전달 (렌더링)
이 장에서 학습할 최적화 기법
- 이미지 지연 로딩
- 이전에는 Intersection Observer API를 사용하였음
- 이번에는 react-lazyload 라이브러리를 사용하여 컴포넌트 자체를 지연 렌더링
- 레이아웃 이동 피하기
- 레이아웃 이동(Layout Shift)란 화면상 요소 변화로 레이아웃이 갑자기 밀리는 현상
- 이미지 로딩 과정에서 레이아웃 이동이 많이 발생
- 레이아웃 이동은 사용자 경험에 좋지 않은 영향을 줌
- 리덕스 렌더링 최적화
- useSelector와 equal함수 잘 이용하기
- 파생 상태 리덕스 스토어에서 뽑아내기
- 병목 코드 최적화
- 로직 개선
- 메모이제이션
분석 툴 소개
React Developer Tools(Profiler)
- 얼마만큼의 렌더링이 발생하였는지 보여줌
- 어떤 컴포넌트가 렌더링되었는지 보여줌
- 해당 컴포넌트의 렌더링에 얼마나 시간이 소요되었는지 보여줌
데이터
데이터에는 섬네일과 커다란 사진이 존재함
레이아웃 이동 피하기
- 레이아웃 이동이란 화면 상의 요소 변화로 레이아웃이 갑자기 밀리는 현상
- 아래 이미지보다 위 이미지가 나중에 로드될 경우 아래 이미지를 밀어내면서 그려짐
- 레이아웃 이동은 사용자의 주의를 산만하게 만들고 의도하지 않은 클릭을 유발할 수 있음
- 사용자 경험에 좋지 않은 영향
Lighthouse는 CLS(Cumulative Layout Shift) 항목을 두고 레이아웃 이동을 성능 점수에 포함함
- 0이면 좋음(발생 x)
- 1이면 나쁨
직접적인 원인 파악을 위해 Performance 패널을 확인
- Experience 섹션을 보면 Layout Shift라는 빨간 막대가 표시됨
- 해당 시간에 레이아웃 이동이 발생하였다는 의미임.
- 해당 막대에 커서를 올리면 레이아웃 이동을 유발한 요소를 보여줌
레이아웃 이동의 원인
가장 흔한 경우
- 사이즈가 미리 정의되지 않은 이미지 요소
- 브라우저는 이미지를 다운로드하기 전까지 사이즈를 알 수 없음
- 미리 영역을 확보할 수 없음
- 이미지가 화면에 표시되기 전까지 해당 영역의 높이,너비는 0임
- 이미지가 로드되면 높이가 해당 이미지의 높이로 변경되면서 그만큼 다른 요소들을 밀어냄
- 브라우저는 이미지를 다운로드하기 전까지 사이즈를 알 수 없음
- 사리즈가 미리 정의되지 않은 광고 요소
- 이미지 요소와 원인 동일(이미지 + 영상)
- 동적으로 삽입된 콘텐츠
- 새로운 요소가 추가되면서 다른 요소를 밀어냄
- 웹 폰트(FOIT, FOUT)
- 폰트에 따라 글자 크기가 조금씩 달라 다른 요소의 위치에 영향을 줌
레이아웃 이동 해결
- 레이아웃 이동을 일으키는 요소의 사이즈를 미리 지정
- 즉, 해당 요소의 사이즈를 미리 예측할 수 있다면, 또는 미리 알고 있다면 해당 사이즈만큼 공간을 확보해둠
- 하지만 이미지 갤러리의 이미지 사이즈는 브라우저의 가로 사이즈에 따라 변함
- 따라서 단순히 너비와 높이를 고정하는 것이 아니라, 이미지의 너비 높이 비율로 공간을 잡아줌
- 이번 예제에서는 16:9
이미지 크기를 비율로 설정하는 방법
1. padding을 이용하여 박스를 만들고 그 안에 이미지를 absolute로 띄우기
-
아래와 같이 하면 wrapper의 너비인 160px의 56.25%만큼 상단 여백(padding-top)이 설정됨
-
즉 너비는 160px, 높이는 90px이 됨
-
이 상태에서 이미지를 absoulte로 넣어주면 부모 요소의 div와 사이즈가 동일하게 맞춰짐
- 1대1 비율을 원하면 padding-top을 100% 넣어주면 됨
-
- 퍼센트를 매번 계산해야 함
- 56.25%는 직관적이지 않음
<div class="wrapper">
<img class="image" src="..." />
</div>
<style>
.wrapper {
position: relative;
width: 160px;
padding-top: 56.25%; /* 16:9 비율 */
}
.image {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
</style>
2. aspect-ratio CSS 속성 사용
위의 퍼센트 계산, 비직관적 문제 해결
.wrapper {
width: 100%;
aspect-ratio: 16/9;
}
.image {
width: 100%;
height: 100%;
}
- 자식 요소에 absolute 적용 필요도 없음
- 코드가 간단함
- 대신 호환성을 조심해야 함
이미지 지연 로딩
- react-lazyload 라이브러리 사용
npm install --save react-lazyload
- 지연 로드하고자 하는 컴포넌트를 감싸주기만 하면 됨
- 해당 컴포넌트의 자식 요소들은 화면에 표시되기 전까지 렌더링 되지 않음
- 이미지 뿐만 아니라 일반 컴포넌트 포함
- 이미지가 크면 뷰포트에 들어올 때 로딩하는데 오래 걸릴 수 있음
- offset을 이용하여 미리 당겨올 수 있음
리덕스 렌더링 최적화
리액트의 렌더링
- 리액트는 렌더링 사이클을 갖고 있음
- 상태가 변경되면 화면에 반영하기 위해 리렌더링 과정을 거침
- 렌더링은 메인 스레드의 리소스를 점유하여 성능에 영향을 줌
- 오래 걸리는 렌더링
- 불필요한 렌더링
- React Developer Tools의 Profiler 패널을 이용하여 리렌더링을 확인할 수 있음.
- Components 패널의 설정 버튼 클릭 후 Highlight updates when components render 항목 체크
- 리렌더링 되는 요소의 테두리가 나타남
- 이 표시를 통해 어떤 컴포넌트가 어느 시점에 렌더링 되었는 지 알 수 있음
- 렌더링이 필요없는 경우 최적화
- 이미지를 클릭해서 이미지 모달을 띄웠을 때, 모달만 렌더링되지 않고 모달과 전혀 상관없는 헤더와 이미지 리스트 컴포넌트가 렌더링되고 있음
- 리덕스 스토어를 구독하기 때문
- useSelector로 최적화
조심해야 할 패턴
- 셀렉터 안에서 항상 새로운 객체 만드는 경우
- 셀렉터가 리턴하는 객체 안에서 filter와 같은 신규 객체 만드는 함수 사용하는 경우
const { category, allPhotos, loading } = useSelector(
state => ({
category: state.category.category,
loading: state.photos.loading,
photos : state.category.category === 'all' ? state.photos.data : state.photos.data.filter(photo => photo.category === category);
})
);
useSelector 문제 해결
이 문제를 해결하는 방법은 두 가지가 있음
1. 객체를 새로 만들지 않도록 반환값을 나눔
verbous 하므로 생략
2. Equality Function 사용
useSelector의 두번째 인자로 Equality Function을 적용할 수 있음
필터링 결과 데이터는 useSelector 밖에서 필터링 해야 함
실습 코드 : 모달 리렌더링 최적화 with useSelector
병목 코드 최적화
이미지 모달 분석
병목 코드는 Performace 패널을 이용해 찾아냄
이미지 갤러리 서비스가 느린 지점 찾기
- 페이지가 최초로 로드될 때
- 그렇게 느리지 않음
- 카테고리를 변경했을 때
- 그렇게 느리지 않음
- 이미지 모달을 띄웠을 때
- 이미지가 늦게 뜸
- 사이즈 때문에 어쩔 수 없음
- 배경색이 늦게 변함
- 이미지가 늦게 뜸
- 모달이 뜨는 과정에서 메인 스레드의 작업을 확인하기
- 화면이 완전히 로드된 상태로 Performance 패널의 새로고침 버튼이 아닌 기록 버튼을 클릭
- 이미지를 클릭하여 모달을 띄운 뒤 기록 버튼을 다시 누르면 기록이 종료됨
- 필요에 따라 네트워크 및 CPU에 throttling 옵션 적용
- (1)번에서 이미지 클릭으로 인해 모달이 뜸
- 이미지 모달이 뜨면 모달 안의 이미지를 로드해야 하기 때문에 Network 섹션에서 이미지가 다운로드 됨
- (2)에선 이미지가 모두 다 다운로드됨
- 작업 명으론 식별할 수 있는게 없지만, 우리는 getAverageColorOflmage 함수가 실행됨을 알 수 있음
- 마지막에 Image Decode라는 작업이 보임. 이 작업에서 이미지에 관한 처리를 하고 있음
- 해당 작업은 drawImage 함수(캔버스)의 하위 작업임
- Image Decode 작업이 끝나고 (3)과 같이 새롭게 렌더링 되면서 변경된 배경화면이 화면에 보임
getAverageColorOflmage 함수 분석
- 행당 함수는 이미지의 평균 픽셀 값을 계산하는 함수
- 캔버스에 이미지를 올리고 픽셀 정보를 불러온 뒤 하나씩 더해서 평균을 냄
- 큰 이미지를 통째로 캔버스에 올린다는 점
- 반복문을 통해 가져온 픽셀 정보를 하나하나 더하고 있다는 점
이 코드를 두 가지 방법으로 최적화 시도
메모이제이션으로 코드 최적화하기
메모이제이션이란 입력값에 대해 실행한 함수 결과값을 기억하고 있다가,
동일 입력값 조건이 주어질 경우 함수를 실행하지 않고 기억해둔 값을 반환하는 방법(기술)
예시
export const getAverageColorOfImage = imgElement => {
imgElement.crossOrigin = 'Anonymous';
const src = imgElement.src;
if (cache[src]) {
return cache[src];
}
const averageColor = _getAverageColorOfImage(imgElement);
cache[src] = averageColor;
return averageColor;
};
이렇게 수정한 후, 하나의 이미지에 대해 모달을 여러 번 띄워 보면,
처음에는 배경이 늦게 뜨지만 이후에는 바로바로 뜸
동일한 이미지에 대해 첫번째 실행 했을때(1)보다
두 번째 실행했을 때(2) 배경색이 더 빠르게 바뀐 것을 확인할 수 있음
메모이제이션의 단점
- 첫 번째 실행에서는 여전히 느리다
- 매번 새로운 인자가 들어오는 함수는 메모리만 잡아먹는 골칫거리다
- 동일 조건에서 충분히 반복 실행되는지 먼저 체크한다.
함수의 로직 개선
이 함수(getAverageColorOflmage)의 느린 코드
- 캔버스에 이미지를 올리고 픽셀 정보를 불러오는 drawImage와 getImageData함수
- 모든 픽셀에 대해 실행되는 반복문
이 세가지 요소는 이미지 사이즈에 따라 작업량이 결정됨
즉, 이미지가 사이즈가 작으면 더 빠르게 처리할 수 있으며, 픽셀 수가 줄어들어 반복문의 실행 횟수도 줄어듬
이미지를 작은 사이즈로 교체하는 방법을 생각해 볼 수 있음
즉, 섬네일을 활용함.
- 이미지 사이즈가 작은 섬네일을 활용하여 배경 색을 계산하게 한다면 작업량이 많이 단축될 것임
- 원본 이미지 다운로드 이전에 계산할 수 있어 더욱 빠르게 배경 색을 적용할 수 있음
메모이제이션과 함수 로직 개선 실습
섬네일은 PhotoImage에서 갖고 있으니 배경 색을 해당 컴포넌트에서 계산 후 모달에서 가져다 쓰도록 로직 변경
표시된부분이 ge AverageColoroflmage작업.
이전에 약 3.5초 걸리던 작업이 0.1초 가량으로 줄어든 것으로 보임
'FrontEnd' 카테고리의 다른 글
B2C IT기업 개발자와 B2B IT기업 개발자의 차이점, B2B에 어울리는 인재상 (0) | 2022.11.19 |
---|---|
크롬 개발자 도구의 QueryObjects 객체로 Javascript 메모리 누수 잡기 (0) | 2022.11.19 |
vue3의 반응성(reactivity) 원리 간단 정리 (0) | 2022.11.17 |
프론트엔드 성능 최적화 가이드 3장 스터디 (0) | 2022.11.17 |
프론트엔드 성능 최적화 가이드 2장 스터디 (0) | 2022.11.15 |