-
Notifications
You must be signed in to change notification settings - Fork 3
/
optimal-virtual-dom.html
72 lines (60 loc) · 10 KB
/
optimal-virtual-dom.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="author" content="Kabir Shah">
<title>Optimal Virtual DOM | Blog of Kabir Shah</title>
<link id="favicon" rel="shortcut icon"/>
<link rel="alternate" type="application/json" title="Kabir Shah" href="/feed.json"/>
<link href="https://fonts.googleapis.com/css?family=Inconsolata&display=swap" rel="stylesheet"/>
<link rel="stylesheet" type="text/css" href="/css/lib/grain.min.css"/>
<link rel="stylesheet" type="text/css" href="/css/styles.css"/>
<script>
(function(i,s,o,g,r,a,m){i["GoogleAnalyticsObject"]=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,"script","https://www.google-analytics.com/analytics.js","ga");
ga("create", "UA-70792533-7", "auto");
ga("send", "pageview");
</script>
</head>
<body>
<div class="s-x-33 p-x-10 p-y-10 m-y-6">
<div class="s-x-i">
<a class="s-x-i plain" href="/">
<img src="/img/logo.png" alt="Personal Logo" class="s-y-8"/>
</a>
<h1 class="s-x-i">Optimal Virtual DOM</h1>
<p class="s-x-i">December 6, 2019</p>
</div>
<p class="s-x-i">The virtual DOM is an idea that stems from functional programming in user interfaces. On every update new UI trees replace the current one. The problem arises, however, when this idea of an immutable, declarative view is applied in the browser.</p><p class="s-x-i">The DOM is inherently imperative; it is updated through mutating method calls. A virtual DOM bridges the gap between declarative and imperative environments, accepting lightweight trees while mutating the DOM under the hood.</p><p class="s-x-i">Still, a fast implementation of the virtual DOM can be a difficult task. As I've worked on <a href="https://kbrsh.github.io/moon">Moon</a>, I've tried many different approaches to the diffing algorithm, with the most <a href="https://github.com/kbrsh/moon/commit/e7a7cd9ab427be89cb7efee70df86dfe0401d770">recent revision</a> being explained here. It's good at benchmarks because it sticks to one principle: avoiding the DOM as much as possible.</p><p class="s-x-i">There are many ways to approach a virtual DOM implementation, each building on top of the previous one to gain better performance.</p><h2 id="replace" class="s-x-i">Replace</h2><p class="s-x-i">The simplest way of implementing a virtual DOM is based on replacing elements. A new element created from a virtual node replaces the old one.</p><pre class="s-x-i s-b-2 p-x-4 p-y-4"><code>node<span class="token punctuation">.</span>parentNode<span class="token punctuation">.</span><span class="token function">replaceChild</span><span class="token punctuation">(</span><span class="token function">nodeFromVNode</span><span class="token punctuation">(</span>vnode<span class="token punctuation">)</span><span class="token punctuation">,</span> node<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre><p class="s-x-i">This is wasteful because the DOM was not designed for large numbers of element creation, preferring granular method calls instead.</p><h2 id="dom-diff" class="s-x-i">DOM Diff</h2><p class="s-x-i">Transforming the DOM through a diff and patch between a virtual node and the DOM allows for more precise changes. For example, updating a <code class="s-b-2 p-x-2 p-y-2">className</code> property may check against the current state of the DOM.</p><pre class="s-x-i s-b-2 p-x-4 p-y-4"><code><span class="token keyword">if</span> <span class="token punctuation">(</span>node<span class="token punctuation">.</span>className <span class="token operator">!==</span> vnode<span class="token punctuation">.</span>className<span class="token punctuation">)</span> <span class="token punctuation">{</span>
node<span class="token punctuation">.</span>className <span class="token operator">=</span> vnode<span class="token punctuation">.</span>className<span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre><p class="s-x-i">Even so, <em>reading</em> the DOM is bad for performance. Virtual node object property access is much faster.</p><h2 id="virtual-dom-diff" class="s-x-i">Virtual DOM Diff</h2><p class="s-x-i">Instead of diffing against the DOM, the previous virtual DOM can be stored and used instead.</p><pre class="s-x-i s-b-2 p-x-4 p-y-4"><code><span class="token keyword">if</span> <span class="token punctuation">(</span>vnodeOld<span class="token punctuation">.</span>className <span class="token operator">!==</span> vnodeNew<span class="token punctuation">.</span>className<span class="token punctuation">)</span> <span class="token punctuation">{</span>
node<span class="token punctuation">.</span>className <span class="token operator">=</span> vnodeNew<span class="token punctuation">.</span>className<span class="token punctuation">;</span>
<span class="token punctuation">}</span></code></pre><p class="s-x-i">Now, the DOM is accessed only when it is necessary — to modify it. However, when diffing against children, this means accessing <code class="s-b-2 p-x-2 p-y-2">childNodes</code>:</p><pre class="s-x-i s-b-2 p-x-4 p-y-4"><code><span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">let</span> i <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> i <span class="token operator"><</span> length<span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> vchildOld <span class="token operator">=</span> vnodeOld<span class="token punctuation">.</span>children<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> vchildNew <span class="token operator">=</span> vnodeNew<span class="token punctuation">.</span>children<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>vchildOld <span class="token operator">!==</span> vchildNew<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token comment">// Assume that `diff` takes an old virtual node, new virtual node, and a</span>
<span class="token comment">// DOM element to patch.</span>
<span class="token function">diff</span><span class="token punctuation">(</span>vchildOld<span class="token punctuation">,</span> vchildNew<span class="token punctuation">,</span> node<span class="token punctuation">.</span>childNodes<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre><p class="s-x-i">Even a loop using <code class="s-b-2 p-x-2 p-y-2">firstChild</code> and <code class="s-b-2 p-x-2 p-y-2">nextSibling</code> would still access the DOM on every iteration. This is slow. Moon gets around this by keeping track of children in a separate property on every DOM element called <code class="s-b-2 p-x-2 p-y-2">MoonChildren</code>.</p><pre class="s-x-i s-b-2 p-x-4 p-y-4"><code><span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token keyword">let</span> i <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> i <span class="token operator"><</span> length<span class="token punctuation">;</span> i<span class="token operator">++</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">const</span> vchildOld <span class="token operator">=</span> vnodeOld<span class="token punctuation">.</span>children<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token keyword">const</span> vchildNew <span class="token operator">=</span> vnodeNew<span class="token punctuation">.</span>children<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>vchildOld <span class="token operator">!==</span> vchildNew<span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token function">diff</span><span class="token punctuation">(</span>vchildOld<span class="token punctuation">,</span> vchildNew<span class="token punctuation">,</span> node<span class="token punctuation">.</span>MoonChildren<span class="token punctuation">[</span>i<span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span></code></pre><h2 id="conclusion" class="s-x-i">Conclusion</h2><p class="s-x-i">A diff between virtual nodes, accessing the DOM only for modification, is the fastest approach to a virtual DOM. It avoids the DOM as much as possible, favoring plain JavaScript objects instead, making reading and writing much cheaper. Combined with using constructors for virtual nodes, storing events on DOM nodes, and using a purely functional design, Moon's view driver is faster than ever before.</p>
<div class="s-x-i m-x-5">
<p><a href="https://kabir.sh">Portfolio</a></p>
<p><a href="https://blog.kabir.sh">Blog</a></p>
<p><a href="https://twitter.com/kbrshah">Twitter</a></p>
<p><a href="https://github.com/kbrsh">GitHub</a></p>
</div>
</div>
<script type="text/javascript" src="/js/index.js"></script>
</body>
</html>