개발자
류준열
모달 렌더링 속도 66% 향상 (약 150ms -> 50ms)
버벅임 발견
레이지로딩이 안되는 이미지들이 있어서, 코드를 보니 모달이 꺼져있는 상태를 visibility:hidden으로 처리하여 항상 렌더링된 상태였다.
이를 display:none으로 바꾸니 레이지로딩은 되었지만 모달을 킬 때 아래와 같은 버벅임이 생겼다.


display:none은 언마운트가 아니다.
이 문제를 해결하면서 잘못 알고 있던 내용을 바로잡았다.
display:none은 렌더트리에 없기 때문에 언마운트라 생각했는데 그게 아니라 css 속성으로 가려지는 것 뿐이었다.
아래처럼 간단히 실험해보았다.

display:none은 마운트된다.
Next의 RSC payload는 렌더트리가 아닌 DOM이다.
Next의 rsc payload는 렌더트리가 아닌 DOM에 관한 데이터다. (공식문서)
그렇기 때문에 DOM에 포함된 display:none은 마운트 되었다가 css에 의해 렌더트리에서 제거되는 것 뿐이다.
원인 발견: react-slick
display가 none에서 block으로 바뀌는것뿐인데 왜 버벅임이 생기는지를 찾다가 react-slick을 지우면 버벅임이 사라지는 것을 발견했다.
react-slick의 inner-slider.js 에서 문제를 찾을 수 있었다.
코드 분석
componentDidMount 에서 슬라이더는 this.updateState를 호출해 내부 스타일(ex: width)등을 계산한다.
// componentDidMount
componentDidMount = () => {
...
let spec = { listRef: this.list, trackRef: this.track, ...this.props };
this.updateState(spec, true, () => {
this.adaptHeight();
this.props.autoplay && this.autoPlay("update");
});
...
this.ro = new ResizeObserver(() => {
...
this.onWindowResized();
});
};
// updateState
updateState = (spec, setTrackStyle, callback) => {
let updatedState = initializedState(spec);
spec = { ...spec, ...updatedState, slideIndex: updatedState.currentSlide };
let targetLeft = getTrackLeft(spec);
spec = { ...spec, left: targetLeft };
let trackStyle = getTrackCSS(spec);
if (
setTrackStyle ||
React.Children.count(this.props.children) !==
React.Children.count(spec.children)
) {
updatedState["trackStyle"] = trackStyle;
}
this.setState(updatedState, callback);
};
// initializedState
export const initializedState = spec => {
// spec also contains listRef, trackRef
let slideCount = React.Children.count(spec.children);
const listNode = spec.listRef;
let listWidth = Math.ceil(getWidth(listNode));
const trackNode = spec.trackRef && spec.trackRef.node;
let trackWidth = Math.ceil(getWidth(trackNode));
let slideWidth;
...
이때 updateState는 곧바로 initializedState를 호출하고, 여기서 listRef랑 연결된 컨테이너가 display:none 상태면 모든 레이아웃 정보가 0으로 저장된 상태로 setState되어 마운트된다.
또한 initializedState 내부의 getTrackCSS는 translate를 계산한다.
// getTrackCSS
export const getTrackCSS = spec => {
...
if (spec.useTransform) {
let WebkitTransform = !spec.vertical
? "translate3d(" + spec.left + "px, 0px, 0px)"
: "translate3d(0px, " + spec.left + "px, 0px)";
let transform = !spec.vertical
? "translate3d(" + spec.left + "px, 0px, 0px)"
: "translate3d(0px, " + spec.left + "px, 0px)";
let msTransform = !spec.vertical
? "translateX(" + spec.left + "px)"
: "translateY(" + spec.left + "px)";
style = {
...style,
WebkitTransform,
transform,
msTransform
};
}
그러다 외부에서 display:block 으로 전환되는 순간에 ref와 연결된 this.list 를 감시하던 observer가 크기 변화를 발견하고 onWindowResized()를 호출한다.
this.ro = new ResizeObserver(() => {
if (this.state.animating) {
this.onWindowResized(false);
...
} else {
this.onWindowResized();
}
});
this.ro.observe(this.list);
onWindowResized는 50ms 디바운스를 거쳐 resizeWindow를 호출하고 resizeWindow는 스타일을 재계산하여 setState한다.
// onWindowResized
onWindowResized = setTrackStyle => {
if (this.debouncedResize) this.debouncedResize.cancel();
this.debouncedResize = debounce(() => this.resizeWindow(setTrackStyle),50);
this.debouncedResize();
};
// resizeWindow
resizeWindow = (setTrackStyle = true) => {
const isTrackMounted = Boolean(this.track && this.track.node);
if (!isTrackMounted) return;
let spec = {
listRef: this.list,
trackRef: this.track,
...this.props,
...this.state
};
this.updateState(spec, setTrackStyle, () => {
if (this.props.autoplay) this.autoPlay("update");
else this.pause("paused");
});
...
};
// updateState
updateState = (spec, setTrackStyle, callback) => {
let updatedState = initializedState(spec);
spec = { ...spec, ...updatedState, slideIndex: updatedState.currentSlide };
let targetLeft = getTrackLeft(spec);
spec = { ...spec, left: targetLeft };
let trackStyle = getTrackCSS(spec);
if (setTrackStyle) {
updatedState["trackStyle"] = trackStyle;
}
this.setState(updatedState, callback);
};
즉, 모달을 킬때 display:none 상태에서 display:block 으로 전환될때 ResizeObserver -> updateState 순환이 일어나고 그때 translate가 새로 계산되어 setState되면서 버벅임이 보이는 것이다.
반면 이전에 visibility:hidden 처리했을때 버벅임이 보이지 않았던 이유는 이미 켜진 레이아웃 상태로 렌더링되어 있었기 때문이다. (mdn)
해결 및 결과
원인을 알고나면 해결책은 의외로 간단할때가 있다. 지금이 그렇다.
display:none 으로 모달을 끄는 것이 아니라, 조건부 렌더링으로 껐다. (아래 이슈 때문에 마우스 호버시 이미지 사전로딩도 함께 적용했다)

모달 렌더링 속도 개선: 150ms -> 47ms (아래 이미지 참고)
ASIS
버벅임을 거쳐서 150ms 후 전체 모달 렌더링 완료

<div className = {isOpen ? 'visible' : 'hidden'><Modal /></div>
TOBE
47ms 후 모달 렌더링 완료

<>{isOpen && <Modal />}</>