Skip to content

Commit 27913e6

Browse files
committed
feat(compiler-dom/runtime-dom): stringify eligible static trees
1 parent e861c6d commit 27913e6

File tree

13 files changed

+301
-84
lines changed

13 files changed

+301
-84
lines changed

packages/compiler-core/src/codegen.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ import {
4848
WITH_SCOPE_ID,
4949
WITH_DIRECTIVES,
5050
CREATE_BLOCK,
51-
OPEN_BLOCK
51+
OPEN_BLOCK,
52+
CREATE_STATIC
5253
} from './runtimeHelpers'
5354
import { ImportItem } from './transform'
5455

@@ -309,7 +310,12 @@ function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
309310
// has check cost, but hoists are lifted out of the function - we need
310311
// to provide the helper here.
311312
if (ast.hoists.length) {
312-
const staticHelpers = [CREATE_VNODE, CREATE_COMMENT, CREATE_TEXT]
313+
const staticHelpers = [
314+
CREATE_VNODE,
315+
CREATE_COMMENT,
316+
CREATE_TEXT,
317+
CREATE_STATIC
318+
]
313319
.filter(helper => ast.helpers.includes(helper))
314320
.map(aliasHelper)
315321
.join(', ')

packages/compiler-core/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ export {
55
CompilerOptions,
66
ParserOptions,
77
TransformOptions,
8-
CodegenOptions
8+
CodegenOptions,
9+
HoistTransform
910
} from './options'
1011
export { baseParse, TextModes } from './parse'
1112
export {

packages/compiler-core/src/options.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1-
import { ElementNode, Namespace } from './ast'
1+
import { ElementNode, Namespace, JSChildNode, PlainElementNode } from './ast'
22
import { TextModes } from './parse'
33
import { CompilerError } from './errors'
4-
import { NodeTransform, DirectiveTransform } from './transform'
4+
import {
5+
NodeTransform,
6+
DirectiveTransform,
7+
TransformContext
8+
} from './transform'
59

610
export interface ParserOptions {
711
isVoidTag?: (tag: string) => boolean // e.g. img, br, hr
@@ -26,9 +30,17 @@ export interface ParserOptions {
2630
onError?: (error: CompilerError) => void
2731
}
2832

33+
export type HoistTransform = (
34+
node: PlainElementNode,
35+
context: TransformContext
36+
) => JSChildNode
37+
2938
export interface TransformOptions {
3039
nodeTransforms?: NodeTransform[]
3140
directiveTransforms?: Record<string, DirectiveTransform | undefined>
41+
// an optional hook to transform a node being hoisted.
42+
// used by compiler-dom to turn hoisted nodes into stringified HTML vnodes.
43+
transformHoist?: HoistTransform | null
3244
isBuiltInComponent?: (tag: string) => symbol | void
3345
// Transform expressions like {{ foo }} to `_ctx.foo`.
3446
// If this option is false, the generated code will be wrapped in a

packages/compiler-core/src/runtimeHelpers.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const CREATE_BLOCK = Symbol(__DEV__ ? `createBlock` : ``)
88
export const CREATE_VNODE = Symbol(__DEV__ ? `createVNode` : ``)
99
export const CREATE_COMMENT = Symbol(__DEV__ ? `createCommentVNode` : ``)
1010
export const CREATE_TEXT = Symbol(__DEV__ ? `createTextVNode` : ``)
11+
export const CREATE_STATIC = Symbol(__DEV__ ? `createStaticVNode` : ``)
1112
export const RESOLVE_COMPONENT = Symbol(__DEV__ ? `resolveComponent` : ``)
1213
export const RESOLVE_DYNAMIC_COMPONENT = Symbol(
1314
__DEV__ ? `resolveDynamicComponent` : ``
@@ -40,6 +41,7 @@ export const helperNameMap: any = {
4041
[CREATE_VNODE]: `createVNode`,
4142
[CREATE_COMMENT]: `createCommentVNode`,
4243
[CREATE_TEXT]: `createTextVNode`,
44+
[CREATE_STATIC]: `createStaticVNode`,
4345
[RESOLVE_COMPONENT]: `resolveComponent`,
4446
[RESOLVE_DYNAMIC_COMPONENT]: `resolveDynamicComponent`,
4547
[RESOLVE_DIRECTIVE]: `resolveDirective`,

packages/compiler-core/src/transform.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ export function createTransformContext(
115115
cacheHandlers = false,
116116
nodeTransforms = [],
117117
directiveTransforms = {},
118+
transformHoist = null,
118119
isBuiltInComponent = NOOP,
119120
scopeId = null,
120121
ssr = false,
@@ -128,6 +129,7 @@ export function createTransformContext(
128129
cacheHandlers,
129130
nodeTransforms,
130131
directiveTransforms,
132+
transformHoist,
131133
isBuiltInComponent,
132134
scopeId,
133135
ssr,

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ function walk(
5252
) {
5353
if (!doNotHoistNode && isStaticNode(child, resultCache)) {
5454
// whole tree is static
55-
child.codegenNode = context.hoist(child.codegenNode!)
55+
const hoisted = context.transformHoist
56+
? context.transformHoist(child, context)
57+
: child.codegenNode!
58+
child.codegenNode = context.hoist(hoisted)
5659
continue
5760
} else {
5861
// node may contain dynamic children, but its props may be eligible for

packages/compiler-dom/src/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { transformModel } from './transforms/vModel'
1818
import { transformOn } from './transforms/vOn'
1919
import { transformShow } from './transforms/vShow'
2020
import { warnTransitionChildren } from './transforms/warnTransitionChildren'
21+
import { stringifyStatic } from './stringifyStatic'
2122

2223
export const parserOptions = __BROWSER__
2324
? parserOptionsMinimal
@@ -41,17 +42,16 @@ export function compile(
4142
template: string,
4243
options: CompilerOptions = {}
4344
): CodegenResult {
44-
const result = baseCompile(template, {
45+
return baseCompile(template, {
4546
...parserOptions,
4647
...options,
4748
nodeTransforms: [...DOMNodeTransforms, ...(options.nodeTransforms || [])],
4849
directiveTransforms: {
4950
...DOMDirectiveTransforms,
5051
...(options.directiveTransforms || {})
51-
}
52+
},
53+
transformHoist: __BROWSER__ ? null : stringifyStatic
5254
})
53-
// debugger
54-
return result
5555
}
5656

5757
export function parse(template: string, options: ParserOptions = {}): RootNode {
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import {
2+
NodeTypes,
3+
ElementNode,
4+
TransformContext,
5+
TemplateChildNode,
6+
SimpleExpressionNode,
7+
createCallExpression,
8+
HoistTransform,
9+
CREATE_STATIC
10+
} from '@vue/compiler-core'
11+
import { isVoidTag, isString, isSymbol, escapeHtml } from '@vue/shared'
12+
13+
// Turn eligible hoisted static trees into stringied static nodes, e.g.
14+
// const _hoisted_1 = createStaticVNode(`<div class="foo">bar</div>`)
15+
export const stringifyStatic: HoistTransform = (node, context) => {
16+
if (shouldOptimize(node)) {
17+
return createCallExpression(context.helper(CREATE_STATIC), [
18+
JSON.stringify(stringifyElement(node, context))
19+
])
20+
} else {
21+
return node.codegenNode!
22+
}
23+
}
24+
25+
// Opt-in heuristics based on:
26+
// 1. number of elements with attributes > 5.
27+
// 2. OR: number of total nodes > 20
28+
// For some simple trees, the performance can actually be worse.
29+
// it is only worth it when the tree is complex enough
30+
// (e.g. big piece of static content)
31+
function shouldOptimize(node: ElementNode): boolean {
32+
let bindingThreshold = 5
33+
let nodeThreshold = 20
34+
35+
function walk(node: ElementNode) {
36+
for (let i = 0; i < node.children.length; i++) {
37+
if (--nodeThreshold === 0) {
38+
return true
39+
}
40+
const child = node.children[i]
41+
if (child.type === NodeTypes.ELEMENT) {
42+
if (child.props.length > 0 && --bindingThreshold === 0) {
43+
return true
44+
}
45+
if (walk(child)) {
46+
return true
47+
}
48+
}
49+
}
50+
return false
51+
}
52+
53+
return walk(node)
54+
}
55+
56+
function stringifyElement(
57+
node: ElementNode,
58+
context: TransformContext
59+
): string {
60+
let res = `<${node.tag}`
61+
for (let i = 0; i < node.props.length; i++) {
62+
const p = node.props[i]
63+
if (p.type === NodeTypes.ATTRIBUTE) {
64+
res += ` ${p.name}`
65+
if (p.value) {
66+
res += `="${p.value.content}"`
67+
}
68+
} else if (p.type === NodeTypes.DIRECTIVE && p.name === 'bind') {
69+
// constant v-bind, e.g. :foo="1"
70+
// TODO
71+
}
72+
}
73+
if (context.scopeId) {
74+
res += ` ${context.scopeId}`
75+
}
76+
res += `>`
77+
for (let i = 0; i < node.children.length; i++) {
78+
res += stringifyNode(node.children[i], context)
79+
}
80+
if (!isVoidTag(node.tag)) {
81+
res += `</${node.tag}>`
82+
}
83+
return res
84+
}
85+
86+
function stringifyNode(
87+
node: string | TemplateChildNode,
88+
context: TransformContext
89+
): string {
90+
if (isString(node)) {
91+
return node
92+
}
93+
if (isSymbol(node)) {
94+
return ``
95+
}
96+
switch (node.type) {
97+
case NodeTypes.ELEMENT:
98+
return stringifyElement(node, context)
99+
case NodeTypes.TEXT:
100+
return escapeHtml(node.content)
101+
case NodeTypes.COMMENT:
102+
return `<!--${escapeHtml(node.content)}-->`
103+
case NodeTypes.INTERPOLATION:
104+
// constants
105+
// TODO check eval
106+
return (node.content as SimpleExpressionNode).content
107+
case NodeTypes.COMPOUND_EXPRESSION:
108+
// TODO proper handling
109+
return node.children.map((c: any) => stringifyNode(c, context)).join('')
110+
case NodeTypes.TEXT_CALL:
111+
return stringifyNode(node.content, context)
112+
default:
113+
// static trees will not contain if/for nodes
114+
return ''
115+
}
116+
}

packages/runtime-core/src/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,12 @@ export { toHandlers } from './helpers/toHandlers'
8585
export { renderSlot } from './helpers/renderSlot'
8686
export { createSlots } from './helpers/createSlots'
8787
export { pushScopeId, popScopeId, withScopeId } from './helpers/scopeId'
88-
export { setBlockTracking, createTextVNode, createCommentVNode } from './vnode'
88+
export {
89+
setBlockTracking,
90+
createTextVNode,
91+
createCommentVNode,
92+
createStaticVNode
93+
} from './vnode'
8994
// Since @vue/shared is inlined into final builds,
9095
// when re-exporting from @vue/shared we need to avoid relying on their original
9196
// types so that the bundled d.ts does not attempt to import from it.

0 commit comments

Comments
 (0)