Skip to content

Commit 4de5d24

Browse files
committedJun 21, 2021
perf: improve static content insertion perf
Especially on multiple insertions of the same static node. fix #3090
1 parent be1e42e commit 4de5d24

File tree

3 files changed

+151
-21
lines changed

3 files changed

+151
-21
lines changed
 

‎packages/runtime-core/src/renderer.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@ export interface RendererOptions<
141141
content: string,
142142
parent: HostElement,
143143
anchor: HostNode | null,
144-
isSVG: boolean
144+
isSVG: boolean,
145+
cached?: [HostNode, HostNode | null] | null
145146
): HostElement[]
146147
}
147148

@@ -635,7 +636,11 @@ function baseCreateRenderer(
635636
n2.children as string,
636637
container,
637638
anchor,
638-
isSVG
639+
isSVG,
640+
// pass cached nodes if the static node is being mounted multiple times
641+
// so that runtime-dom can simply cloneNode() instead of inserting new
642+
// HTML
643+
n2.el && [n2.el, n2.anchor]
639644
)
640645
}
641646

‎packages/runtime-dom/__tests__/nodeOps.spec.ts

+98-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { nodeOps } from '../src/nodeOps'
1+
import { nodeOps, svgNS } from '../src/nodeOps'
22

33
describe('runtime-dom: node-ops', () => {
44
test('the _value property should be cloned', () => {
@@ -25,4 +25,101 @@ describe('runtime-dom: node-ops', () => {
2525
expect(option1.selected).toBe(true)
2626
expect(option2.selected).toBe(true)
2727
})
28+
29+
describe('insertStaticContent', () => {
30+
test('fresh insertion', () => {
31+
const content = `<div>one</div><div>two</div>three`
32+
const parent = document.createElement('div')
33+
const [first, last] = nodeOps.insertStaticContent!(
34+
content,
35+
parent,
36+
null,
37+
false
38+
)
39+
expect(parent.innerHTML).toBe(content)
40+
expect(first).toBe(parent.firstChild)
41+
expect(last).toBe(parent.lastChild)
42+
})
43+
44+
test('fresh insertion with anchor', () => {
45+
const content = `<div>one</div><div>two</div>three`
46+
const existing = `<div>existing</div>`
47+
const parent = document.createElement('div')
48+
parent.innerHTML = existing
49+
const anchor = parent.firstChild
50+
const [first, last] = nodeOps.insertStaticContent!(
51+
content,
52+
parent,
53+
anchor,
54+
false
55+
)
56+
expect(parent.innerHTML).toBe(content + existing)
57+
expect(first).toBe(parent.firstChild)
58+
expect(last).toBe(parent.childNodes[parent.childNodes.length - 2])
59+
})
60+
61+
test('fresh insertion as svg', () => {
62+
const content = `<text>hello</text><circle cx="100" cy="100" r="80"></circle>`
63+
const parent = document.createElementNS(svgNS, 'svg')
64+
const [first, last] = nodeOps.insertStaticContent!(
65+
content,
66+
parent,
67+
null,
68+
true
69+
)
70+
expect(parent.innerHTML).toBe(content)
71+
expect(first).toBe(parent.firstChild)
72+
expect(last).toBe(parent.lastChild)
73+
expect(first.namespaceURI).toMatch('svg')
74+
expect(last.namespaceURI).toMatch('svg')
75+
})
76+
77+
test('fresh insertion as svg, with anchor', () => {
78+
const content = `<text>hello</text><circle cx="100" cy="100" r="80"></circle>`
79+
const existing = `<path></path>`
80+
const parent = document.createElementNS(svgNS, 'svg')
81+
parent.innerHTML = existing
82+
const anchor = parent.firstChild
83+
const [first, last] = nodeOps.insertStaticContent!(
84+
content,
85+
parent,
86+
anchor,
87+
true
88+
)
89+
expect(parent.innerHTML).toBe(content + existing)
90+
expect(first).toBe(parent.firstChild)
91+
expect(last).toBe(parent.childNodes[parent.childNodes.length - 2])
92+
expect(first.namespaceURI).toMatch('svg')
93+
expect(last.namespaceURI).toMatch('svg')
94+
})
95+
96+
test('cached', () => {
97+
const content = `<div>one</div><div>two</div>three`
98+
99+
const cacheParent = document.createElement('div')
100+
const [cachedFirst, cachedLast] = nodeOps.insertStaticContent!(
101+
content,
102+
cacheParent,
103+
null,
104+
false
105+
)
106+
107+
const parent = document.createElement('div')
108+
109+
const [first, last] = nodeOps.insertStaticContent!(
110+
``,
111+
parent,
112+
null,
113+
false,
114+
[cachedFirst, cachedLast]
115+
)
116+
117+
expect(parent.innerHTML).toBe(content)
118+
expect(first).toBe(parent.firstChild)
119+
expect(last).toBe(parent.lastChild)
120+
121+
expect(first).not.toBe(cachedFirst)
122+
expect(last).not.toBe(cachedLast)
123+
})
124+
})
28125
})

‎packages/runtime-dom/src/nodeOps.ts

+46-18
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@ export const svgNS = 'http://www.w3.org/2000/svg'
44

55
const doc = (typeof document !== 'undefined' ? document : null) as Document
66

7-
let tempContainer: HTMLElement
8-
let tempSVGContainer: SVGElement
9-
107
export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
118
insert: (child, parent, anchor) => {
129
parent.insertBefore(child, anchor || null)
@@ -71,23 +68,54 @@ export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
7168
},
7269

7370
// __UNSAFE__
74-
// Reason: innerHTML.
71+
// Reason: insertAdjacentHTML.
7572
// Static content here can only come from compiled templates.
7673
// As long as the user only uses trusted templates, this is safe.
77-
insertStaticContent(content, parent, anchor, isSVG) {
78-
const temp = isSVG
79-
? tempSVGContainer ||
80-
(tempSVGContainer = doc.createElementNS(svgNS, 'svg'))
81-
: tempContainer || (tempContainer = doc.createElement('div'))
82-
temp.innerHTML = content
83-
const first = temp.firstChild as Element
84-
let node: Element | null = first
85-
let last: Element = node
86-
while (node) {
87-
last = node
88-
nodeOps.insert(node, parent, anchor)
89-
node = temp.firstChild as Element
74+
insertStaticContent(content, parent, anchor, isSVG, cached) {
75+
if (cached) {
76+
let [cachedFirst, cachedLast] = cached
77+
let first, last
78+
while (true) {
79+
let node = cachedFirst.cloneNode(true)
80+
if (!first) first = node
81+
parent.insertBefore(node, anchor)
82+
if (cachedFirst === cachedLast) {
83+
last = node
84+
break
85+
}
86+
cachedFirst = cachedFirst.nextSibling!
87+
}
88+
return [first, last] as any
89+
}
90+
91+
// <parent> before | first ... last | anchor </parent>
92+
const before = anchor ? anchor.previousSibling : parent.lastChild
93+
if (anchor) {
94+
let insertionPoint
95+
let usingTempInsertionPoint = false
96+
if (anchor instanceof Element) {
97+
insertionPoint = anchor
98+
} else {
99+
// insertAdjacentHTML only works for elements but the anchor is not an
100+
// element...
101+
usingTempInsertionPoint = true
102+
insertionPoint = isSVG
103+
? doc.createElementNS(svgNS, 'g')
104+
: doc.createElement('div')
105+
parent.insertBefore(insertionPoint, anchor)
106+
}
107+
insertionPoint.insertAdjacentHTML('beforebegin', content)
108+
if (usingTempInsertionPoint) {
109+
parent.removeChild(insertionPoint)
110+
}
111+
} else {
112+
parent.insertAdjacentHTML('beforeend', content)
90113
}
91-
return [first, last]
114+
return [
115+
// first
116+
before ? before.nextSibling : parent.firstChild,
117+
// last
118+
anchor ? anchor.previousSibling : parent.lastChild
119+
]
92120
}
93121
}

0 commit comments

Comments
 (0)
Please sign in to comment.