원문 : https://itnext.io/infinite-component-scrolling-with-react-lazy-and-intersectionobserver-7774c03b08f2
서론
브라우저와 자바스크립트
단순화하면 다음과 같습니다.
성능 개선 지침
JavaScript의 실행은 소스 코드를 가져오는 것으로 시작됩니다.
이 가져오기가 적시에 수행되었는지 확인하는 것이 중요합니다.
스크립트 태그에는 async 및 defer(지연)과 같은 속성이 있습니다.
이러한 속성은 성능 향상에 기여합니다. 그러나 이것으로 충분하지 않을 때가 있습니다.
index.html
<!DOCTYPE html>
<html lang="en">
<body>
<script src="/heavy.js"></script>
</body>
</html>
heavy.js
console.log('Long words ...')
Heavy.js가 매우 큰 실행 파일이라고 가정해 보겠습니다.
html이 로드되는 즉시 heavy.js가 실행됩니다.
이 때는 async, defer를 사용해도 tbt(Total Blocking Time)의 증가를 막을 수 없습니다.
사용자 상호 작용과 Lazy 전략
Heavy.js가 내 TBT 점수를 악화시키고 있습니다.
이를 개선하기 위해 우리는 무엇을 해야 할까요?
heavy.js가 종속성이 없는 독립 스크립트이고 언제든지 실행할 수 있는 경우 지연 로딩할 수 있습니다.
가장 간단한 방법은 setTimeout을 사용하는 것입니다.
index.html
<!DOCTYPE html>
<html lang="en">
<body>
<script type="module">
window.addEventListener(
'load',
() => {
setTimeout(() => {
const script = document.createElement('script')
script.src = '/heavy.js'
script.async = true
const body = document.querySelector('body')
body.appendChild(script)
}, 3000)
},
{
once: true
}
)
</script>
</body>
</html>
이 발생할 때까지 지연할 수 있습니다.
측정에서 도망치기
이 방법을 사용하면 말 그대로 Lighthouse의 측정을 피할 수 있습니다.
그러나 Loading JavaScript After User Interaction #11904에서는
이 방법이 문제를 연기할 수 있을 뿐이라고 지적했습니다.
Intersection Observers(교차지점 관찰자)와 지연 로딩 전략
좋은 방법 중 하나는 필요할 때까지 아무것도 하지 않는 것입니다.
사용자의 입장에서 생각해보면 알 수 있는 것들이 있습니다.
React.lazy 사용법
Dialog.tsx
import type { FC } from 'react'
const Dialog: FC<{ open: boolean }> = ({ open }) => <dialog {...{ open }}>...</dialog>
export default Dialog
다른 파일
import { lazy, useState } from 'react'
import type { FC } from 'react'
const Dialog = lazy(() => import('./Dialog.tsx'))
const Index: FC = () => {
const [isShow, chagneShow ] = useState(false)
return {isShow && <Dialog open={isShow} /> }
}
Intersection Observer component
컴포넌트가 뷰포트에 들어갈 때까지 컴포넌트의 렌더링을 지연합니다.
교차 관찰자가 있는 컴포넌트는 다음과 같습니다.
Intersection.tsx
import { useRef, useState, useEffect, createElement } from 'react'
import type {
FC,
ReactNode,
ReactHTML,
DetailedHTMLProps,
HTMLAttributes
} from 'react'
const Intersection: FC<
{
children: ReactNode
as?: keyof ReactHTML
keepRender?: boolean
} & IntersectionObserverInit &
DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>
> = ({
children,
as = 'div',
keepRender = true,
root,
rootMargin,
threshold,
...props
}) => {
const [isShow, setShow] = useState(false)
const ref = useRef<HTMLElement>(null)
useEffect(() => {
const observer = new IntersectionObserver(
([entry], obs) => {
if (entry.isIntersecting) {
setShow(true)
if (keepRender && ref.current) {
obs.unobserve(ref.current)
}
} else {
setShow(false)
}
},
{ root, rootMargin, threshold }
)
if (ref.current) {
observer.observe(ref.current)
}
return () => observer.disconnect()
}, [keepRender, root, rootMargin, threshold])
return (
<>
{createElement(as, { ref, ...props })}
{isShow && children}
</>
)
}
export default Intersection
다음과 같이 사용합니다.
import Intersection from 'path/to/Intersection.tsx'
import { lazy } from 'react'
const LazyComponent = lazy(() => import('path/to/Lazy.tsx'))
const Index = () => {
return (
<>
...
<Intersection>
<LazyComponent />
</Intersection>
...
</>
)
}
뷰포트 외부에 있는 경우 마크업은 다음과 같이 표시됩니다.
<body>
...
<div></div>
...
</body>
div 태그는 교차 감지에 사용됩니다. div 태그가 뷰포트와 교차하면 자식이 렌더링됩니다.
<body>
...
<div></div>
<children />
...
</body>
return (
<>
<Intersection rootMargin="100px">
<LazyComponent />
</Intersection>
</>
)
과도한 DOM 크기 피하기
- 총 1500개 이상의 노드가 있습니다.
- 깊이가 32 노드보다 깊습니다.
- 60개 이상의 자식 노드가 있는 부모 노드가 있습니다.
Interaction Observer 컴포넌트를 사용하는 경우 교차할 때까지 렌더링되지 않으므로 이 경고가 표시되지 않습니다.
그러나 DOM 트리가 커질수록 리플로우가 더 오래 걸린다는 사실을 알아야 합니다.
교차점을 지난 후의 DOM을 생각해 봅시다.
교차 중에 렌더링된 구성 요소가 다시 뷰포트를 벗어나면 어떻게 해야 합니까?
-
DOM 트리에서 제거
- 아무 것도 하지 않고 Intersection Observer를 연결 해제하십시오.
대부분의 경우 DOM을 조작하는 데 비용이 많이 들기 때문에 아무것도 하지 않는 것이 좋습니다.
Interaction Observer 컴포넌트에는 이 동작을 선택할 수 있는 keepRender props가 있습니다.
컴포넌트 내부 최적화
React는 immutable한 접근 방식을 사용합니다. 상태 변경은 객체 ID 검사에 의해 감지됩니다.
덕분에 데이터 업데이트 흐름이 매우 간단합니다. 이것이 React가 단순하다고 여겨지는 이유 중 하나입니다.
반면에 상태가 업데이트될 때마다 해당 컴포넌트를 다시 계산해야 합니다.
React는 아키텍처로 인해 컴포넌트 내부에서 비교적 쉽게 성능 저하가 발생하기 쉽습니다.
메모이제이션은 이를 보완하는 방법입니다.
memo, useMemo, useCallback 등을 통해 렌더링 성능을 향상시킵니다.
결론 및 참고
NPM에는 비슷한 라이브러리들이 있으니, 해당 기능이 필요할 경우 라이브러리를 사용하세요
intersection observer 더 알아보기
'FrontEnd' 카테고리의 다른 글
리액트 개발자를 위한 CSS Variable (0) | 2022.06.24 |
---|---|
React Portal과 타입스크립트로 모달 만들기 (0) | 2022.06.22 |
리액트 쿼리 : 폼 (2) | 2022.06.20 |
리액트 쿼리 : 뮤테이션 (0) | 2022.06.20 |
리액트 쿼리 : 네트워크 오프라인 시 동작 (2) | 2022.06.20 |