Web

Critical Rendering Path

잉여개발자 2024. 11. 13. 15:42
반응형

HTML의 렌더링 과정에 대해서 지난번에 공부한 적이 있다. 

간단하게는 다음과 같은 과정을 거치게 된다. 

  1. 다운로드 : 화면을 그려주는데 필요한 리소스(html, css, js)를 다운로드 한다. 
  2. HTML 준비 : 렌더링 되어야 할 HTML 요소로 DOM을 만들어 준다. 
  3. CSS 준비 : css 코드를 가지고 와서 CSSOM을 만들어 준다. 
  4. 두개 합치기 : 둘을 합쳐서 렌더링 트리를 생성한다. 
  5. 위치 그리기 : 화면에 요소들이 어디 놓일 지 그려준다. 
  6. 색칠하기 : 그려친 요소에 색을 칠해준다. 

여기서 위치 그리기, Layout 단계가 다시 실행되면 색칠하기, Paint 단계도 다시 실행되는 Reflow는 성능 저하의 주요 원인이 된다. 

 

메모이제이션을 통해 불필요한 기능 재실행을 줄이는 것 외에도, reflow 발생을 줄이거나 사전 로드(prefetch, preload) 기능을 통해 필요한 데이터를 미리 받아와 성능을 개선할 수 있다. 

 

이번 글에서는 이러한 최적화 기법을 사용해 어떻게 브라우저 성능을 최적화할 수 있는지 살펴보려고 한다. 

Reflow

게시글 데이터를 가져오는 상황을 가정해보자. 목록을 조회할 때 데이터를 불러오는 동안 UI상에 빈 공간이 나타났다가 데이터가 로드되면 해당 영역이 다시 렌더링된다.

import { useEffect, useState } from "react";

const getData = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve([
        {
          id: 1,
          title: "1",
          content: "content",
        },
        {
          id: 2,
          title: "2",
          content: "content",
        },
        {
          id: 3,
          title: "3",
          content: "content",
        },
        {
          id: 4,
          title: "4",
          content: "content",
        },
        {
          id: 5,
          title: "5",
          content: "content",
        },
        {
          id: 6,
          title: "6",
          content: "content",
        },
        {
          id: 7,
          title: "7",
          content: "content",
        },
        {
          id: 8,
          title: "8",
          content: "content",
        },
      ]);
    }, 1000);
  });
};

const Crp = () => {
  const [data, setData] = useState([]);

  useEffect(() => {
    getData().then((data) => setData(data));
  }, []);

  return (
    <>
      {data.map(({ id, title, content }) => (
        <div key={id}>
          <p>{title}</p>
          <p>{content}</p>
        </div>
      ))}

      <div
        style={{
          width: "100%",
          height: "100px",
          background: "red",
        }}
      >
        Footer
      </div>
    </>
  );
};

export default Crp;

예제 코드를 보면, setTimeout을 사용하여 인터넷 속도가 느려지는 상황을 시뮬레이션하고, 1초 뒤에 게시글 데이터를 화면에 렌더링하도록 설정했다. 

 

데이터가 늦게 로드되면 Footer로 예상되는 요소가 게시글 데이터가 화면에 나타나면서 아래로 밀리는 것을 확인할 수 있다. 성능이 좋은 환경에서는 문제가 덜하겠지만, 모바일 환경이나 인터넷 속도가 느린 경우 layout shift가 발생해 UX가 나빠지고, 잦은 reflow로 성능 저하가 발생할 수 있다. 

 

이를 해결하기 위해 데이터 로드 전, 미리 placeholder로 자리 크기를 지정해둔다면, reflow 없이 안정적인 레이아웃이 유지 된다. 예를 들어 아래와 같이 height를 미리 지정하여 데이터 로딩 중 요소의 위치를 유지할 수 있다. 

{(data ?? new Array(8).fill(1)).map(({ id, title, content }) => (
<div
    key={id}
    style={{
    height: "58px",
    }}
>
    <p>{title}</p>
    <p>{content}</p>
</div>
))}

