⚡ Chrome에서 제공하는 웹사이트 성능 측정 도구, LightHouse를 사용해서 성능 최적화를 해봤다. 2차 프로젝트에 적용했으며, 18점까지도 내려갔던 점수를 90점 이상까지 끌어올릴 수 있었다.
우선 lighthouse에는 다음과 같은 항목이 존재한다.
항목 별 점수를 모두 90점 이상으로 끌어올리는 것이 목표였다.
- ✔ Performance
- 웹 페이지의 로딩 속도 등 실제 성능을 측정한다.
- 🐾 Accessibility
- 웹 페이지의 접근성을 측정한다.
- 🧾 Best Practices
- 웹 페이지가 웹에 대한 표준 모범 사례를 따르고 있는지 확인한다.
- 🔎 SEO (Search Engine Optimization)
- 검색 엔진 수집 최적화가 된 정도를 측정한다.
- 📱 PWA (Progressive Web App)
- 해당 웹사이트가 모바일 애플리케이션으로도 잘 작동하는지 확인한다.
성능 최적화를 진행한 Main 페이지에는 크기가 큰(용량도 큰) 이미지가 많다.
이미지 슬라이드(캐러셀)은 물론이고, 클래스 항목별로 이미지를 불러오기 때문이다.
그래서 처음 lighthouse로 성능을 측정했을 때 점수가 정말 참담했다..╚(•⌂•)╝
보통 이미지는 api 호출을 통해 url을 가져와 사용한다.
이렇게 받아온 이미지의 용량을 줄일 수 있는 방법은 image cdn을 사용하면 됐다.
📘 image cdn?
image cdn을 사용하면 원하는 형태로 이미지를 바꿀 수 있다.
나같은 경우, 무료로 사용 가능한 unsplash의 이미지를 사용했기 때문에
upsplash에서 자체 제공하는 image cdn을 사용했다.
getParametersForUnsplash()
함수를 선언해준 뒤, 이미지를 불러올 때 URL뒤에 해당 함수와 함께 원하는 크기값을 지정해주면 된다.
이미지 슬라이드는 react-slick
라이브러리를 사용했다. 따라서 아래의 코드는 슬라이드 관련 코드를 생략하고 직접적으로 image cdn이 사용되는 부분만 가져왔다.
function Carousel() {
// unsplash에서 제공하는 image cdn
function getParametersForUnsplash({ width, height, quality, format }) {
return `?w=${width}&h=${height}&q=${quality}&fm=${format}&fit=crop`;
}
return (
// 생략
<Image
src={
image.url +
getParametersForUnsplash({
width: 676,
height: 415,
quality: 80,
format: 'jpg',
})
}
alt="이미지 슬라이드"
/>
);
}
18점이었던 Performance 점수가 78점까지 올라갔다.
이미지 용량이 얼마나 많은 비중을 차지하고 있었던건가..(゚Д゚*)ノ
react-icons
번들 사이즈 줄이기Opportunity
에서는 리소스 관점에서의 가이드를 확인할 수 있다. 즉, 로딩 성능을 이야기하는데. Reduce unused JavaScript
가 가장 많은 시간을 잡어먹고 있는 것을 확인할 수 있었다.
그런데 그 중에서도 눈에 띄었던 것은 바로 react-icons
였다. 아이콘 이미지를 따로 저장해 사용하지 않고, react-icons
라이브러리를 설치해서 사용해주었었는데 bundle.js
에서 이렇게나 많은 비중을 차지하고 있을 줄 몰랐다.
react-icons
는 아이콘의 종류별로 하나의 JavaScript파일에 아이콘 전체를 포함하고 있다고 한다. 따라서 나는 하나만 import해왔다고 생각했지만 해당 아이콘을 담고있는 전체 파일을 가져와 성능상 문제가 생겼던 것이다.
따라서, react-icons
에서 제공하는 @react-icons/all-files
라이브러리를 사용했다. 이를 사용하면 아이콘 별로 JavaScript 파일을 가지고 있기 때문에 빌드할 때 보다 적은 크기의 chunk를 만들어낼 수 있다고 한다.
📘 chunk?
Webpack에서 애플리케이션 코드를 각기 다른 파일로 나눈것을 chunk라고 부른다고 한다.
// Before
import { AiOutlineClose, AiFillFacebook } from 'react-icons/ai';
import { RiKakaoTalkFill } from 'react-icons/ri';
import { FiLink2 } from 'react-icons/fi';
// After
import { AiOutlineClose } from '@react-icons/all-files/ai/AiOutlineClose';
import { AiFillFacebook } from '@react-icons/all-files/ai/AiFillFacebook';
import { RiKakaoTalkFill } from '@react-icons/all-files/ri/RiKakaoTalkFill';
import { FiLink2 } from '@react-icons/all-files/fi/FiLink2';
1.56초였던 Reduce unused JavaScript
항목이 0.99초로
절반가량의 시간이 단축된 것을 확인할 수 있었다!🎊
(라이브러리 사용하면 편하다고 이것저것 막 갖다가 사용하면 나중에 성능상으로 문제가 생길 수 있겠단 생각이 들었다..)
react-icons
번들링을 줄였음에도 Reduce unused JavaScript
에서 많은 시간이 지체된다고 생각했다.
이런 문제가 생긴 이유는 CRA(Create-react-app)으로 프로젝트 환경을 세팅했기 때문인데. CRA(Create-react-app)으로 환경 세팅할 경우, 기본적인 Webpack 및 Babel 설정이 자동적으로 되어있다. 그리고 번들링이 되면 모든 JavaScript의 파일이 하나의 JavaScript 파일로 합쳐지는 과정(bundle.js)을 거치게 된다.
이를 개선하는 방법으로 React에서는 코드 분할(Code Splitting)에 대해 설명하고 있다. 그래서 나도 코드 분할을 해보기로 했다!
📘 코드 분할(Code Splitting) 사용방법
React.lazy()
함수를 사용하면 동적 import를 사용해 컴포넌트를 렌더링할 수 있다.// Before import Detail from './pages/Detail/Detail';
// After import { lazy } from 'react'; const Detail = lazy(() => import('./pages/Detail/Detail'));
모든 import
가 아닌 Route
(페이지)에 기반해여 코드 분할 방법을 사용했다. 그런데 여기서 또 문제💥가 발생했다. lazy
를 건 컴포넌트 페이지로 이동 시 아래와 같은 error 문구가 나오며 페이지가 정상적으로 나오지 않았다.
error가 하는 말은 다음과 같았다. 코드 분할로 페이지 로딩하는 시간이 생겼는데 loading indicator가 없다는 것!
그래서 다음과 같이 lazy
를 걸어 import하는 컴포넌트를 감싸는 Suspense
를 통해 해결할 수 있었다. 로딩하는 동안 fallback
속성의 내용이 화면에 보이게 된다.
Loading이란 텍스트만 임시로 넣어두었지만, spinner 컴포넌트를 따로 만들어 넣어준다면 더 사용자 경험이 좋아지겠지?👍🏻
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
import Nav from './components/Nav/Nav';
import Main from './pages/Main/Main';
const Detail = lazy(() => import('./pages/Detail/Detail'));
const MyPage = lazy(() => import('./pages/MyPage/MyPage'));
const Footer = lazy(() => import('./components/Footer/Footer'));
const NavVisibleComponents = () => {
return (
<>
<Nav />
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Main />} />
<Route path="/classes/detail/:id" element={<Detail />} />
<Route path="/mypage" element={<MyPage />} />
</Routes>
</Suspense>
<Footer />
</>
);
};
export default NavVisibleComponents;
0.99초였던 Reduce unused JavaScript
항목이 0.8초로
조금이나마 줄어든 것을 확인할 수 있었다!(@^0^@)
위의 내용을 개선해나가며 Performance점수가 많이 좋아졌지만,
한 가지 계속 괴롭히는 항목이 있었는데.. Cumulative Layout Shift
였다.
이 항목은 동적 DOM 변경 등으로 웹 페이지의 레이아웃이 얼마나 변하는 지 측정한 값으로, 사용자가 잘못된 클릭을 하도록 유발하는 시각적 불안정성을 체크하는 지표다.
아래의 예시와 같이 ❌No, go back❌을 클릭했지만, 클릭하는 순간 광고창이 나오면서 ⭕Yes, place my order⭕버튼을 클릭하게 되는 경우이다. 이렇게 예상하지 못한 레이아웃 이동이 많다면 좋은 사용자 경험을 제공하고 있다고 할 수 없을 것이다.
아무튼 CLS에 걸리는 시간이 많다는 것은, 지금의 내 웹사이트에서
레이아웃 이동이 일어나고 있다는 것이었다.
페이지 가장 하단에 있는 footer
에서 계속 CLS점수가 높게 나오고 있었다.
footer
는 동적으로 콘텐츠가 삽입되는 일이 없는데 왜 자꾸 footer
에서
점수가 높게 나오는건지 알 지 못해서 계속 footer
위치만 조정해주며 헤맸었다.
그런데 lighthouse performance
탭을 통해 웹페이지가 화면에 그려지는 과정을 단계적으로 보던 그 순간..! 중간에 강의 목록들이 동적으로 추가되어서 처음에 footer
가 그려졌다가 밑으로 쭉 밀리는 모습을 볼 수 있었다. 그렇다.. 애초에 footer
문제가 아니었다. 동적으로 요소가 추가되고 있는건 그 위의 강의목록들이었다.
그래서 강의목록들이 동적으로 추가되는 전체 Container에 height
를 지정해주었다. 그랬더니 Performance 점수가 놀랍게 올라가는 것을 확인할 수 있었다.
맨 처음 18점이었던 Performance 점수가 91점까지 올라감으로써
성능 최적화를 모두 90점 이상인 ✅초록불✅로 끝낼 수 있게 되었다.
react-icons 최적화 관련해서 궁금한게 있습니다.
최적화를 위해서는 설치할 때 2번으로 설치해야 되는 건가요?
1. npm install react-icons --save
2. npm install @react-icons/all-files --save