티스토리 뷰
프론트엔드 로딩, 랜더링 성능을 전혀 생각하지 않고 개발된 프로젝트를 배포하려고 하니
이상하게 페이지가 느리고 버벅거리는 현상도 종종 나타났다. 그래서 성능에 관심을 가지게 되었고
메모이제이션이나 눈에 띄는 레이아웃 변경에 따른 reflow방지 외에
웹페이지 성능개선을 위해 노력한 것들이다.
부족한 점을 파악하기 위해 lighthouse를 이용하고 유동균 님의 인프런 강의를 참고했다.
가장 급한건 '텍스트파일 gzip형식으로 압축하기'이다
lighthouse 검사결과 텍스트 압축사용을 하지 않았다는 것이 가장 큰 성능저하요인으로 나왔다.
텍스트 압축사용이 뭔고 알아보니
https://vnthf.github.io/blog/Front-Gzip%EC%97%90-%EA%B4%80%ED%95%98%EC%97%AC/
[Front] Gzip에 관하여
웹서비스 성능향상을 시켜주는 Gzip의 사용법과 설명
vnthf.github.io
이런 글이 있었고, 실제로 우리 프로젝트의 자바스크립트 파일 용량이 굉장히 크다.
1mb만 넘어도 많다고 하는데 3mb가 넘었다.
chunk파일에 대체 뭐가 들어있나? 보려고 cra-bundle-analyzer를 설치해서 번들파일을 눈으로 확인해 보니
우리 프로젝트의 전체 라이브러리와 전체코드가 다 들어있다. 그중에 가장 눈에 띄는 것은 echart라는 라이브러리였는데
parsed size가 무려 1mb에 달했다.. 전체 코드가 3mb인데 echart라이브러리 하나가 1mb라니!
여러 라이브러리를 비교해 보고 react-chartjs-2 라이브러리로 갈아타려고 했으나 배포까지 시간이 부족하여 일단 다음배포로 연기하고
gzip으로 압축하고 lazy loading을 적용하여 필요 없는 페이지에서는 echart라이브러리가 들어있는 chunk는 불러오지 않도록
처리하자!
보통 리액트 프로젝트에 gzip압축 설정을 하는 방법을 찾으면 인스턴스의 nginx설정으로 하는 방법이 나오거나
nextjs를 사용하는 경우 프론트엔드 서버를 직접 구성하므로 express서버에서 압축 설정을 하는 방법이 나온다.
우리 프로젝트의 역사는 잘 모르지만 특이하게 java 코드로 프론트엔드 서버를 구성한다.
2년 전에 마지막 수정이었는데 그 당시에 Nextjs를 왜 적용하지 않았는지,
아니면 회사에 nodejs로 express서버를 만들 줄 아는 사람이 없었는지는 알 수 없다.
아무튼 나는 이 웹서버 역할을 하는 java코드에서 gzip압축설정을 해주고 싶다. 백엔드 개발자님의 도움을 받아 코드 수정
// application.yml
server:
compression:
enabled: true
mime-types: text/html,text/plain,text/css,application/javascript,application/json
min-response-size: 2048
프로젝트 설정값을 application.yml파일에서 설정할 수 있다. 위와 같이 설정하면 정적파일을 서빙할 때 gzip으로 압축해 준다.
gzip으로 압축된 파일들은 평균적으로 약 70%의 용량이 압축되었다.
2kb 미만의 작은 파일들은 압축하지 않는다.
그 이유는 압축해서 용량을 줄이는 이점보다 브라우저에서 압축을 푸는 데 걸리는 시간이 더 크다고 한다.
이제 가장 컸던 echart라이브러리가 들어있는 chunk를 splitting 해서 꼭 필요할 때만 lazy loading 해주자
import { lazy, Suspense } from "react"
const LazyChart = lazy(()=>import('./path'))
const Component = () => {
return (
<>
//... 기존 view 내용
<Suspense fallback={null}>
<LazyChart />
</Suspense>
</>
)
}
export default Component;
lazy loading을 적용할 때는 chunk를 아직 받지 못했을 때 보여줄 컴포넌트를 지정하기 위해 Suspense로 꼭 감싸야한다.
나는 테스트를 위해 null로 지정했다. 이제 결과물을 보자
위는 최적화 작업을 시작하기 전의 상태이다. chunk파일이 무려 2mb, 1.2mb짜리 두 개를 무조건 첫 페이지에서 받아온다
위는 route를 기준으로 lazyloading을 적용해서 코드를 분리해 놓은 상태이다.
chunk가 나눠지긴 했지만 계산해 보면 모든 파일의 용량은 크게 다르지 않았다.
(블로그 작성 시점에는 몇 개의 라이브러리를 정리해서 용량이 조금 줄긴 했다.)
위는 echart 라이브러리가 필요한 코드를 Lazy loading 적용한 후다.
833kB짜리 chunk다운로드를 생략하고 페이지를 로딩한다.
위는 gzip압축이 적용된 후의 모습이다. chunk들의 용량이 많이 줄어들었다. 그럼 차트를 랜더링 해보자
차트를 랜더링 하는 순간 339kb의 chunk를 다운로드하고 차트가 등장했다. gzip압축도 잘 되었고 청크도 분리해서
lazy loading을 적용했다. 이제 우리 페이지에 처음 들어올 때는 훨씬 작은 용량의 js파일들을 다운로드하며
당장 필요가 없는 chart 모듈이 들어간 chunk는 다운로드하지 않고 로딩시킨다.
근데 파일크기가 생각보다 크니까 클릭해서 딜레이를 주지 말고 적절한 위치에서 preloading 시켜서 ux를 개선하자
const handleMouseEnter = () => {
import('./path)
}
적절한 위치에 mouseEnter이벤트를 적용해 주면 유저에게 딜레이가 느껴지지 않는다.
그래도 echart라이브러리는 개선해야 한다. chart하나 띄우는데 저렇게 큰 chunk를 받아야 한다니...
조만간 시간을 잡아서 react-chartjs-2로 교체예정
나머지 청크를 보니 이럴 수가... react-flicking이 대체 뭔데 우리 라이브러리 중에 가장 큰 거야?
코드를 살펴보니 carousel을 구현하기 위해 사용하고 있는 라이브러리이다.. 근데 그 아래 framer-motion도 있잖아?
그리고 나중에 보니 react-slick인가 하는 라이브러리도 있더라.
이 프로젝트의 가장 큰 문제는 수많은 개발자가 발을 담갔음에도 코드컨벤션이나
일정한 아키텍처나 폴더구조 같은 게 없고, 같은 역할을 하는 라이브러리도
본인 취향에 맞는 것을 매번 새로 다운로드해서 쓴다는 것이다.
그래서 이런 일이 발생한 것인데, 일단 react-flicking은 사이즈가 가장 크고 같은 기능을 구현할 수 있는 framer-motion도
있으므로 모든 carousel은 framer-motion으로 재구현하고 flicking은 지운다.
flicking으로 구현되었던 캐러셀을 framer-motion으로 변경하니 한 가지 문제점이 생겼다.
flicking으로 구현된 캐러셀은 5장의 이미지를 띄우는데 총 10개의 image element를 dom에 띄워서 중간에 캐러셀이 끊어지지 않고
드래그만으로 바로 다음페이지를 슬쩍 볼 수 있도록 구현되어 있다.
반면에 framer-motion으로 만든 캐러셀은 딱 실제로 화면에 뜨는 image element만 dom에 띄운다.
캐러셀이 넘어가면 애니메이션이 변경되면서 다음 dom이 대체하는 방법으로 구현되어 있다.
그렇다 보니 dom이 뜨고 나서 이미지를 다운로드하기 때문에 네트워크가 느린 환경에서 이미지가 안 보이고
흰 배경만 보이는 상황이 생겼다.
네트워크 throttle을 걸어놓고 확인한 결과이다. 캐러셀이 넘어갈 때 해당 이미지를 다운로드하면서 약 700~800ms의 딜레이가 생겼고
그 사이에 유저는 흰 배경만 보게 되기 때문에 첫 화면부터 이 서비스 뭔가 느리다는 이미지가 생기게 된다.
이미지 프리로딩을 해서 해결해 보자
const ImagePreloader = (srcArr) => {
srcArr?.forEach((imgSrc) => {
const imagePreloader = new Image();
imagePreloader.src = imgSrc;
});
};
useEffect(() => {
ImagePreloader(thumbnails);
}, []);
이미지 생성자 함수로 이미지 객체를 만들고 src에 주소를 입력하는 순간 이미지를 다운로드한다.
나는 컴포넌트 마운트 직후에 캐러셀의 모든 이미지를 사전에 다운로드하도록 했다.
캐러셀에 들어가는 모든 이미지가 사전에 로딩이 되었기 때문에 실제 화면에 보일 때는 캐시를 사용할 수 있다.
소요시간은 800ms -> 1ms로 개선되었다.
결과
gzip 용량 기준으로 35kb? 정도 삭제가 되었다. 큰 차이는 느낄 수 없지만 이런 게 하나하나 모여서 성능이 좋아질 것이라고 믿는다.
성과는 첫 로딩 약 3200kb -> 550kb로 개선되었다. (833kb -> 339kb / 1.1mb -> 290kb / 910kb ->220kb)
페이지 로딩속도는 js파일 다운로드까지 약 780ms 정도에서 약 80ms 정도로 1/10로 단축
마우스를 올려놓은 main 청크 하나만 봐도 360ms에서 43ms로 단축되었다.
css 다운로드까지는 약 1/6 정도로 단축되었다.
다음은 레이아웃 변경이 필요한 부분에 애니메이션을 gpu사용하는 속성 (opacity, transform)으로 교체하여 애니메이션을 개선하고 병목코드를 개선한다.
이후 트러블슈팅
https://pungwa.tistory.com/188
react lazy import 이후 발생한 loading chunk failed 에러 해결
웹사이트 성능개선을 위해 무거운 자바스크립트를 code splitting 해서 초기 진입 속도를 빠르게 개선했다. 그 뒤에 발생한 에러이다. 브라우저를 미리 켜두었던 경우에 발생했다. 페이지를 이동하
pungwa.tistory.com
'React' 카테고리의 다른 글
비제어 컴포넌트(Uncontrolled Components)로 랜더링 개선 (0) | 2023.07.28 |
---|---|
react lazy import 이후 발생한 loading chunk failed 에러 해결 (0) | 2023.05.26 |
처음 고민해본 프론트엔드 아키텍처와 디자인패턴 (0) | 2023.04.16 |
프로젝트 실패 회고 (0) | 2023.04.05 |
[React-Query]Optimistic Updates로 빠른 UI제공하기 (0) | 2023.01.16 |