React 의 성능을 조금 이라도 올려보자 (Performance Optimize)
React 는 UI (User Interface) 라이브러리 로써 매우 가볍고 빠르다.
또한 React 는 개발자에게 매우 자유롭다.
자유롭다는 뜻은 만드는 사람이 얼마나 이해하고, 어떻게 사용 하느냐에 따라, 제품의 성능이 극명하게 갈린다는 뜻이기도 하다.
아직은 React 을 정확히 이해하지 못하는 것 같아 최근 성능을 올리기 위해 배운 내용들을 정리 해보고자 한다.
브라우저 렌더링 과 React Virtual DOM
브라우저가 html
와 css
를 가지고 화면을 그리기 위해서는 먼저 DOM Tree
와 CSSOM Tree
로 Render Tree
를 만들어야 한다.
그 후 Layer 위치를 잡는 flow 단계와 색을 칠하는 paint 단계를 거쳐 최종적으로 화면에 출력하게 된다.
첫 렌더링이 끝나고, 사용자가 화면을 조작하기 시작하면, 무수히 많은 DOM Tree
의 변화와 Reflow
와 Repaint
가 일어나게 되는데, 이는 이전 과정의 반복으로 많은 리소스가 필요하다.
많은 리소스가 필요한 작업들은 화면에 반영 되기까지 시간이 걸리기 때문에, 화면이 뚝뚝 끊기는 느낌을 줘 유저 경험에 좋지 않다.
Virtual DOM 과 Reconciliation
React 는 화면이 그려지는 성능을 빠르게 하기 위해, 브라우저 Memory 에 Tree 구조로 이루어진 Virtual DOM 을 저장하고 있다.
이 Virtual DOM 으로 렌더링 여부를 결정하는데, 브라우저의 DOM Tree 를 변경하기 전에 이전 Virtual DOM
와 변경될 Virtual DOM
을 비교 (Diff)
하고, 바뀐 최소 부분만을 실제 DOM 에 반영하여, 브라우저 렌더링 과정을 최소화한다.
이 Virtual DOM 은 Reconciliation (재조정) 과정을 통해 재생성하게 된다.
변경이 있어 났을때 각 Virtual DOM 의 Element 와 Component 를 비교하며 재렌더링을 하는데, 이를 자식 노드까지 반복하게 된다.
모든 비교가 끝나면, DOM Tree 에 반영하게 된다.
비교하는 Element 가 다른경우 div -> section
- 이전 Virtual DOM 을 제거한다. (componentWillUnmount, useEffect clean-up)
- 이때 모든 자식들도 모두 제거되고 재생성 된다.
비교하는 Element 와 Component 가 같은 경우 div -> div
- Virtual DOM 의 property 를 수정한 뒤, 자식들의 노드를 비교한다. (componentWillUpdate, useEffect diff)
- 이때 자식들은 key 를 가질 수 있는데, 이전과 변경되는 key 를 비교한다.
- key 가 없다면, 이전 자식 node 를 삭제하고, key 가 추가 됬다면, 새로 자식 node 를 생성한다.
- key 가 같다면, 비교를 진행한다.
React 는 Virtual DOM 으로 실제 DOM 에 반영하는 빈도를 줄여, React 의 성능을 올리고 있다.
하지만 개발자가 어떻게 Component 를 적용하고 배치 하느냐에 따라 성능 차이가 극명하게 난다.
필요 없는 렌더링 줄이기 shouldComponentUpdate
React 의 Virtual DOM 의 ReRendering
은 4 가지 상황에서 발생된다.
- 부모 Component 가 Rendering 되서, 자식을 비교할때
- props 가 변경 되었을 때
- state 가 변경 되었을 때
- forceUpdate 가 호출되었을 때
이중 부모 Component 가 Rendering 되서, 자식을 비교할때, 자식의 props 가 변경되지 않았지만, 재렌더링되는 것은, 불필요한 작업일 수 있다.
특히 거대한 웹을 만들다보면 Tree 가 깊어지고, 자식들이 많아지면, 비교 해야하는 코드도 많아진다.
Component 의 props 정보를 저장해두고, 동일한 props 가 입력되 었을 때 재랜더링을 막는다면 성능을 올릴 수 있을 것이다.
이러한 기법을 Memoization 라고 부르며, Memoization 이란 입력한 결과를 저장하고 동일한 입력이 다시 발생했을때, 동일한 결과를 빠르게 반환하는 기법이다.
이를 기법을 적용 하기 위해 Class Component
에서는 shouldComponentUpdate
를 제공한다.
shouldComponentUpdate
는 이전 props, state
와 다음 props, state
를 비교하여, 렌더링 여부를 결정할 수 있다.
React 의 Lifecycle 은 create 와 update, delete 로 나눌 수 있는데,
shouldComponentUpdate
는 update 시점의 componentWillUpdate
이전에 발생한다.
이를 이용하면, 불필요한 Rendering 과 자식들의 비교를 막아 성능을 올릴 수 있다.
// jsx
class Class extends React.Component { shouldComponentUpdate(nextProps, nextState) {
console.log('shouldComponentUpdate', nextProps, nextState); return (JSON.stringify(nextProps) != JSON.stringify(this.props))
|| (JSON.stringify(nextState) != JSON.stringify(this.state));
} render() {
//…
}
}
필요 없는 렌더링 줄이기 React.memo
Class Component 에서 shouldComponentUpdate 가 있다면, Functional Component
에는 React.memo
가 있다.
React.memo
는 Component 함수
와 비교 함수(propsAreEqual)
를 받아 Component 를 반환하는 HOC 이다.
props 가 변경 되었을때, 얕은 비교 로, Rerendering 여부를 결정할 수 있다.
// jsxconst Component = () => (<div>text</div>)
const Memo = React.memo(Component,
(prevProps, nextProps) => {
console.log(‘propsAreEqual’, prevProps, nextProps) return JSON.stringify(prevProps) === JSON.stringify(nextProps);
}
);
shouldComponentUpdate
와 React.memo
의 차이점은, shouldComponentUpdate
은 true 일때, Rendering 를 하고,
React.memo
의 propsAreEqual 는 false 일때 Rendering 을 한다.
Memoization
으로 성능을 올리더 라도 props 를 자주 변경하는 Component 는 오히려 비교하는데 많은 성능이 들어, Memoization 기법의 이점을 얻기 힘들 수도 있다.
생각지도 못한 실수 Inline Function
위에서 Memoization
기법으로 Rerendering 을 막는데는 성공했다.
하지만 우리가 React 에 대해 고민하지 않고 쓰다보면, 놓치는 부분이 있다.
props 로 전달 하는 Object 나 Function 은 참조 값을 전달 한다. 때문에 참조 값이 변경되면, Rerendering 이 일어난다.
// jsx
const [bool, toggle] = useState(false);
return (
<MyButton onClick={ (e) => setState(!bool) }>
button
</MyButton>
);
위 코드에서는 onClick 에게 전달하는 함수는 Rendering 시점에 생성하고 있다.
만약 state 변경으로 Rerendering 이 일어나게 되면, onClick 에 새로운 function 을 생성해서 props 로 전달하게 된다.
새로운 props 는 shouldComponentUpdate
나 React.memo
로 Memoization
했던 자식 컴포넌트도 Rerendering 이 일어나게 때문에, Memoization
을 한 의미가 없어진다.
때문에, 이를 특정 값이 변경 되었을 때만 생성하는 방법이 필요하다.
Hooks Memoization
useCallback
와 useMemo
은 Memoization
된 값을 반환하는 Hook 이다.
이를 이용해서 같은 props 를 자식에게 전달하여, Rerendering 을 막을 수 있다.
useCallback
useCallback 은 함수와 비교 배열을 전달받아 함수를 반환 하는 Hook 이다.
이때 비교 배열의 값이 변경 되었을때에만, 새로운 함수를 반환하게 된다.
// jsx
const [bool, toggle] = useState(false);const onClick = useCallback((e) => {
setState(!bool)
}, [bool])return (<>
<MyButton onClick={(e) => setState(!bool)}>
button
</MyButton>
<MyButton onClick={onClick}>
button
</MyButton>
</>);
때문에 Inline Funtion 으로 작성 했을때 발생하던, Rerendering 시의 새로운 함수 생성을 막아 동일한 props 를 전달하므로써, 성능을 올릴 수 있다.
useMemo
useMemo 는 함수와 비교 배열을 전달 받아 값을 반환 하는 Hook 이다.
이때 비교 배열의 값이 변경 되었을때에만, 새로운 값을 반환하게 된다.
// jsx
const {
a, b
} = props;const sum = useMemo(() => {
return a + b;
}, [a, b]);return (<>
<div>
{a + b}
</div>
<div>
{sum}
</div>
</>);
props 값은 동일하지만, a + b 연산은 리렌더링 시점마다 일어나게 된다.
때문에, useMemo 를 이용해서 불필요한 연산을 제거하면 성능을 올릴 수 있다.
useContext
useContext 는 React.createContext
로 생성한 context 의 Provider value
를 구독하는 Hook 이다.
React.memo
나 shouldComponentUpdate
를 사용하더라도 useContext 를 사용하는 Component 는 Rerendering
이 일어난다.
// jsx
const MyContext = React.createContext('false');const ChildComponent = () => {
const value = useContext(MyContext);
return (<div>{value}</div>)
}const Memo = React.memo( ChildComponent )const ParentComponent = () => {
const [state, setState] = useState('false'); useEffect(() => {
setTimeout(() => {
setState('true');
}, 1000);
}, [true]); return <MyContext.Provider value={state}>
<Memo />
</MyContext.Provider>
}
Redux 같은 상태관리 도구들은 Context API 로 구현되어 있기 때문에, useSelector 를 쓸 경우 React.memo 를 쓰더라도 Rerendering 이 일어날 수 있다.
immutable-js
React 에서 props 로 전달 받는 Object 값은 참조 값이다.
때문에, Object 의 property 가 변경 되더라도 React 는 이를 인식하지 못하기 때문에, 리렌더링이 일어나지 않는다.
React 사용자들은, 보통 강제로 Rerendering 을 발생 시키기위해, forceUpdate 를 호출하거나, 내부 state 를 변경 하거나,
아니면 새로운 참조 값을 전달하기 위해, Object.assign
이나 object-rest-spread
(…obj) 를 이용해서 새로운 객체를 생성해서 전달한다.
// javascript
Object.assign(
{},
state,
nextState
)
///
{
...state,
...nextState
}
매번 새로운 Object 참조 값을 전달하는 방식의 경우, 내부 값이 바뀌지 않더라도, 재 생성해서 전달하기 때문에, 이 방법은 Memoization 을 했더라도 불필요한 Rerendering을 발생시키게 된다.
특히 이러한 현상은 Redux 같은 상태관리 도구에서 많이 볼 수 있다.
immutable-js
같은 도구를 사용하면, 같은 값으로 업데이트 하더라도, 새로운 객체가 아니라 현재 객체를 반환하게 되어, 불필요한 Rerendering 을 막을 수 있다.
// jsx
import { Map, is } from 'immutable';var map1 = Map({ a: 1, b: 2, c: 3 });
var map2 = map1.set('c', 3);const Memo = React.memo(Component, (prev, next) => is(prev, next));export default function Render() {
const [state, setState] = useState(map1); useEffect(() => {
setTimeout(() => {
setState(map2);
}, 1000);
}, [true]); return (<Memo data={state} />);
};
성능 측정하기 React Developer Tools
성능을 개선하기 위해 노력하는것도 좋지만, 어느 부분에서 병목이 일어나는지 확인할 수 있는 도구를 이용하면, 더 쉽게 개선 작업을 진행 할 수 있다.
Chrome 의 확장도구 중 React Developer Tools for Chrome 은 Chrome 개발자 도구에서 React 요소들의 병목 지점 측정을 도와준다.
성능 측정하기 Profile API
React Profiler 를 사용하면, mount update시점, 렌더링 된 시간 등을 측정할 수 있다.
// jsx
import React, { Profiler } from "react"
import { unstable_trace as trace } from "scheduler/tracing";function Render() {
const [value, setValue] = React.useState('') const onChange = useCallback((e => {
trace('updated', performance.now(), () => {
setValue(event.target.value)
})
}, []); return (<div>
<React.StrictMode>
<Profiler id="Render" onRender={onRenderCallback}>
<input onChange={onChange} value={value} />
{value}
</Profiler>
</React.StrictMode>
</div>); function onRenderCallback(
id,
phase, // mount | update
actualDuration, // 렌더링 시간
baseDuration, // 전체 서브트리를 렌더링하는데 걸린 시간
startTime, // 렌더링을 시작한 시간
commitTime, // 업데이트요청한 시간
interactions // trace set
) {
//...
}
};
https://gist.github.com/bvaughn/25e6233aeb1b4f0cdb8d8366e54a3977