10일 전
페이지 링크
: https://wakcraft.vercel.app/architect
테스트 환경
: Vercel Production + AMD Ryzen™ 5 5600X + CPU 4x Throttle
현재 사이트에 등록된 건축가는 326명이다. 더 나은 사용자 경험(검색)을 위해 모든 건축가들을 DOM에 추가했기 때문에, DOM의 크기가 매우 컸다.
(Lighthouse를 돌려봤을 때)
과도한 DOM 크기 때문에 스타일 계산 시간이 오래 걸리고 레이아웃 비용이 비쌌기 때문에 초기 마운트가 오래 걸리는 문제가 발생했다.
모든 컴포넌트를 렌더링하기 위해 1,244ms
나 소요됐다.
input 이벤트가 발생하면 input 값에 따라 건축가들이 필터링 되고, 필터링 된 건축가 컴포넌트가 모두 리렌더링 된다.
예를 들어, a
를 검색하면 요소의 개수가 326에서→ 178로 변경된다.
키보드 입력 이벤트를 처리하는데 483.32ms
가 소요됐다.
메인 스레드는 한 번에 하나의 작업만 처리할 수 있는데, 메인 스레드에서 50ms
이상 걸리는 작업을 “긴 작업”이라고 명시한다. → 483.32ms
는 CPU 4x throttle을 감안해도 매우 긴 작업이다.
성능 향상을 위해 즉각적인 반응이 필요한 부분과 즉각적인 반응이 필요하지 않은 부분을 분리를 하는 것이 좋다고 판단했다.
검색 필드와 검색 결과에서 유저의 이름은 빠른 반응이 필요하고, 검색 결과에서 유저의 활동내역 같은 부분은 상대적으로 덜 중요하다고 판단했다.
const [input, setInput] = useState("");
const defferedValue = useDeferredValue(input);
return (
...
{props.input === props.debouncedSearchText && (
<Statistics noobprohackerInfo={architect.statistics} />
)}
...
)
useDeferredValue
를 사용하면, 값 변화를 지연시켜 나중에 처리되도록 할 수 있다.
→ 즉각적인 반응이 필요한 부분은 우선적으로 처리하게 하고, 즉각적인 반응이 필요하지 않은 부분은 리렌더링 지연을 할 수 있다.
즉각적인 반응이 필요한 부분, 즉각적인 반응이 필요하지 않은 부분이 따로 리렌더링되는 것을 볼 수 있다. → 커밋이 총 2번 발생한다.
즉각적인 반응이 필요한 부분이 렌더링 되었을 때 - 소요시간: 350.00ms
즉각적인 반응이 필요하지 않은 부분이 렌더링 되었을 떄 - 소요시간: 272.78ms
모든 컴포넌트가 리렌더링 될 때까지 이전에는 483.32ms
, 이후에는 350.00ms
+ 272.78ms
가 걸렸다. 총 리렌더링 시간은 더 증가했지만 사용자 입장에서는 화면의 첫 반응이 483.32ms
에서 350.00ms
로 많이 빨라졌다.
하지만 DOM 크기가 애초에 너무 컸기 때문에 350.00ms
도 사용자 입장에서는 빠른 반응이 아니다.
326개의 요소들을 전부 렌더링하지 않고, 처음에는 빈 요소를 렌더링하고 뷰포트에 보여지는 요소들만 렌더링하는 방법인 Lazy Loading을 이용했다.
const [isIntersecting, setIsIntersecting] = useState(false);
const observerRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const element = observerRef.current
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
setIsIntersecting(true)
}
},
{ threshold: 0.01 },
)
if (element) {
observer.observe(element)
}
return () => {
if (element) {
observer.unobserve(element)
}
}
}, [])
return (
<div ref={observerRef}>
{isIntersecting ? (
<Fragment>
...
</Fragment> // 뷰포트에 보여지면 실제 요소를 렌더링한다.
) : (
<div className="h-[95px]"></div> // 처음에는 빈 요소를 렌더링한다.
)}
</div>
)
Lazy Loading를 구현하기 위해,IntersectionObserver
를 이용했다.
Intersection Observer API
는 특정 요소가 뷰포트와 교차하는지 감지하는 API이다.
이는 메인 스레드의 부하를 줄이기 위해, 별도의 브라우저 최적화 프로세스에서 실행하기 때문에 onscroll
이벤트보다 더 적은 리소스를 사용한다.
렌더링 시간이 1,244ms에서 317ms 로 줄어들었다.
[이전]
[이후]
[이전]
[이후]
초기 마운트 시 렌더링에 소요된 시간
1313.2ms
→ 311.6ms
(5번 평균)
검색 창에 a
를 입력 했을 떄 INP
(4x throttle)
424.0ms
→ 97.33ms
(10번 평균)
검색 창에서 a
를 지웠을 때 INP
(4x throttle)
678.4ms
→ 138.18ms
(10번 평균)