perf(codegen): optimize line / column calculation during codegen

Previously, many CodegenContext.push() calls were unnecessarily
iterating through the entire pushed string to find newlines, when we
already know the newline positions for most of calls. Providing fast
paths for these calls significantly improves codegen performance when
source map is needed.

In benchmarks, this PR improves full SFC compilation performance by ~6%.
This commit is contained in:
Evan You 2023-11-24 00:58:47 +08:00
parent e8e3ec6ca7
commit 3be53d9b97
1 changed files with 87 additions and 28 deletions

View File

@ -69,6 +69,13 @@ export interface CodegenResult {
map?: RawSourceMap
}
const enum NewlineType {
Start = 0,
End = -1,
None = -2,
Unknown = -3
}
export interface CodegenContext
extends Omit<Required<CodegenOptions>, 'bindingMetadata' | 'inline'> {
source: string
@ -80,7 +87,7 @@ export interface CodegenContext
pure: boolean
map?: SourceMapGenerator
helper(key: symbol): string
push(code: string, node?: CodegenNode): void
push(code: string, newlineIndex?: number, node?: CodegenNode): void
indent(): void
deindent(withoutNewLine?: boolean): void
newline(): void
@ -127,7 +134,7 @@ function createCodegenContext(
helper(key) {
return `_${helperNameMap[key]}`
},
push(code, node) {
push(code, newlineIndex = NewlineType.None, node) {
context.code += code
if (!__BROWSER__ && context.map) {
if (node) {
@ -140,7 +147,41 @@ function createCodegenContext(
}
addMapping(node.loc.start, name)
}
advancePositionWithMutation(context, code)
if (newlineIndex === NewlineType.Unknown) {
// multiple newlines, full iteration
advancePositionWithMutation(context, code)
} else {
// fast paths
context.offset += code.length
if (newlineIndex === NewlineType.None) {
// no newlines; fast path to avoid newline detection
if (__TEST__ && code.includes('\n')) {
throw new Error(
`CodegenContext.push() called newlineIndex: none, but contains` +
`newlines: ${code.replace(/\n/g, '\\n')}`
)
}
context.column += code.length
} else {
// single newline at known index
if (newlineIndex === NewlineType.End) {
newlineIndex = code.length - 1
}
if (
__TEST__ &&
(code.charAt(newlineIndex) !== '\n' ||
code.slice(0, newlineIndex).includes('\n') ||
code.slice(newlineIndex + 1).includes('\n'))
) {
throw new Error(
`CodegenContext.push() called with newlineIndex: ${newlineIndex} ` +
`but does not conform: ${code.replace(/\n/g, '\\n')}`
)
}
context.line++
context.column = code.length - newlineIndex
}
}
if (node && node.loc !== locStub) {
addMapping(node.loc.end)
}
@ -162,7 +203,7 @@ function createCodegenContext(
}
function newline(n: number) {
context.push('\n' + ` `.repeat(n))
context.push('\n' + ` `.repeat(n), NewlineType.Start)
}
function addMapping(loc: Position, name?: string) {
@ -250,8 +291,10 @@ export function generate(
// function mode const declarations should be inside with block
// also they should be renamed to avoid collision with user properties
if (hasHelpers) {
push(`const { ${helpers.map(aliasHelper).join(', ')} } = _Vue`)
push(`\n`)
push(
`const { ${helpers.map(aliasHelper).join(', ')} } = _Vue\n`,
NewlineType.End
)
newline()
}
}
@ -282,7 +325,7 @@ export function generate(
}
}
if (ast.components.length || ast.directives.length || ast.temps) {
push(`\n`)
push(`\n`, NewlineType.Start)
newline()
}
@ -334,11 +377,14 @@ function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
const helpers = Array.from(ast.helpers)
if (helpers.length > 0) {
if (!__BROWSER__ && prefixIdentifiers) {
push(`const { ${helpers.map(aliasHelper).join(', ')} } = ${VueBinding}\n`)
push(
`const { ${helpers.map(aliasHelper).join(', ')} } = ${VueBinding}\n`,
NewlineType.End
)
} else {
// "with" mode.
// save Vue in a separate variable to avoid collision
push(`const _Vue = ${VueBinding}\n`)
push(`const _Vue = ${VueBinding}\n`, NewlineType.End)
// in "with" mode, helpers are declared inside the with block to avoid
// has check cost, but hoists are lifted out of the function - we need
// to provide the helper here.
@ -353,7 +399,7 @@ function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
.filter(helper => helpers.includes(helper))
.map(aliasHelper)
.join(', ')
push(`const { ${staticHelpers} } = _Vue\n`)
push(`const { ${staticHelpers} } = _Vue\n`, NewlineType.End)
}
}
}
@ -363,7 +409,8 @@ function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
push(
`const { ${ast.ssrHelpers
.map(aliasHelper)
.join(', ')} } = require("${ssrRuntimeModuleName}")\n`
.join(', ')} } = require("${ssrRuntimeModuleName}")\n`,
NewlineType.End
)
}
genHoists(ast.hoists, context)
@ -402,18 +449,21 @@ function genModulePreamble(
push(
`import { ${helpers
.map(s => helperNameMap[s])
.join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`
.join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`,
NewlineType.End
)
push(
`\n// Binding optimization for webpack code-split\nconst ${helpers
.map(s => `_${helperNameMap[s]} = ${helperNameMap[s]}`)
.join(', ')}\n`
.join(', ')}\n`,
NewlineType.End
)
} else {
push(
`import { ${helpers
.map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
.join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`
.join(', ')} } from ${JSON.stringify(runtimeModuleName)}\n`,
NewlineType.End
)
}
}
@ -422,7 +472,8 @@ function genModulePreamble(
push(
`import { ${ast.ssrHelpers
.map(s => `${helperNameMap[s]} as _${helperNameMap[s]}`)
.join(', ')} } from "${ssrRuntimeModuleName}"\n`
.join(', ')} } from "${ssrRuntimeModuleName}"\n`,
NewlineType.End
)
}
@ -554,7 +605,7 @@ function genNodeList(
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (isString(node)) {
push(node)
push(node, NewlineType.Unknown)
} else if (isArray(node)) {
genNodeListAsArray(node, context)
} else {
@ -573,7 +624,7 @@ function genNodeList(
function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
if (isString(node)) {
context.push(node)
context.push(node, NewlineType.Unknown)
return
}
if (isSymbol(node)) {
@ -671,12 +722,16 @@ function genText(
node: TextNode | SimpleExpressionNode,
context: CodegenContext
) {
context.push(JSON.stringify(node.content), node)
context.push(JSON.stringify(node.content), NewlineType.Unknown, node)
}
function genExpression(node: SimpleExpressionNode, context: CodegenContext) {
const { content, isStatic } = node
context.push(isStatic ? JSON.stringify(content) : content, node)
context.push(
isStatic ? JSON.stringify(content) : content,
NewlineType.Unknown,
node
)
}
function genInterpolation(node: InterpolationNode, context: CodegenContext) {
@ -694,7 +749,7 @@ function genCompoundExpression(
for (let i = 0; i < node.children!.length; i++) {
const child = node.children![i]
if (isString(child)) {
context.push(child)
context.push(child, NewlineType.Unknown)
} else {
genNode(child, context)
}
@ -715,9 +770,9 @@ function genExpressionAsPropertyKey(
const text = isSimpleIdentifier(node.content)
? node.content
: JSON.stringify(node.content)
push(text, node)
push(text, NewlineType.None, node)
} else {
push(`[${node.content}]`, node)
push(`[${node.content}]`, NewlineType.Unknown, node)
}
}
@ -726,7 +781,11 @@ function genComment(node: CommentNode, context: CodegenContext) {
if (pure) {
push(PURE_ANNOTATION)
}
push(`${helper(CREATE_COMMENT)}(${JSON.stringify(node.content)})`, node)
push(
`${helper(CREATE_COMMENT)}(${JSON.stringify(node.content)})`,
NewlineType.Unknown,
node
)
}
function genVNodeCall(node: VNodeCall, context: CodegenContext) {
@ -754,7 +813,7 @@ function genVNodeCall(node: VNodeCall, context: CodegenContext) {
const callHelper: symbol = isBlock
? getVNodeBlockHelper(context.inSSR, isComponent)
: getVNodeHelper(context.inSSR, isComponent)
push(helper(callHelper) + `(`, node)
push(helper(callHelper) + `(`, NewlineType.None, node)
genNodeList(
genNullableArgs([tag, props, children, patchFlag, dynamicProps]),
context
@ -785,7 +844,7 @@ function genCallExpression(node: CallExpression, context: CodegenContext) {
if (pure) {
push(PURE_ANNOTATION)
}
push(callee + `(`, node)
push(callee + `(`, NewlineType.None, node)
genNodeList(node.arguments, context)
push(`)`)
}
@ -794,7 +853,7 @@ function genObjectExpression(node: ObjectExpression, context: CodegenContext) {
const { push, indent, deindent, newline } = context
const { properties } = node
if (!properties.length) {
push(`{}`, node)
push(`{}`, NewlineType.None, node)
return
}
const multilines =
@ -834,7 +893,7 @@ function genFunctionExpression(
// wrap slot functions with owner context
push(`_${helperNameMap[WITH_CTX]}(`)
}
push(`(`, node)
push(`(`, NewlineType.None, node)
if (isArray(params)) {
genNodeList(params, context)
} else if (params) {
@ -934,7 +993,7 @@ function genTemplateLiteral(node: TemplateLiteral, context: CodegenContext) {
for (let i = 0; i < l; i++) {
const e = node.elements[i]
if (isString(e)) {
push(e.replace(/(`|\$|\\)/g, '\\$1'))
push(e.replace(/(`|\$|\\)/g, '\\$1'), NewlineType.Unknown)
} else {
push('${')
if (multilines) indent()