본문 바로가기

TroubleShooting/Next.js

[Next.js] 프로젝트의 CLS(Cumulative Layout Shift) 개선하기

간단한 자기소개 페이지를 작성하고 배포 후 lighthouse에서 성능 체크를 해봤습니다.

 

 

CLS(Cumulative Layout Shift) 지표가 나쁜 걸 확인할 수 있었습니다.

 

 

CLS 는 페이지 로딩 중 발생하는 예기치 않은 레이아웃 이동을 측정하는 지표로써, 갑자기 변경되는 레이아웃을 말합니다.

Google에서는 좋은 사용자 경험을 위해 CLS 점수를 0.1 이하로 유지할 것을 권장합니다.

 

 

예상치 못한 레이아웃 전환은 텍스트가 갑자기 이동하여 읽는 도중에 위치를 놓치는 것부터 잘못된 링크나 버튼을 클릭하게 하는 것까지 다양한 방식으로 사용자 경험을 방해할 수 있습니다. 결재 버튼 위치가 갑자기 나타나서 원치 않는 구입을 하는 예시가 아래 링크에 있습니다.

 

https://web.dev/articles/cls?hl=ko#measure-layout-shifts-in-javascript

 

레이아웃 변경 횟수(CLS)  |  Articles  |  web.dev

이 게시물에서는 레이아웃 변경 횟수 (CLS) 측정항목을 소개하고 이를 측정하는 방법을 설명합니다.

web.dev

 

 

 

제 사이트의 경우 레이아웃 시프트의 주요 원인은 아래와 같았습니다

 

1. SUIT 웹폰트 로딩으로 인한 텍스트 변화
2. 이미지 로딩 시 레이아웃 변화
3. CSS 파일의 늦은 로딩

 

 

 

1. 웹폰트 최적화

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="ko" suppressHydrationWarning>
      <head suppressHydrationWarning>
        <link
          rel="preload"
          href="https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_suit@1.0/SUIT-Regular.woff2"
          as="font"
          type="font/woff2"
          crossOrigin="anonymous"
        />
      </head>
      <body>
        {children}
      </body>
    </html>
  );
}

 

rel="preload" 속성을 주고 높은 우선순위를 주어 즉시 로딩하게 했습니다.

as 속성 필수 사용해야하며 (  as="font" , as="image"  등)

순수하게 리소스만 요청하기 위해 crossOrigin="anonymous" 을 추가했습니다.

(crossOrigin 속성이 없을 때는 두번 다운로드 되는 현상이 있다고 합니다)

 

https://camel-dev.blogspot.com/2018/04/preload.html

 

폰트 preload시 두 번 다운로드 하는 문제

스태틱 리소스에 대해서 preload 를 사용하면 초기 로딩 속도를 최적화시킬 수 있다. css와 font 파일들은 렌더링 블럭을 발생시키기 때문에 이 파일들을 모두 다운받기 전까지는 렌더링을 하지 않

camel-dev.blogspot.com

 

 

2. 이미지 스켈레톤 UI 적용

const About = () => {
  const [isLoading, setIsLoading] = useState(true)

  return (
    <div className="relative">
      <Image
        src={imageSrc}
        alt="About Image"
        fill
        sizes="(max-width: 1024px) 150px, 200px"
        priority
        quality={70}
        className={`
          object-cover rounded-sm
          transition-opacity duration-300
          ${isLoading ? 'opacity-0' : 'opacity-100'}
        `}
        onLoad={() => setIsLoading(false)}
      />
      
      
      {isLoading && (
        <div 
          className="absolute inset-0 animate-pulse bg-gray-200 dark:bg-gray-700 rounded-sm"
          aria-hidden="true"
        />
      )}
      
    </div>
  )
}

 

isLoading 일때는 부모 요소인 Image 태그의 전체를 채워서 네모 모양이 되고( inset-0 )

로딩이 끝나면 서서히 Image 가 나타나는 애니메이션 효과가 나타납니다.

그래서 레이아웃의 크기 변화 cls 문제를 잡을 수 있었습니다.

 

 

 

참고 링크

 

Web.dev - CLS 이해하기
Next.js - Font Optimization
Next.js - Image Optimization