이처럼 placeholder 영역을 설정하면 브라우저가 페이지를 불필요하게 다시 그리는 것을 방지할 수 있어 성능 개선과 안정적인 UX에 기여하게 된다. 

 

prefetch

prefetch란 사용자가 다음에 이동할 가능성이 높은 페이지나 리소스를 미리 받아두는 것을 의미한다. 현재 페이지의 로드가 완료된 후 우선순위가 낮은 리소스를 처리하며, 예를 들어 아래와 같이 prefetch 속성을 추가하여 다음 페이지를 미리 다운로드할 수 있다. 

<!DOCTYPE html>
<html lang="ko">
  <head>
    <title>프리페치</title>

    <!-- 프리페치: 다음페이지를 미리 다운로드 받으므로, 버튼 클릭시 페이지이동 빠름 -->
    <link rel="prefetch" href="board.html" />
  </head>
  <body>
    <a href="board.html">게시판으로 이동하기</a>
  </body>
</html>
<!DOCTYPE html>
<html lang="ko">
  <head>
    <title>게시판</title>
  </head>
  <body>
    여기는 게시판입니다
  </body>
</html>

이를 통해 사용자가 링크를 클릭하여 다음 페이지로 이동할 때 로드 시간이 거의 없어 빠르게 페이지가 나타난다. 예를 들어, index.html에서 board.html을 prefetch하면 개발자 도구의 Network 탭에서 'Size: prefetch cache'로 표시되어, 캐시에 저장된 리소스를 로드하지 않아도 되는 것을 확인할 수 있다. 

 

이처럼 prefetch는 UX 개선에 매우 유용하지만, 사용자가 해당 페이지로 이동하지 않을 경우 불필요한 리소스 사용이 발생할 수 있다. 따라서 링크에 마우스를 올렸을 때 prefetch하도록 설정하는 것도 좋은 방법이다. 상황에 따라 적절히 사용하는 것이 중요하다. 

 

preload

preload란 페이지가 로드될 때 필요한 리소스(이미지, 폰트, 스크립트 등)을 우선적으로 다운로드하여 페이지 로드 속도를 개선하는 방법이다. 

<!DOCTYPE html>
<html lang="ko">
  <head>
    <title>프리로드</title>

    <link rel="stylesheet" href="./index.css" />
    <script src="index1.js"></script>
    <script src="index2.js"></script>
    <script src="index3.js"></script>
    <script src="index4.js"></script>
    <script src="index5.js"></script>
    <script src="index6.js"></script>
  </head>
  <body>
    <img src="./dog.jpeg" />
  </body>
</html>

HTML 1.1 버전에서는 일반적으로 최대 6개의 리소스를 병렬로 다운로드하므로, 강아지 이미지가 다운로드 순서에서 밀려서 사용자가 해당 이미지를 보기까지 기다려야 할 수 있다. 

실제 결과에서도 추측했던 것과 동일하게 데이터를 불러오고 있다. 

 

하지만 강아지 이미지는 사용자에게 먼저 보이는 부분이기 때문에 다른 요소보다 먼저 로드가 된다면 홈페이지를 접속했을 때 화면이 변경되는 문제가 적어질 것이다. 이때 사용하는 것이 preload이다. 

<!DOCTYPE html>
<html lang="ko">
  <head>
    <title>프리로드</title>

    <link rel="preload" as="image" href="./dog.jpeg" />

    <link rel="stylesheet" href="./index.css" />
    <script src="index1.js"></script>
    <script src="index2.js"></script>
    <script src="index3.js"></script>
    <script src="index4.js"></script>
    <script src="index5.js"></script>
    <script src="index6.js"></script>
  </head>
  <body>
    <img src="./dog.jpeg" />
  </body>
</html>

preload의 장점은 사용자에게 시각적으로 중요한 리소스를 빠르게 로드해 초기 화면 표시 속도를 높일 수 있다는 점이다. 다만, 모든 리소스에 preload를 사용하는 것은 브라우저 성능에 오히려 부정적인 영향을 줄 수 있으므로, 정말 우선 로드가 필요한 리소스에만 사용하는 것이 좋다.

반응형