Also check out the codesandbox with the complete code.
Our first intuitive take is to make the component pure:
class PureIcon extends PureComponent {
render() {
return <Icon />;
}
}
// or, with a dash recompose
const PureIcon = pure(Icon);
Fine, this helps make the updates almost free. But mounting the component still takes time. Honestly, does it make sense to build the vDOM every time we mount, given that we know it'll always be the same? No. Great, we can make it better.
So, we want to cache the vDOM globally instead of building it from scratch every time we mount. Note that we can't use _.memoize(Icon)
because Icon is still called with the props argument that gets a new reference every time. No problem, we'll write it ourselves:
let cache = null;
const ManualMemoIcon = () => {
if (!cache) {
cache = <Icon />;
}
return cache;
};
So, we only build the vDOM (<Icon />
) on the first mount, application-wide. Should be good? It's not. The premise of React that building vDOM is cheap is true, after all. Once React quickly gets the vDOM, it goes and starts building the real DOM — painfully slowly. We can strip a fraction of the mount time, but the updates were better with Pure
. We have to skip the vDOM-to-DOM step if we want to succeed. Can I do that? You bet I can!
So, caching vDOM does not help us much — we need to cache the real DOM. The plan is simple: when we first (application-wide first) mount the component, we take the resulting DOM and put it into a variable. On subsequent mounts we don't really render anything, but clone the saved DOM subtree into our mount point. In code:
let domCache;
class DomMemoIcon extends PureComponent {
componentDidMount() {
if (domCache) {
this.el.appendChild(domCache.cloneNode(true));
} else {
domCache = this.el.firstChild;
}
}
render() {
// yes, there may be minor trouble with simultaneous first mounts
const children = domCache ? null : <Icon />;
return <div ref={e => this.el = e}>{ children }</div>;
}
}
This works! In my truck benchmark, we're down from 25 to 5 ms — pretty amazing! Let's now focus on what we've lost in the process:
Sounds like a fair deal to me. But can we go faster yet? Ha-ha, we can. If you never have more than one component instance mounted at the same time, you can skip the .cloneNode
. Or you could add instance counting to treat this as a special case.
But we like props, props are cool! Can we please have them back? Maybe we can. If the component's prop space — that is, all the prop combinations we ever use — is fairly small, we could put the DOMs for each prop object into a map of sorts: { [prop object] -> resulting DOM }
, then try to retrieve the DOM from cache on each mount / update. I'll leave implementing this to you as an exercise.
So, do I suggest actually using this? Probably not, it's very tricky and not very useful. If you can make it work for you — great, let me know. Anyways, it's very cool that we can stuff like this is doable with React.