Skip to content

Commit cb94448

Browse files
committed
wip(compiler): improve node stringification to support adjacent nodes
1 parent c2f3ee4 commit cb94448

File tree

5 files changed

+125
-39
lines changed

5 files changed

+125
-39
lines changed

packages/compiler-core/src/ast.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,11 @@ export interface SimpleExpressionNode extends Node {
194194
content: string
195195
isStatic: boolean
196196
isConstant: boolean
197+
/**
198+
* Indicates this is an identifier for a hoist vnode call and points to the
199+
* hoisted node.
200+
*/
201+
hoisted?: JSChildNode
197202
/**
198203
* an expression parsed as the params of a function will track
199204
* the identifiers declared inside the function body.

packages/compiler-core/src/options.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ElementNode, Namespace, JSChildNode, PlainElementNode } from './ast'
1+
import { ElementNode, Namespace, TemplateChildNode } from './ast'
22
import { TextModes } from './parse'
33
import { CompilerError } from './errors'
44
import {
@@ -52,9 +52,9 @@ export interface ParserOptions {
5252
}
5353

5454
export type HoistTransform = (
55-
node: PlainElementNode,
55+
children: TemplateChildNode[],
5656
context: TransformContext
57-
) => JSChildNode
57+
) => void
5858

5959
export interface TransformOptions {
6060
/**

packages/compiler-core/src/transform.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,12 +230,14 @@ export function createTransformContext(
230230
},
231231
hoist(exp) {
232232
context.hoists.push(exp)
233-
return createSimpleExpression(
233+
const identifier = createSimpleExpression(
234234
`_hoisted_${context.hoists.length}`,
235235
false,
236236
exp.loc,
237237
true
238238
)
239+
identifier.hoisted = exp
240+
return identifier
239241
},
240242
cache(exp, isVNode = false) {
241243
return createCacheExpression(++context.cached, exp, isVNode)

packages/compiler-core/src/transforms/hoistStatic.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,7 @@ function walk(
5454
// whole tree is static
5555
;(child.codegenNode as VNodeCall).patchFlag =
5656
PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``)
57-
const hoisted = context.transformHoist
58-
? context.transformHoist(child, context)
59-
: child.codegenNode!
60-
child.codegenNode = context.hoist(hoisted)
57+
child.codegenNode = context.hoist(child.codegenNode!)
6158
continue
6259
} else {
6360
// node may contain dynamic children, but its props may be eligible for
@@ -100,6 +97,10 @@ function walk(
10097
}
10198
}
10299
}
100+
101+
if (context.transformHoist) {
102+
context.transformHoist(children, context)
103+
}
103104
}
104105

105106
export function isStaticNode(

packages/compiler-dom/src/transforms/stringifyStatic.ts

Lines changed: 109 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ import {
1010
createCallExpression,
1111
HoistTransform,
1212
CREATE_STATIC,
13-
ExpressionNode
13+
ExpressionNode,
14+
ElementTypes,
15+
PlainElementNode,
16+
JSChildNode,
17+
createSimpleExpression
1418
} from '@vue/compiler-core'
1519
import {
1620
isVoidTag,
@@ -24,41 +28,113 @@ import {
2428
stringifyStyle
2529
} from '@vue/shared'
2630

31+
export const enum StringifyThresholds {
32+
ELEMENT_WITH_BINDING_COUNT = 5,
33+
NODE_COUNT = 20
34+
}
35+
2736
// Turn eligible hoisted static trees into stringied static nodes, e.g.
2837
// const _hoisted_1 = createStaticVNode(`<div class="foo">bar</div>`)
2938
// This is only performed in non-in-browser compilations.
30-
export const stringifyStatic: HoistTransform = (node, context) => {
31-
if (shouldOptimize(node)) {
32-
return createCallExpression(context.helper(CREATE_STATIC), [
33-
JSON.stringify(stringifyElement(node, context))
34-
])
35-
} else {
36-
return node.codegenNode!
39+
export const stringifyStatic: HoistTransform = (children, context) => {
40+
let nc = 0 // current node count
41+
let ec = 0 // current element with binding count
42+
const currentEligibleNodes: PlainElementNode[] = []
43+
44+
for (let i = 0; i < children.length; i++) {
45+
const child = children[i]
46+
const hoisted = getHoistedNode(child)
47+
if (hoisted) {
48+
// presence of hoisted means child must be a plain element Node
49+
const node = child as PlainElementNode
50+
const result = analyzeNode(node)
51+
if (result) {
52+
// node is stringifiable, record state
53+
nc += result[0]
54+
ec += result[1]
55+
currentEligibleNodes.push(node)
56+
continue
57+
}
58+
}
59+
60+
// we only reach here if we ran into a node that is not stringifiable
61+
// check if currently analyzed nodes meet criteria for stringification.
62+
if (
63+
nc >= StringifyThresholds.NODE_COUNT ||
64+
ec >= StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
65+
) {
66+
// combine all currently eligible nodes into a single static vnode call
67+
const staticCall = createCallExpression(context.helper(CREATE_STATIC), [
68+
JSON.stringify(
69+
currentEligibleNodes
70+
.map(node => stringifyElement(node, context))
71+
.join('')
72+
),
73+
// the 2nd argument indicates the number of DOM nodes this static vnode
74+
// will insert / hydrate
75+
String(currentEligibleNodes.length)
76+
])
77+
// replace the first node's hoisted expression with the static vnode call
78+
replaceHoist(currentEligibleNodes[0], staticCall, context)
79+
80+
const n = currentEligibleNodes.length
81+
if (n > 1) {
82+
for (let j = 1; j < n; j++) {
83+
// for the merged nodes, set their hoisted expression to null
84+
replaceHoist(
85+
currentEligibleNodes[j],
86+
createSimpleExpression(`null`, false),
87+
context
88+
)
89+
}
90+
// also remove merged nodes from children
91+
const deleteCount = n - 1
92+
children.splice(i - n + 1, deleteCount)
93+
// adjust iteration index
94+
i -= deleteCount
95+
}
96+
}
97+
98+
// reset state
99+
nc = 0
100+
ec = 0
101+
currentEligibleNodes.length = 0
37102
}
38103
}
39104

40-
export const enum StringifyThresholds {
41-
ELEMENT_WITH_BINDING_COUNT = 5,
42-
NODE_COUNT = 20
43-
}
105+
const getHoistedNode = (node: TemplateChildNode) =>
106+
node.type === NodeTypes.ELEMENT &&
107+
node.tagType === ElementTypes.ELEMENT &&
108+
node.codegenNode &&
109+
node.codegenNode.type === NodeTypes.SIMPLE_EXPRESSION &&
110+
node.codegenNode.hoisted
44111

45112
const dataAriaRE = /^(data|aria)-/
46113
const isStringifiableAttr = (name: string) => {
47114
return isKnownAttr(name) || dataAriaRE.test(name)
48115
}
49116

50-
// Opt-in heuristics based on:
51-
// 1. number of elements with attributes > 5.
52-
// 2. OR: number of total nodes > 20
53-
// For some simple trees, the performance can actually be worse.
54-
// it is only worth it when the tree is complex enough
55-
// (e.g. big piece of static content)
56-
function shouldOptimize(node: ElementNode): boolean {
57-
let bindingThreshold = StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
58-
let nodeThreshold = StringifyThresholds.NODE_COUNT
117+
const replaceHoist = (
118+
node: PlainElementNode,
119+
replacement: JSChildNode,
120+
context: TransformContext
121+
) => {
122+
const hoistToReplace = (node.codegenNode as SimpleExpressionNode).hoisted!
123+
context.hoists[context.hoists.indexOf(hoistToReplace)] = replacement
124+
}
59125

126+
/**
127+
* for a hoisted node, analyze it and return:
128+
* - false: bailed (contains runtime constant)
129+
* - [x, y] where
130+
* - x is the number of nodes inside
131+
* - y is the number of element with bindings inside
132+
*/
133+
function analyzeNode(node: PlainElementNode): [number, number] | false {
134+
let nc = 1 // node count
135+
let ec = node.props.length > 0 ? 1 : 0 // element w/ binding count
60136
let bailed = false
61-
const bail = () => {
137+
const bail = (): false => {
62138
bailed = true
63139
return false
64140
}
@@ -67,7 +143,7 @@ function shouldOptimize(node: ElementNode): boolean {
67143
// output compared to imperative node insertions.
68144
// probably only need to check for most common case
69145
// i.e. non-phrasing-content tags inside `<p>`
70-
function walk(node: ElementNode) {
146+
function walk(node: ElementNode): boolean {
71147
for (let i = 0; i < node.props.length; i++) {
72148
const p = node.props[i]
73149
// bail on non-attr bindings
@@ -97,26 +173,28 @@ function shouldOptimize(node: ElementNode): boolean {
97173
}
98174
}
99175
for (let i = 0; i < node.children.length; i++) {
100-
if (--nodeThreshold === 0) {
176+
nc++
177+
if (nc >= StringifyThresholds.NODE_COUNT) {
101178
return true
102179
}
103180
const child = node.children[i]
104181
if (child.type === NodeTypes.ELEMENT) {
105-
if (child.props.length > 0 && --bindingThreshold === 0) {
106-
return true
107-
}
108-
if (walk(child)) {
109-
return true
182+
if (child.props.length > 0) {
183+
ec++
184+
if (ec >= StringifyThresholds.ELEMENT_WITH_BINDING_COUNT) {
185+
return true
186+
}
110187
}
188+
walk(child)
111189
if (bailed) {
112190
return false
113191
}
114192
}
115193
}
116-
return false
194+
return true
117195
}
118196

119-
return walk(node)
197+
return walk(node) ? [nc, ec] : false
120198
}
121199

122200
function stringifyElement(

0 commit comments

Comments
 (0)