-
Notifications
You must be signed in to change notification settings - Fork 0
lighthouse 개선(2) FCP(lazy loading)
lighthouse 개선(1) - FCP(사용하지 않는 자바스크립트 줄이기) 에 이어서 FCP 지표를 개선하려합니다.
vite에 의해 번들링된 앱은 최적화되지만 앱이 커지면 번들의 크기도 커집니다, 앱의 규모가 커짐에 따라 번들링되어 제공되는 파일의 크기도 커지게 되어 초기 로딩 속도가 느려집니다.
최초 페이지에 집입하면 웹팩에서 압축한 번들 파일을 다운 받고, 페이지 전체의 리소스를 다운 받게 됩니다. 즉, 당장 필요하지 않은 페이지의 리소스도 다운받습니다.
이러한 문제를 해결하기 위해 Code Splitting을 적용하여 최초 페이지 진입에 필요한 모듈만 로딩(lazy loading) 되도록 하여 성능을 향상 시킬 수 있습니다.
https://crystallize.com/comics/no-code-splitting-vs-code-splitting
코드 분할은 런타임에 여러 번들을 동적으로 만들고 불러오는 것으로 Webpack, Rollup과 Browserify factor-bundle 같은 번들러가 지원하는 기능입니다.
코드 분할은 여러분의 앱을 “지연 로딩” 하게 도와주고 앱 사용자에게 획기적인 성능 향상을 하게 합니다. 앱의 코드 양을 줄이지 않고도 사용자가 필요하지 않은 코드를 불러오지 않게 하며 앱의 초기화 로딩에 필요한 비용을 줄여줍니다.
lazy loading
리소스를 비차단(non-blocking) 방식으로 필요할 때만 로드하여 중요한 렌더링 경로를 단축시키는 전략입니다. 일반적으로 사용자 상호작용(스크롤, 클릭 등)에 따라 로드가 발생합니다.
React에서는 코드 스플리팅을 더욱 쉽게 구현할 수 있도록 React.Lazy와 Suspense라는 기능을 제공합니다.
React.Lazy는 동적으로 컴포넌트를 로드하기 위한 React의 레이지 로딩 함수입니다. 컴포넌트를 필요한 시점에 로드하며, 필요한 모듈을 자동으로 분리하여 별도의 번들로 생성합니다.
React.lazy는 **동적 import()**를 호출하는 함수를 인자로 가집니다.
const OtherComponent = React.lazy(() => import('./OtherComponent'));
동적 import는 일반적인 정적인 Module Import를 필요한 시점 에 로드 할 수 있도록 도와줍니다. 이를 이용하면 거대한 하나의 js를 여러개로 쪼갤 수 있고, 화면이 위치할때마다 import 하는 기법입니다.
import(...)
표현식은 모듈을 읽고 내보내는 모든 것을 포함하는 객체를 담는 Promise를 반환합니다. 호출은 어디에서든 가능합니다.
lazy 컴포넌트는 Suspense 컴포넌트 하위에서 렌더링되어야 합니다.
Suspense는 React.Lazy와 함께 사용되는 기능으로, 로딩 상태를 처리하기 위한 컴포넌트입니다. Suspense 컴포넌트는 로딩 중인 동안 특정한 UI를 렌더링할 수 있어, 사용자 친화적인 환경을 구성할수 있게 도와줍니다.
import React, { lazy, Suspense } from 'react';
const LazyComponent = lazy(() => import('./NeedComponent'));
const App = () => {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
</div>
);
};
export default App;
위의 예시에서 Suspense 컴포넌트의 fallback 속성을 사용하여 로딩 중에 보여줄 대체 컨텐츠를 설정합니다. 대체 컨텐츠는 JSX 문법으로 작성합니다.
Suspense 컴포넌트로 감싼 LazyComponent 컴포넌트를 렌더링하는 시점에서 해당 컴포넌트가 비동기적으로 로드됩니다. 로드되기 전에는 fallback에 설정한 로딩 메시지가 보여지며, 로드가 완료되면 실제 컴포넌트가 렌더링됩니다.
OctoDocs는 페이지 이동이 많은 서비스가 아니므로, 페이지 단위보다는 최초 페이지 진입 시 보이지 않는 컴포넌트를 개선 대상으로 결정하였습니다.
- Editor
- 워크스페이스 리스트 (비로그인: 로그인 화면, 로그인: 워크스페이스)
- 페이지 리스트
- 공유, 사용자 및 커서 정보
- 노드 도구 선택
- 개선 대상들은 React.lazy()와 Suspense를 이용하여 code splitting 적용
- fallback 컴포넌트들은 tailwind의
animate-pulse
를 사용해서 스켈레톤 UI를 구현함.
-
UserInfoView 컴포넌트
import { lazy } from "react"; import { LogoBtn } from "@/features/pageSidebar"; import { Popover, Skeleton } from "@/shared/ui"; import { Suspense } from "react"; const WorkspacePanel = lazy( () => import("@/features/workspace/ui/WorkspacePanel"), ); export function UserInfoView() { return ( <div className="flex flex-row items-center gap-2"> <Popover align="start" offset={{ x: -7, y: 16 }}> <Popover.Trigger> <LogoBtn /> </Popover.Trigger> <Popover.Content className="rounded-lg border border-neutral-200 bg-white shadow-md"> <Suspense fallback={<Skeleton className="h-[228px] w-[282px]" />}> <WorkspacePanel /> </Suspense> </Popover.Content> </Popover> </div>
-
Skeleton 컴포넌트
import { cn } from "@/shared/lib"; interface SkeletonProps { className: string; } export function Skeleton({ className }: SkeletonProps) { return ( <div className={cn( "h-full animate-pulse bg-gray-200 bg-gradient-to-r from-gray-200 via-gray-300 to-gray-200", className, )} /> ); }
Code Splitting과 Lazy Loading을 적용한 결과, FCP 지표와 초기 로딩 성능이 다음과 같이 개선되었습니다
항목 | 개선 비율 (%) |
---|---|
FCP | 35.85% |
LCP | 37.25% |
Speed Index | 23.73% |
https://ko.legacy.reactjs.org/docs/code-splitting.html
https://velog.io/@rl0425/Code-Splitting
https://developer.mozilla.org/en-US/docs/Web/Performance/Lazy_loading https://developer.mozilla.org/en-US/docs/Web/Performance/Lazy_loading