캡스톤 디자인 프로젝트에서 Lighthouse를 이용해 웹사이트 성능 측정을 진행하고, 성능 최적화를 수행한 과정을 공유하려 한다. 초기 성능은 다음과 같다. 성능 측면에서 FCP와 LCP가 3.6초로 개선이 필요했으며, 콘텐츠가 포함된 최대 페인트 요소를 확인해보니 렌더링 지연이 96%로 압도적으로 비중이 높았다 이를 해결하기 위해 먼저 폰트 최적화, 이미지 최적화를 수행했다.
기존에는 Google Fonts를 외부에서 불러와 렌더링 지연이 발생했다. 이를 개선하기 위해 Google Fonts를 로컬 폰트 woff2형식으로 다운로드하고, 프로젝트 내부에서 직접 호출하도록 수정하였다. 또한 font-display: swap 속성을 적용하여 텍스트가 즉시 렌더링되도록 변경하였다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link href="/src/index.css" rel="stylesheet" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<link rel="manifest" href="/manifest.json" />
<title>Sound Palette</title>
<style>
@font-face {
font-family: "Fira Sans Condensed";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("src/assets/fonts/wEOhEADFm8hSaQTFG18FErVhsC9x-tarUfbtrelWfx4.woff2")
format("woff2");
}
@font-face {
font-family: "Fira Sans Condensed";
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("src/assets/fonts/wEOsEADFm8hSaQTFG18FErVhsC9x-tarWQXOuMR0cjRYhY8.woff2")
format("woff2");
}
@font-face {
font-family: "Fira Sans Condensed";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("src/assets/fonts/wEOsEADFm8hSaQTFG18FErVhsC9x-tarWSnJuMR0cjRYhY8.woff2")
format("woff2");
}
@font-face {
font-family: "Fira Sans Condensed";
font-style: normal;
font-weight: 300;
font-display: swap;
src: url("src/assets/fonts/wEOsEADFm8hSaQTFG18FErVhsC9x-tarWV3PuMR0cjRYhY8.woff2")
format("woff2");
}
</style>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
사진 파일 크기를 줄여 로드 시간 줄이기 위해 기존 ".png" 파일들을 ".webp"로 변경해서 성능개선을 시도하려고 했다.
다음과 같이 이미지 크기가 크게 절감되는 효과를 얻을 수 있었으나, 성능 점수에는 큰 영향을 미치지 못했다.
초기 로딩 시 네트워크 탭을 확인한 결과, App.tsx에 정의된 모든 라우터의 페이지 컴포넌트 리소스가 한 번에 로드되는 문제가 발견되었다. 이것이 렌더링 지연에 큰 영향을 미치는 것으로 파악되어 이를 해결하기 위해 Lazy-Loading(지연 로딩) 방식을 도입하였다.
React.lazy를 사용하지 않을 경우 사용자가 첫 페이지를 로드하는 즉시 App.tsx에 import된 컴포넌트 파일들이 빌드 시 하나의 커다란 파일에 병합되어 대규모의 단일 JS번들이 사용자에게 전송이 된다. 만약 수십,수백개의 라우트와 컴포넌트가 있다면 모든 파일(코드)을 불러오는게 문제가 될 수도 있으며, 첫 페이지의 로딩속도도 느려질것이다.(CSR의 단점)
→ React.lazy() 메서드 요소들은 손쉽게 개별 JS 청크로 분리하는 기본 방법을 제공한다.
해당 컴포넌트 코드가 처음 렌더링될 때까지 로드하는 것을 연기하려면 import를 다음과 같이 대체한다. lazy 컴포넌트는 반드시 컴포넌트 외부에서 선언해야 한다.
React.lazy()는 import() 구문을 반환하는콜백 함수를 인자로 받는다.
동적으로 불러오는 컴포넌트 파일에는 반드시 지켜줘야 하는 두 가지 규칙이 있다.
React 컴포넌트를 포함해야 한다.
default export를 가진 컴포넌트여야 한다.
const MainPage = lazy(() => import("./pages/Main"));
const ServiceSelectionPage = lazy(
() => import("./pages/Create/ServiceSelection")
);
function App() {
...
}
이제 요청에 따라 컴포넌트 코드가 로드되므로 로드하는 동안 표시할 항목도 지정해야 하는데, lazy 컴포넌트 또는 부모 컴포넌트 중 하나를 바운더리로 감싸서 이 작업을 수행할 수 있다.
const router = createBrowserRouter([
{
path: "/",
element: <RootLayout />,
children: {
...
{
path: "create",
element: <PrivateRoute />,
children: [
{
path: "service-selection",
element: (
<Suspense fallback={<div>Loading...</div>}>
<ServiceSelectionPage />
</Suspense>
),
},
{
path: "file-upload",
element: (
<Suspense fallback={<div>Loading...</div>}>
<FileUploadPage />
</Suspense>
),
},
{
path: "check-lyric/:itemId",
element: (
<Suspense fallback={<div>Loading...</div>}>
<CheckLyricPage />
</Suspense>
),
},
{
path: "analysis-result/:itemId",
element: (
<Suspense fallback={<div>Loading...</div>}>
<AnalysisResultPage />
</Suspense>
),
},
{
path: "generate-prompt/:itemId",
element: (
<Suspense fallback={<div>Loading...</div>}>
<GeneratePromptPage />
</Suspense>
),
},
{
path: "view-result/:itemId",
element: (
<Suspense fallback={<div>Loading...</div>}>
<ViewResultPage />
</Suspense>
),
},
],
},
],
},
]);