개발자
류준열

code spliting으로 블로그 성능 소폭 개선

게시글 페이지를 light house, performance tab, 번들분석툴로 측정해보았을때 코드 스티펫 스타일을 입혀주는 highlight.js에서 리소스를 많이 차지하여 조금의 병목이 있었다. 그래서 highlight.js를 지연로딩시켜 lighthouse 점수를 78점에서 85점으로 증가시켰다.

어떻게 highlight.js에서 병목이 나타나는 것을 발견했고, 어떻게 개선했고, 어떤게 좋아졌는지 한 번 보자!

개선 전

번들 분석 툴

@next/bundle-analyzer를 이용하여 번들을 분석해보니 다음과 같이 highlight.js가 절반을 차지 하고 있었다. 개선전 번들분석

lighthouse

lighthouse는 78점이다.

개선전 lighthouse

성능 측정

개선전 성능을 측정해보니 performance tab에서는 page.js가 로드된 후 highlighter가 로드되는 것을 볼 수 있다. (이미지 클릭하면 확대 됩니다.)

개선전 성능측정

해당 부분을 더 자세히 보면 다음과 같이 병목사항을 확인할 수 있다. 개선전 병목지점

그럼 코드는 어떻게 되어 있을까?

"use client"
import SyntaxHighlighter from "react-syntax-highlighter";
import MarkdownLibrary from "react-markdown";
...

export default function Markdown({ markdown }: PostProps) {
  return (
    <section className={`markdown-body ${styles.markdown}`}>
      <MarkdownLibrary
      ...
        components={{
          code({ className, children, ...props }) {
            const match = /language-(\w+)/.exec(className || "");
            return match ? (
              <SyntaxHighlighter language={match[1]} style={github}>
                {String(children).replace(/\n$/, "")}
              </SyntaxHighlighter>
            ) : (
              <code {...props}>{children}</code>
            );
          },
        }}
      >
        {markdown}
      </MarkdownLibrary>
    </section>
  );

react-markdown 라이브러리를 먼저 사용한 후에 code snippet에 SyntaxHighlighter를 사용하는 구조이다.
즉, react-markdown가 있어야만 SyntaxHighlighter가 사용될 수 있기에 react-markdown가 먼저 실행되어야 한다.

개선 과정

문제

SyntaxHighlighterreact-markdown을 막아서 병목이 생기고 있다.

해결책

react-markdownSyntaxHighlighter의 영향을 받지 않고 로드되게 한다.

code spliting으로 SyntaxHighlighter 지연로드

해당 컴포넌트는 'use client'를 명시한 클라이언트 컴포넌트라서 Suspenselazy를 사용하였다.

  1. SyntaxHighlighter를 사용할 Code.tsx를 만들고
  2. 기존 코드를 Code.tsx에 넣어주고
  3. SuspenseCode.tsx를 말아주고
  4. lazy를 이용하여 Code.tsx 를 지연로딩 시켰다. (지연로딩: 해당 코드가 필요해질때 로드하는 것)

(file change 깃허브 링크)

"use client"
import { Suspense, lazy } from "react";
const Code = lazy(() => import("@/app/post/[id]/Markdown/Code"));

// 아래 두 라이브러리는 Code.tsx에서 import함.
// import SyntaxHighlighter from "react-syntax-highlighter";
// import MarkdownLibrary from "react-markdown"; 

...

export default function Markdown({ markdown }: PostProps) {
  return (
    <section className={`markdown-body ${styles.markdown}`}>
      <MarkdownLibrary
      ...
        components={{
          // code({ className, children, ...props }) {
          //   const match = /language-(\w+)/.exec(className || "");
          //   return match ? (
          //     <SyntaxHighlighter language={match[1]} style={github}>
          //       {String(children).replace(/\n$/, "")}
          //     </SyntaxHighlighter>
          //   ) : (
          //     <code {...props}>{children}</code>
          //   );
          // },
          code({ className, children, ...props }) {
            const match = /language-(\w+)/.exec(className || "");
            if (match === null) {
              return <code {...props}>{children}</code>;
            }
            return (
              <Suspense fallback={<code {...props}>{children}</code>}>
                <Code match={match}>{children}</Code>
              </Suspense>
            );
          },
        }}
      >
        {markdown}
      </MarkdownLibrary>
    </section>
  );

개선 전 후 비교

번들 분석 툴

highlight.js에서 특정 부분이 분리되어 다른 chunk가 생성되었다.

개선 전 후 번들 비교

lighthouse

lighthouse는 78점에서 85점이 되었다.

개선 전 후 Lighthouse 비교

SI(Speed Index)가 개선 된 걸 보면 잘 된 것 같다. SI는 페이지 로드중 콘텐츠가 시각적으로 표시되는 속도이다.

SI(Speed Index) 설명

A,B 둘 다 화면이 표시되는데 4초라는 동일한 시간이 걸렸지만, A페이지는 일부 콘텐츠가 B페이지보다 먼저 나타났다. 이 경우 A페이지가 B페이지보다 먼저 로드된것으로 간주되어 더 높은 점수를 받는다.

개선전에는 react-markdown가 먼저 렌더링되어야 함에도 SyntaxHighlighter 가 로드가 끝나기를 기다리다보니 B의 상황이었을 것이다. 하지만 SyntaxHighlighter를 지연로딩하면서 react-markdown의 로드가 앞당겨졌다.

결과적으로는 개별 콘텐츠의 로드시간이 빨라지면서 SI가 2.2초에서 0.6초로 단축되었다.

개선 전 후 성능 측정 비교

성능 측정시 page.js가 두개로 분리되고 기존 page.js의 로드시간이 짧아진 것을 확인할 수 있었다.(이미지 클릭하면 확대 가능)

개선 전 후 성능 측정 비교

무조건 code spliting이 좋은가?

그렇진 않다 상황마다 다르다. 때로는 미리 로드하는 pre-load가 좋을때도 있다.

예를 들면 이 블로그에서 이미지를 클릭했을때 이미지 원본 크기를 모달로 보여주는 기능이 있는데, 이미지에 마우스를 올리는 순간 이미지를 미리 로드하여 이미지 모달을 렌더링했을때 로딩이 없도록 하였다.

      <ImageComponent
        onMouseOver={preloadImage}
        ...
      />

정답은 없고 그때그때 적절한 방법을 사용할 수 있는 지식이 필요하다.