]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
perf(codegen): optimize line / column calculation during codegen
authorEvan You <yyx990803@gmail.com>
Thu, 23 Nov 2023 16:58:47 +0000 (00:58 +0800)
committerEvan You <yyx990803@gmail.com>
Sat, 25 Nov 2023 08:18:29 +0000 (16:18 +0800)
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%.

packages/compiler-core/src/codegen.ts

index fb3040dd944c14c84311c1305a94da1d2d5b7e3e..b2f2a8017979ff6f37574972b9c6037c120aa448 100644 (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()