해당 글은 Million.js v2.3.2을 기준으로 작성되었습니다
Million.js 제작자의 트윗을 우연히 보게 되었는데 React 환경에서 성능 최적화를 이끌어내는 방식이 흥미로워서 파헤쳐 보았다.
Million.js는 무엇인가
Million.js는 단순히 React Component를 HOC(Higher Order Component)로 감싸는 것만으로도 렌더링 속도를 빠르게 만들어 주는 virtual DOM 라이브러리이다. 어떻게 개선하는지가 재밌는데 뒤에서 자세히 기술하겠지만 react reconciliation 대신 자체 제작한 virtual DOM을 사용해 조작하는 식으로 개선하였다.
(Million.js Documentation > Introduction)
Million is an extremely fast and lightweight (
<4kb
) virtual DOM that makes React components up to 70% fasterOh man... Another /virtual dom|javascript/gi framework? I'm fine with React already, why do I need this?
Million works with React. Million makes creating web apps just as easy (It's just wrapping a React component!), but with faster rendering and loading speeds. By using a fine-tuned, optimized virtual DOM, Million.js reduces the overhead of React.
실제 렌더링 속도는 krausest/js-framework-benchmark 벤치마크를 참조해보면 확연하게 차이가 나는 걸 확인할 수 있다.
어떻게 성능 최적화를 이끌어냈을까?
리액트는 잘 알려진 것처럼 virtual DOM 데이터와 diff 알고리즘으로 reconciliation 과정을 거쳐 화면을 수정한다. 그리고 이 과정은 n
개의 node가 있는 트리에 대해 O(n)
복잡도를 가지게 된다. (React Reconciliation document)
node가 많아질수록 diff 작업이 오래 걸리기 때문에 diff 과정에서 병목 현상이 발생하는 건 리액트 개발자라면 한 번쯤 격어봤을 것이다.
Million.js는 reconciliation을 사용하지 않기 때문에 tree를 렌더마다 재생성하지도 않고 node에 대한 diff 알고리즘도 사용하지 않는다.
대신 보다 fine-grained reactivity framework(SolidJS, Qwik…)에 가깝게 필요한 부분만 update 하는데 이를 위해 정적 분석(Static Analysis)과 더티 체킹(Dirty Checking)을 사용한다.
- 컴포넌트를 정적 분석해 JSX tree의 변경될 수 있는 부분(dynamic part), 즉
{expression}
의 tree 내 위치와 데이터 및 관련 정보를 수집해 저장한다. (해당 데이터를 "Edit Map"이라 부른다) - 컴포넌트가 update 되면 Edit Map을 통해 데이터를 이전과 비교(Dirty Checking)함으로써 변경된 부분만 DOM 업데이트를 진행한다.
위 처리 과정은 JSX tree에 expression이 m개 존재할 때 O(m)
의 복잡도를 가지며 node 개수에 영향을 받지 않고 update 처리를 할 수 있다.