mithril.js
mithril.js copied to clipboard
Memory usage statistics
I did some research into our memory allocation patterns within Chrome.
- Each vnode requires 52 bytes total allocated.
- 10 slots for 10 properties, 4 bytes per slot
- 12 bytes of general overhead per object
- An empty attributes object allocates 28 bytes. Adding a single property (say,
{a: 1}
) reduces that allocation to only 16 bytes, indicating it's allocating 4 slots by default for empty objects and only the minimum slots needed for non-empty objects. - A simple
<div class="foo">foo</div>
inserted into the body as its first child requires 140 bytes for the<div>
and 96 bytes for the inner text node, resulting in 236 bytes total. - A simple cached
m("div.foo", "foo")
allocates 80 bytes- 52 bytes for the vnode
- 28 bytes for the (unnecessarily) cloned
vnode.attrs
assembled from the selector cache entry, created from an empty object with 1 property added
- A simple cached
m("div.container", m("div.foo", "foo"))
allocates 252 bytes- 80 each for 2 vnodes, same as above
- 92 bytes for a single-element array: 16 bytes for the wrapper object and 12 bytes of overhead + 16 slots reserved (as it's starting from empty) for the underlying elements array
- A simple cached
m("div.container", m("div.foo", "foo"), m("div.bar", "bar"))
allocates 332 bytes- 80 each for 3 vnodes, same as above
- 92 bytes for a single-element array: 16 bytes for the wrapper object and 12 bytes of overhead + 16 slots reserved (as it's starting from empty) for the underlying elements array
- A full
m.render(root, m("div.foo", "foo"))
against an empty, pre-initialized detached DOM node allocates:- 236 bytes for the generated DOM, same as above:
- 140 bytes for the
<div>
- 96 bytes for the detached text node
- 140 bytes for the
- 108 bytes of object overhead
- 80 bytes for the vnode, same as above
- 28 bytes for the empty
vnode.state
object allocated for each vnode
- 236 bytes for the generated DOM, same as above:
- A full
m.render(root, m("div.container", m("div.foo", "foo")))
against an empty, pre-initialized detached DOM node allocates:- 376 bytes for the generated DOM, same as above:
- 140 bytes each for 2
<div>
s - 96 bytes for the detached text node
- 140 bytes each for 2
- 308 bytes of object overhead
- 252 bytes can be explained away per above
- 28 bytes each for empty
vnode.state
objects allocated for 2 vnodes
- 376 bytes for the generated DOM, same as above:
- A full
m.render(root, m("div.container", m("div.foo", "foo"), m("div.bar", "bar"))
against an empty, pre-initialized detached DOM node allocates:- 612 bytes for the generated DOM, same as above:
- 140 bytes each for 3
<div>
s - 96 bytes each for 2 detached text nodes
- 140 bytes each for 3
- 416 bytes of object overhead
- 332 bytes can be explained away per above
- 28 bytes each for empty
vnode.state
objects allocated for 3 vnodes
- 612 bytes for the generated DOM, same as above:
I see a few easy ways to cut down on memory allocation dramatically:
- Fix
execSelector
to return the underlying frozen object directly rather than cloning it when no user-provided attributes exist- Saves 28 bytes per DOM vnode reusing a cached selector
- (Breaking) Ditch
vnode.state
on DOM elements - it's rarely used anyways- Saves 28 bytes per DOM vnode
- Optimize for single-element children (common case) to only allocate 1 slot for it
- Saves 60 bytes per non-empty DOM vnode created via
m("tag", child)
(a relatively common case)
- Saves 60 bytes per non-empty DOM vnode created via
In total, I predict that would reduce total memory consumption of front-end view code by roughly 5-20% along with a 20-30% reduction in object generation depending on the nature of the trees (I'm accounting for style rendering in that overestimate):
- A full
m.render(root, m("div.foo", "foo"))
against an empty, pre-initialized detached DOM node allocates:- 236 bytes for the generated DOM, same as above:
- 140 bytes for the
<div>
- 96 bytes for the detached text node
- 140 bytes for the
- 52 bytes of object overhead for the vnode
- Savings: 16% memory (344 B → 288 B) + 40% objects (5 → 3)
- 236 bytes for the generated DOM, same as above:
- A full
m.render(root, m("div.container", m("div.foo", "foo")))
against an empty, pre-initialized detached DOM node allocates:- 376 bytes for the generated DOM, same as above:
- 280 bytes for 2
<div>
s - 96 bytes for the detached text node
- 280 bytes for 2
- 136 bytes of object overhead:
- 52 bytes each for 2 vnodes
- 32 bytes for a single-element array: 16 bytes for the wrapper object and 12 bytes of overhead + 1 slot for the underlying elements array
- Savings: 25% memory (684 B → 512 B) + 22% objects (9 → 7)
- 376 bytes for the generated DOM, same as above:
- A full
m.render(root, m("div.container", m("div.foo", "foo"), m("div.bar", "bar"))
against an empty, pre-initialized detached DOM node allocates:- 612 bytes for the generated DOM, same as above:
- 140 bytes each for 3
<div>
s - 96 bytes each for 2 detached text nodes
- 140 bytes each for 3
- 332 bytes of object overhead:
- 80 each for 3 vnodes, same as above
- 92 bytes for a single-element array: 16 bytes for the wrapper object and 12 bytes of overhead + 16 slots reserved (as it's starting from empty) for the underlying elements array
- Savings: 8% memory (1028 B → 944 B) + 23% objects (13 → 10)
- 612 bytes for the generated DOM, same as above:
Notes:
- Those with lots of single-child nodes would see a much larger improvement in memory than those with relatively few.
- Those with lots of empty nodes will see a much larger reduction in objects allocated (and thus reduced GC pressure and more nursery usage) than those with relatively few.
This would be relatively straightforward to implement:
- The change in how we allocate attributes would just add an extra condition, and might even speed that path up slightly.
- Removing
vnode.state
from DOM vnodes would be as simple as just moving that to the create component flow. - Fixing
Vnode.normalizeChildren
to special-case single-element arrays -hyperscriptVnode
already returns them, butVnode.normalizeChildren
doesn't check for that.- That branch would also be very simple:
if (input.length === 1) return [Vnode.normalize(input[0])]
. It wouldn't need to verify keys as it's a degenerate case, so a mild perf boost may also come out of it.
- That branch would also be very simple:
Very interesting and accurate findings!
Great findings
Also forgot to mention the part on reusing cached attributes will also moderately reduce object count and mildly memory usage on update.