Skip to content

Commit 9b3c97f

Browse files
bmeurerdevelopit
authored andcommitted
Improve performance with multiple documents by up to 100x (#24)
Previously this exported a single function `undom()`, which had all the classes and functions inside, and would create new classes (including new prototypes, constructors, etc. for those classes) everytime you create a new document, which is not very efficient to say the least. So the first step to improve the performance here was to move all the classes and functions out of the `undom()` function (which I took the liberty to rename to `createDocument()` now, as that seems to make more sense to me). The next step was to move the methods and accessors that were slapped onto the `Document` and `Element` instances to the respective prototypes instead. Specifically we avoid installing the 'cssText' and 'className' accessors in the constructor, and instead have these accessors installed once on the `Element.prototype`, which gave a huge performance boost. The execution time of the following micro-benchmark, which creates lot's of documents and adds a single HTMLUnknownElement <app-root> to it, goes from around 6690ms to 67ms, which is a solid **100x** performance improvement. ```js const createDocument = require('undom'); function render() { const d = createDocument(); d.body.appendChild(d.createElement('app-root')); return d; } for (let i = 0; i < 1e2; ++i) render(); console.time('timer'); for (let i = 0; i < 1e5; ++i) render(); console.timeEnd('timer'); ``` Note that this is a breaking change in the sense that prototypes will be shared by multiple documents. But this seems to be consistent with what other DOM libraries like jsdom and domino do.
1 parent 657ed96 commit 9b3c97f

File tree

1 file changed

+150
-161
lines changed

1 file changed

+150
-161
lines changed

src/undom.js

Lines changed: 150 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -20,201 +20,190 @@ const NODE_TYPES = {
2020
*/
2121

2222

23-
/** Create a minimally viable DOM Document
24-
* @returns {Document} document
25-
*/
26-
export default function undom() {
23+
function isElement(node) {
24+
return node.nodeType===1;
25+
}
2726

28-
function isElement(node) {
29-
return node.nodeType===1;
27+
class Node {
28+
constructor(nodeType, nodeName) {
29+
this.nodeType = nodeType;
30+
this.nodeName = nodeName;
31+
this.childNodes = [];
3032
}
31-
32-
class Node {
33-
constructor(nodeType, nodeName) {
34-
this.nodeType = nodeType;
35-
this.nodeName = nodeName;
36-
this.childNodes = [];
37-
}
38-
get nextSibling() {
39-
let p = this.parentNode;
40-
if (p) return p.childNodes[findWhere(p.childNodes, this, true) + 1];
41-
}
42-
get previousSibling() {
43-
let p = this.parentNode;
44-
if (p) return p.childNodes[findWhere(p.childNodes, this, true) - 1];
45-
}
46-
get firstChild() {
47-
return this.childNodes[0];
48-
}
49-
get lastChild() {
50-
return this.childNodes[this.childNodes.length-1];
51-
}
52-
appendChild(child) {
53-
this.insertBefore(child);
54-
return child;
55-
}
56-
insertBefore(child, ref) {
57-
child.remove();
58-
child.parentNode = this;
59-
!ref ? this.childNodes.push(child) : splice(this.childNodes, ref, child);
60-
return child;
61-
}
62-
replaceChild(child, ref) {
63-
if (ref.parentNode===this) {
64-
this.insertBefore(child, ref);
65-
ref.remove();
66-
return ref;
67-
}
68-
}
69-
removeChild(child) {
70-
splice(this.childNodes, child);
71-
return child;
72-
}
73-
remove() {
74-
if (this.parentNode) this.parentNode.removeChild(this);
33+
get nextSibling() {
34+
let p = this.parentNode;
35+
if (p) return p.childNodes[findWhere(p.childNodes, this, true) + 1];
36+
}
37+
get previousSibling() {
38+
let p = this.parentNode;
39+
if (p) return p.childNodes[findWhere(p.childNodes, this, true) - 1];
40+
}
41+
get firstChild() {
42+
return this.childNodes[0];
43+
}
44+
get lastChild() {
45+
return this.childNodes[this.childNodes.length-1];
46+
}
47+
appendChild(child) {
48+
this.insertBefore(child);
49+
return child;
50+
}
51+
insertBefore(child, ref) {
52+
child.remove();
53+
child.parentNode = this;
54+
!ref ? this.childNodes.push(child) : splice(this.childNodes, ref, child);
55+
return child;
56+
}
57+
replaceChild(child, ref) {
58+
if (ref.parentNode===this) {
59+
this.insertBefore(child, ref);
60+
ref.remove();
61+
return ref;
7562
}
7663
}
64+
removeChild(child) {
65+
splice(this.childNodes, child);
66+
return child;
67+
}
68+
remove() {
69+
if (this.parentNode) this.parentNode.removeChild(this);
70+
}
71+
}
7772

7873

79-
class Text extends Node {
80-
constructor(text) {
81-
super(3, '#text'); // TEXT_NODE
82-
this.nodeValue = text;
83-
}
84-
set textContent(text) {
85-
this.nodeValue = text;
86-
}
87-
get textContent() {
88-
return this.nodeValue;
89-
}
74+
class Text extends Node {
75+
constructor(text) {
76+
super(3, '#text'); // TEXT_NODE
77+
this.nodeValue = text;
9078
}
79+
set textContent(text) {
80+
this.nodeValue = text;
81+
}
82+
get textContent() {
83+
return this.nodeValue;
84+
}
85+
}
9186

9287

93-
class Element extends Node {
94-
constructor(nodeType, nodeName) {
95-
super(nodeType || 1, nodeName); // ELEMENT_NODE
96-
this.attributes = [];
97-
this.__handlers = {};
98-
this.style = {};
99-
Object.defineProperty(this, 'className', {
100-
set: val => { this.setAttribute('class', val); },
101-
get: () => this.getAttribute('class')
102-
});
103-
Object.defineProperty(this.style, 'cssText', {
104-
set: val => { this.setAttribute('style', val); },
105-
get: () => this.getAttribute('style')
106-
});
107-
}
88+
class Element extends Node {
89+
constructor(nodeType, nodeName) {
90+
super(nodeType || 1, nodeName); // ELEMENT_NODE
91+
this.attributes = [];
92+
this.__handlers = {};
93+
this.style = {};
94+
}
10895

109-
get children() {
110-
return this.childNodes.filter(isElement);
111-
}
96+
get className() { return this.getAttribute('class'); }
97+
set className(val) { this.setAttribute('class', val); }
11298

113-
setAttribute(key, value) {
114-
this.setAttributeNS(null, key, value);
115-
}
116-
getAttribute(key) {
117-
return this.getAttributeNS(null, key);
118-
}
119-
removeAttribute(key) {
120-
this.removeAttributeNS(null, key);
121-
}
99+
get cssText() { return this.getAttribute('style'); }
100+
set cssText(val) { this.setAttribute('style', val); }
122101

123-
setAttributeNS(ns, name, value) {
124-
let attr = findWhere(this.attributes, createAttributeFilter(ns, name));
125-
if (!attr) this.attributes.push(attr = { ns, name });
126-
attr.value = String(value);
127-
}
128-
getAttributeNS(ns, name) {
129-
let attr = findWhere(this.attributes, createAttributeFilter(ns, name));
130-
return attr && attr.value;
131-
}
132-
removeAttributeNS(ns, name) {
133-
splice(this.attributes, createAttributeFilter(ns, name));
134-
}
102+
get children() {
103+
return this.childNodes.filter(isElement);
104+
}
135105

136-
addEventListener(type, handler) {
137-
(this.__handlers[toLower(type)] || (this.__handlers[toLower(type)] = [])).push(handler);
138-
}
139-
removeEventListener(type, handler) {
140-
splice(this.__handlers[toLower(type)], handler, 0, true);
141-
}
142-
dispatchEvent(event) {
143-
let t = event.target = this,
144-
c = event.cancelable,
145-
l, i;
146-
do {
147-
event.currentTarget = t;
148-
l = t.__handlers && t.__handlers[toLower(event.type)];
149-
if (l) for (i=l.length; i--; ) {
150-
if ((l[i].call(t, event) === false || event._end) && c) {
151-
event.defaultPrevented = true;
152-
}
153-
}
154-
} while (event.bubbles && !(c && event._stop) && (t=t.parentNode));
155-
return l!=null;
156-
}
106+
setAttribute(key, value) {
107+
this.setAttributeNS(null, key, value);
108+
}
109+
getAttribute(key) {
110+
return this.getAttributeNS(null, key);
111+
}
112+
removeAttribute(key) {
113+
this.removeAttributeNS(null, key);
157114
}
158115

116+
setAttributeNS(ns, name, value) {
117+
let attr = findWhere(this.attributes, createAttributeFilter(ns, name));
118+
if (!attr) this.attributes.push(attr = { ns, name });
119+
attr.value = String(value);
120+
}
121+
getAttributeNS(ns, name) {
122+
let attr = findWhere(this.attributes, createAttributeFilter(ns, name));
123+
return attr && attr.value;
124+
}
125+
removeAttributeNS(ns, name) {
126+
splice(this.attributes, createAttributeFilter(ns, name));
127+
}
159128

160-
class Document extends Element {
161-
constructor() {
162-
super(9, '#document'); // DOCUMENT_NODE
163-
}
129+
addEventListener(type, handler) {
130+
(this.__handlers[toLower(type)] || (this.__handlers[toLower(type)] = [])).push(handler);
131+
}
132+
removeEventListener(type, handler) {
133+
splice(this.__handlers[toLower(type)], handler, 0, true);
134+
}
135+
dispatchEvent(event) {
136+
let t = event.target = this,
137+
c = event.cancelable,
138+
l, i;
139+
do {
140+
event.currentTarget = t;
141+
l = t.__handlers && t.__handlers[toLower(event.type)];
142+
if (l) for (i=l.length; i--; ) {
143+
if ((l[i].call(t, event) === false || event._end) && c) {
144+
event.defaultPrevented = true;
145+
}
146+
}
147+
} while (event.bubbles && !(c && event._stop) && (t=t.parentNode));
148+
return l!=null;
164149
}
150+
}
165151

166152

167-
class Event {
168-
constructor(type, opts) {
169-
this.type = type;
170-
this.bubbles = !!(opts && opts.bubbles);
171-
this.cancelable = !!(opts && opts.cancelable);
172-
}
173-
stopPropagation() {
174-
this._stop = true;
175-
}
176-
stopImmediatePropagation() {
177-
this._end = this._stop = true;
178-
}
179-
preventDefault() {
180-
this.defaultPrevented = true;
181-
}
153+
class Document extends Element {
154+
constructor() {
155+
super(9, '#document'); // DOCUMENT_NODE
182156
}
183157

184-
185-
function createElement(type) {
158+
createElement(type) {
186159
return new Element(null, String(type).toUpperCase());
187160
}
188161

189-
190-
function createElementNS(ns, type) {
191-
let element = createElement(type);
162+
createElementNS(ns, type) {
163+
let element = this.createElement(type);
192164
element.namespace = ns;
193165
return element;
194166
}
195167

196168

197-
function createTextNode(text) {
169+
createTextNode(text) {
198170
return new Text(text);
199171
}
172+
}
200173

201174

202-
function createDocument() {
203-
let document = new Document();
204-
assign(document, document.defaultView = { document, Document, Node, Text, Element, SVGElement:Element, Event });
205-
assign(document, { createElement, createElementNS, createTextNode });
206-
document.appendChild(
207-
document.documentElement = createElement('html')
208-
);
209-
document.documentElement.appendChild(
210-
document.head = createElement('head')
211-
);
212-
document.documentElement.appendChild(
213-
document.body = createElement('body')
214-
);
215-
return document;
175+
class Event {
176+
constructor(type, opts) {
177+
this.type = type;
178+
this.bubbles = !!(opts && opts.bubbles);
179+
this.cancelable = !!(opts && opts.cancelable);
180+
}
181+
stopPropagation() {
182+
this._stop = true;
183+
}
184+
stopImmediatePropagation() {
185+
this._end = this._stop = true;
186+
}
187+
preventDefault() {
188+
this.defaultPrevented = true;
216189
}
190+
}
217191

218192

219-
return createDocument();
193+
/** Create a minimally viable DOM Document
194+
* @returns {Document} document
195+
*/
196+
export default function createDocument() {
197+
let document = new Document();
198+
assign(document, document.defaultView = { document, Document, Node, Text, Element, SVGElement:Element, Event });
199+
document.appendChild(
200+
document.documentElement = document.createElement('html')
201+
);
202+
document.documentElement.appendChild(
203+
document.head = document.createElement('head')
204+
);
205+
document.documentElement.appendChild(
206+
document.body = document.createElement('body')
207+
);
208+
return document;
220209
}

0 commit comments

Comments
 (0)