Skip to content

Commit 6c7b0b0

Browse files
kfuledead-claudia
authored andcommitted
Assorted Performance Improvements (#3041)
* hyperscriptVnode: use spread syntax The implementation adopts spread syntax, inspired by the approach mentioned in the comments. However, argument handling has been adjusted for better performance. Using spread syntax not only allows related comments to be deleted, but also slightly reduces bundle size and improves benchmarks by several percent. All existing tests pass successfully. Note that spread syntax does not seem to be supported by the existing internal bundler in some cases, so the bundler has also been updated to prevent incorrect suffix handling. * allow attrs to be undefined and execute early return in `execSelector()` Since commit f9e5163, attrs is never null/undefined. However, `m.render()` takes into account the case where attrs is null/undefined, and it is better to use undefined from a performance point of view. Also, early return within `execSelector()` contributes to further performance improvement. Some tests are changed because of the null/undefined attrs. This commit may be breaking from this point of view, but the v2.0.4 era allows for attrs to be null/undefined and the impact will be quite small. * use the cached attrs as-is and skip `updateAttrs()` if the static attrs are identical Since `compileSelector()` generates attrs objects containing only strings (no functions or nested objects). So, `updateAttrs()` can safely be skipped by checking the equality of the cached attrs objects themselves if the objects do not contain form attributes. If you mainly use the "static" attrs and do not use dynamic attrs, skipping `updateAttrs()` allows significant performance improvements. Also, the added checks are lightweight and there appears to be little or no performance degradation due to these checks. This commit itself is not a breaking change in the sense that it passes all existing tests. The checks in `m.render()` above and the form attribute checks in `compileSelector()` result in a minor increase in code volume, though. * refactor execSelector() This state.attrs.className != null check is redundant. * README: drop IE11 support It seems that the current codebase (including generator, dom-for and spread syntax) already has little support for IE11 because it requires transpiling as well as polyfills. * prevents existing tests from being changed As this results in a slight decrease in performance improvement, comments regarding potential performance improvements have also been added. * use a common empty attrs object This simplifies the processing within execSelector. * cachedAttrsIsStaticMap: use WeakMap to avoid potential memory leaks * hyperscriptVnode: add performance sensitivity comments * cachedAttrsIsStaticMap: revert back to Map from WeakMap with comments * pass emptyAttrs to the Map constructor This slightly reduces the bundle size.
1 parent 6de598c commit 6c7b0b0

File tree

9 files changed

+150
-62
lines changed

9 files changed

+150
-62
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ A modern client-side JavaScript framework for building Single Page Applications.
1919

2020
Mithril.js is used by companies like Vimeo and Nike, and open source platforms like Lichess 👍.
2121

22-
Mithril.js supports IE11, Firefox ESR, and the last two versions of Firefox, Edge, Safari, and Chrome. No polyfills required. 👌
22+
Mithril.js supports Firefox ESR, and the last two versions of Firefox, Edge, Safari, and Chrome. No polyfills required. 👌
2323

2424
## Installation
2525

render/cachedAttrsIsStaticMap.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"use strict"
2+
3+
var emptyAttrs = require("./emptyAttrs")
4+
5+
// This Map manages the following:
6+
// - Whether an attrs is cached attrs generated by compileSelector().
7+
// - Whether the cached attrs is "static", i.e., does not contain any form attributes.
8+
// These information will be useful to skip updating attrs in render().
9+
//
10+
// Since the attrs used as keys in this map are not released from the selectorCache object,
11+
// there is no risk of memory leaks. Therefore, Map is used here instead of WeakMap.
12+
module.exports = new Map([[emptyAttrs, true]])

render/emptyAttrs.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
"use strict"
2+
3+
// This is an attrs object that is used by default when attrs is undefined or null.
4+
module.exports = {}

render/fragment.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
var Vnode = require("../render/vnode")
44
var hyperscriptVnode = require("./hyperscriptVnode")
55

6-
module.exports = function() {
7-
var vnode = hyperscriptVnode.apply(0, arguments)
6+
module.exports = function(attrs, ...children) {
7+
var vnode = hyperscriptVnode(attrs, children)
88

99
vnode.tag = "["
1010
vnode.children = Vnode.normalizeChildren(vnode.children)

render/hyperscript.js

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
var Vnode = require("../render/vnode")
44
var hyperscriptVnode = require("./hyperscriptVnode")
55
var hasOwn = require("../util/hasOwn")
6+
var emptyAttrs = require("./emptyAttrs")
7+
var cachedAttrsIsStaticMap = require("./cachedAttrsIsStaticMap")
68

79
var selectorParser = /(?:(^|#|\.)([^#\.\[\]]+))|(\[(.+?)(?:\s*=\s*("|'|)((?:\\["'\]]|.)*?)\5)?\])/g
810
var selectorCache = Object.create(null)
@@ -12,8 +14,12 @@ function isEmpty(object) {
1214
return true
1315
}
1416

17+
function isFormAttributeKey(key) {
18+
return key === "value" || key === "checked" || key === "selectedIndex" || key === "selected"
19+
}
20+
1521
function compileSelector(selector) {
16-
var match, tag = "div", classes = [], attrs = {}
22+
var match, tag = "div", classes = [], attrs = {}, isStatic = true
1723
while (match = selectorParser.exec(selector)) {
1824
var type = match[1], value = match[2]
1925
if (type === "" && value !== "") tag = value
@@ -23,32 +29,40 @@ function compileSelector(selector) {
2329
var attrValue = match[6]
2430
if (attrValue) attrValue = attrValue.replace(/\\(["'])/g, "$1").replace(/\\\\/g, "\\")
2531
if (match[4] === "class") classes.push(attrValue)
26-
else attrs[match[4]] = attrValue === "" ? attrValue : attrValue || true
32+
else {
33+
attrs[match[4]] = attrValue === "" ? attrValue : attrValue || true
34+
if (isFormAttributeKey(match[4])) isStatic = false
35+
}
2736
}
2837
}
2938
if (classes.length > 0) attrs.className = classes.join(" ")
30-
if (isEmpty(attrs)) attrs = null
31-
return selectorCache[selector] = {tag: tag, attrs: attrs}
39+
if (isEmpty(attrs)) attrs = emptyAttrs
40+
else cachedAttrsIsStaticMap.set(attrs, isStatic)
41+
return selectorCache[selector] = {tag: tag, attrs: attrs, is: attrs.is}
3242
}
3343

3444
function execSelector(state, vnode) {
45+
vnode.tag = state.tag
46+
3547
var attrs = vnode.attrs
48+
if (attrs == null) {
49+
vnode.attrs = state.attrs
50+
vnode.is = state.is
51+
return vnode
52+
}
53+
3654
var hasClass = hasOwn.call(attrs, "class")
3755
var className = hasClass ? attrs.class : attrs.className
3856

39-
vnode.tag = state.tag
40-
41-
if (state.attrs != null) {
57+
if (state.attrs !== emptyAttrs) {
4258
attrs = Object.assign({}, state.attrs, attrs)
4359

4460
if (className != null || state.attrs.className != null) attrs.className =
4561
className != null
4662
? state.attrs.className != null
4763
? String(state.attrs.className) + " " + String(className)
4864
: className
49-
: state.attrs.className != null
50-
? state.attrs.className
51-
: null
65+
: state.attrs.className
5266
} else {
5367
if (className != null) attrs.className = className
5468
}
@@ -70,12 +84,12 @@ function execSelector(state, vnode) {
7084
return vnode
7185
}
7286

73-
function hyperscript(selector) {
87+
function hyperscript(selector, attrs, ...children) {
7488
if (selector == null || typeof selector !== "string" && typeof selector !== "function" && typeof selector.view !== "function") {
7589
throw Error("The selector must be either a string or a component.");
7690
}
7791

78-
var vnode = hyperscriptVnode.apply(1, arguments)
92+
var vnode = hyperscriptVnode(attrs, children)
7993

8094
if (typeof selector === "string") {
8195
vnode.children = Vnode.normalizeChildren(vnode.children)

render/hyperscriptVnode.js

Lines changed: 13 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,52 +2,22 @@
22

33
var Vnode = require("../render/vnode")
44

5-
// Call via `hyperscriptVnode.apply(startOffset, arguments)`
5+
// Note: the processing of variadic parameters is perf-sensitive.
66
//
7-
// The reason I do it this way, forwarding the arguments and passing the start
8-
// offset in `this`, is so I don't have to create a temporary array in a
9-
// performance-critical path.
7+
// In native ES6, it might be preferable to define hyperscript and fragment
8+
// factories with a final ...args parameter and call hyperscriptVnode(...args),
9+
// since modern engines can optimize spread calls.
1010
//
11-
// In native ES6, I'd instead add a final `...args` parameter to the
12-
// `hyperscript` and `fragment` factories and define this as
13-
// `hyperscriptVnode(...args)`, since modern engines do optimize that away. But
14-
// ES5 (what Mithril.js requires thanks to IE support) doesn't give me that luxury,
15-
// and engines aren't nearly intelligent enough to do either of these:
16-
//
17-
// 1. Elide the allocation for `[].slice.call(arguments, 1)` when it's passed to
18-
// another function only to be indexed.
19-
// 2. Elide an `arguments` allocation when it's passed to any function other
20-
// than `Function.prototype.apply` or `Reflect.apply`.
21-
//
22-
// In ES6, it'd probably look closer to this (I'd need to profile it, though):
23-
// module.exports = function(attrs, ...children) {
24-
// if (attrs == null || typeof attrs === "object" && attrs.tag == null && !Array.isArray(attrs)) {
25-
// if (children.length === 1 && Array.isArray(children[0])) children = children[0]
26-
// } else {
27-
// children = children.length === 0 && Array.isArray(attrs) ? attrs : [attrs, ...children]
28-
// attrs = undefined
29-
// }
30-
//
31-
// if (attrs == null) attrs = {}
32-
// return Vnode("", attrs.key, attrs, children)
33-
// }
34-
module.exports = function() {
35-
var attrs = arguments[this], start = this + 1, children
36-
37-
if (attrs == null) {
38-
attrs = {}
39-
} else if (typeof attrs !== "object" || attrs.tag != null || Array.isArray(attrs)) {
40-
attrs = {}
41-
start = this
42-
}
43-
44-
if (arguments.length === start + 1) {
45-
children = arguments[start]
46-
if (!Array.isArray(children)) children = [children]
11+
// However, benchmarks showed this was not faster. As a result, spread is used
12+
// only in the parameter lists of hyperscript and fragment, while an array is
13+
// passed to hyperscriptVnode.
14+
module.exports = function(attrs, children) {
15+
if (attrs == null || typeof attrs === "object" && attrs.tag == null && !Array.isArray(attrs)) {
16+
if (children.length === 1 && Array.isArray(children[0])) children = children[0]
4717
} else {
48-
children = []
49-
while (start < arguments.length) children.push(arguments[start++])
18+
children = children.length === 0 && Array.isArray(attrs) ? attrs : [attrs, ...children]
19+
attrs = undefined
5020
}
5121

52-
return Vnode("", attrs.key, attrs, children)
22+
return Vnode("", attrs && attrs.key, attrs, children)
5323
}

render/render.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ var Vnode = require("../render/vnode")
44
var df = require("../render/domFor")
55
var delayedRemoval = df.delayedRemoval
66
var domFor = df.domFor
7+
var cachedAttrsIsStaticMap = require("./cachedAttrsIsStaticMap")
78

89
module.exports = function() {
910
var nameSpace = {
@@ -453,7 +454,9 @@ module.exports = function() {
453454
var element = vnode.dom = old.dom
454455
ns = getNameSpace(vnode) || ns
455456

456-
updateAttrs(vnode, old.attrs, vnode.attrs, ns)
457+
if (old.attrs != vnode.attrs || (vnode.attrs != null && !cachedAttrsIsStaticMap.get(vnode.attrs))) {
458+
updateAttrs(vnode, old.attrs, vnode.attrs, ns)
459+
}
457460
if (!maybeSetContentEditable(vnode)) {
458461
updateNodes(element, old.children, vnode.children, hooks, null, ns)
459462
}
@@ -715,7 +718,7 @@ module.exports = function() {
715718
// so removal should be done first to prevent accidental removal for newly setting values.
716719
var val
717720
if (old != null) {
718-
if (old === attrs) {
721+
if (old === attrs && !cachedAttrsIsStaticMap.has(attrs)) {
719722
console.warn("Don't reuse attrs object, use new object for every redraw, this will throw in next major")
720723
}
721724
for (var key in old) {

scripts/_bundler-impl.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,12 @@ module.exports = async (input) => {
158158
})
159159

160160
//fix props
161-
const props = new RegExp(`((?:[^:]\\/\\/.*)?\\.\\s*)(${candidates})|([\\{,]\\s*)(${candidates})(\\s*:)`, "gm")
162-
code = code.replace(props, (match, dot, a, pre, b, post) => {
161+
const props = new RegExp(`(\\.\\.)?((?:[^:]\\/\\/.*)?\\.\\s*)(${candidates})|([\\{,]\\s*)(${candidates})(\\s*:)`, "gm")
162+
code = code.replace(props, (match, dotdot, dot, a, pre, b, post) => {
163163
// Don't do anything because dot was matched in a comment
164164
if (dot && dot.indexOf("//") === 1) return match
165+
// Don't do anything because dot is a part of spread syntax or destructuring
166+
if (dotdot) return match
165167
if (dot) return dot + a.replace(/\d+$/, "")
166168
return pre + b.replace(/\d+$/, "") + post
167169
})

scripts/tests/test-bundler.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,4 +368,87 @@ o.spec("bundler", async () => {
368368
// check that the argument z2 is not z00
369369
o(await bundle(p("a.js"))).equals(";(function() {\nvar z0 = {}\nvar _1 = function(z1){}\nvar b = _1(z0)\nvar z = z0\nvar _5 = function(z2){}\nvar c = _5(z)\n}());")
370370
})
371+
o.spec("spread syntax and destructuring (...)", () => {
372+
o("rest parameter", async () => {
373+
await setup({
374+
"a.js": 'var b = require("./b")\nvar c = require("./c")',
375+
"b.js": "var a = 1\nmodule.exports = a",
376+
"c.js": "function f(d, ...a){}\nmodule.exports = f",
377+
})
378+
379+
o(await bundle(p("a.js"))).equals(";(function() {\nvar a = 1\nvar b = a\nfunction f(d, ...a0){}\nvar c = f\n}());")
380+
})
381+
o("function call", async () => {
382+
await setup({
383+
"a.js": 'var b = require("./b")\nvar c = require("./c")',
384+
"b.js": "var a = 1\nmodule.exports = a",
385+
"c.js": "var a = [1, 2, 3]\nvar d = f(...a)\nmodule.exports = d",
386+
})
387+
388+
o(await bundle(p("a.js"))).equals(";(function() {\nvar a = 1\nvar b = a\nvar a0 = [1, 2, 3]\nvar d = f(...a0)\nvar c = d\n}());")
389+
})
390+
o("new", async () => {
391+
await setup({
392+
"a.js": 'var b = require("./b")\nvar c = require("./c")',
393+
"b.js": "var a = 1\nmodule.exports = a",
394+
"c.js": "var a = [1, 2, 3]\nvar d = new f(...a)\nmodule.exports = d",
395+
})
396+
397+
o(await bundle(p("a.js"))).equals(";(function() {\nvar a = 1\nvar b = a\nvar a0 = [1, 2, 3]\nvar d = new f(...a0)\nvar c = d\n}());")
398+
})
399+
o("array spread", async () => {
400+
await setup({
401+
"a.js": 'var b = require("./b")\nvar c = require("./c")',
402+
"b.js": "var a = 1\nmodule.exports = a",
403+
"c.js": "var a = [1, 2, 3]\nvar arr = [...a]\nmodule.exports = arr",
404+
})
405+
406+
o(await bundle(p("a.js"))).equals(";(function() {\nvar a = 1\nvar b = a\nvar a0 = [1, 2, 3]\nvar arr = [...a0]\nvar c = arr\n}());")
407+
})
408+
o("array spread (merge)", async () => {
409+
await setup({
410+
"a.js": 'var b = require("./b")\nvar c = require("./c")',
411+
"b.js": "var a = 1\nmodule.exports = a",
412+
"c.js": "var a = [1, 2, 3]\nvar arr = [0, ...a, 4]\nmodule.exports = arr",
413+
})
414+
415+
o(await bundle(p("a.js"))).equals(";(function() {\nvar a = 1\nvar b = a\nvar a0 = [1, 2, 3]\nvar arr = [0, ...a0, 4]\nvar c = arr\n}());")
416+
})
417+
o("array destructuring", async () => {
418+
await setup({
419+
"a.js": 'var b = require("./b")\nvar c = require("./c")',
420+
"b.js": "var a = 1\nmodule.exports = a",
421+
"c.js": "var a = [1, 2, 3]\nvar d\n[d, ...a] = a\nmodule.exports = a",
422+
})
423+
424+
o(await bundle(p("a.js"))).equals(";(function() {\nvar a = 1\nvar b = a\nvar a0 = [1, 2, 3]\nvar d\n[d, ...a0] = a0\nvar c = a0\n}());")
425+
})
426+
o("object spread", async () => {
427+
await setup({
428+
"a.js": 'var b = require("./b")\nvar c = require("./c")',
429+
"b.js": "var a = 1\nmodule.exports = a",
430+
"c.js": "var a = { p: 1, q: 2, r: 3 }\nvar d = {...a}\nmodule.exports = d",
431+
})
432+
433+
o(await bundle(p("a.js"))).equals(";(function() {\nvar a = 1\nvar b = a\nvar a0 = { p: 1, q: 2, r: 3 }\nvar d = {...a0}\nvar c = d\n}());")
434+
})
435+
o("object spread (merge)", async () => {
436+
await setup({
437+
"a.js": 'var b = require("./b")\nvar c = require("./c")',
438+
"b.js": "var a = 1\nmodule.exports = a",
439+
"c.js": "var a = { p: 1, q: 2, r: 3 }\nvar d = {o:0,...a}\nmodule.exports = d",
440+
})
441+
442+
o(await bundle(p("a.js"))).equals(";(function() {\nvar a = 1\nvar b = a\nvar a0 = { p: 1, q: 2, r: 3 }\nvar d = {o:0,...a0}\nvar c = d\n}());")
443+
})
444+
o("object destructuring", async () => {
445+
await setup({
446+
"a.js": 'var b = require("./b")\nvar c = require("./c")',
447+
"b.js": "var a = 1\nmodule.exports = a",
448+
"c.js": "var obj = { p: 1, q: 2, r: 3 }\nvar p,a\n({p,...a}=obj)\nmodule.exports = a",
449+
})
450+
451+
o(await bundle(p("a.js"))).equals(";(function() {\nvar a = 1\nvar b = a\nvar obj = { p: 1, q: 2, r: 3 }\nvar p,a0\n({p,...a0}=obj)\nvar c = a0\n}());")
452+
})
453+
})
371454
})

0 commit comments

Comments
 (0)