개발자
류준열
code spliting으로 블로그 성능 소폭 개선
게시글 페이지를 light house, performance tab, 번들분석툴로 측정해보았을때 코드 스티펫 스타일을 입혀주는 highlight.js에서 리소스를 많이 차지하여 조금의 병목이 있었다. 그래서 highlight.js를 지연로딩시켜 lighthouse 점수를 78점에서 85점으로 증가시켰다.
어떻게 highlight.js에서 병목이 나타나는 것을 발견했고, 어떻게 개선했고, 어떤게 좋아졌는지 한 번 보자!
개선 전
번들 분석 툴
@next/bundle-analyzer를 이용하여 번들을 분석해보니 다음과 같이 highlight.js
가 절반을 차지 하고 있었다.
lighthouse
lighthouse는 78점이다.
성능 측정
개선전 성능을 측정해보니 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
가 먼저 실행되어야 한다.
개선 과정
문제
SyntaxHighlighter
가 react-markdown
을 막아서 병목이 생기고 있다.
해결책
react-markdown
가 SyntaxHighlighter
의 영향을 받지 않고 로드되게 한다.
code spliting으로 SyntaxHighlighter 지연로드
해당 컴포넌트는 'use client'
를 명시한 클라이언트 컴포넌트라서 Suspense
와 lazy
를 사용하였다.
SyntaxHighlighter
를 사용할Code.tsx
를 만들고- 기존 코드를
Code.tsx
에 넣어주고 Suspense
로Code.tsx
를 말아주고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점이 되었다.
SI(Speed Index)가 개선 된 걸 보면 잘 된 것 같다. SI는 페이지 로드중 콘텐츠가 시각적으로 표시되는 속도이다.
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} ... />
정답은 없고 그때그때 적절한 방법을 사용할 수 있는 지식이 필요하다.