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