]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
perf: also hoist all-static children array
authorEvan You <yyx990803@gmail.com>
Thu, 8 Jul 2021 20:12:04 +0000 (16:12 -0400)
committerEvan You <yyx990803@gmail.com>
Fri, 16 Jul 2021 18:30:49 +0000 (14:30 -0400)
packages/compiler-core/__tests__/transforms/__snapshots__/hoistStatic.spec.ts.snap
packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts
packages/compiler-core/src/ast.ts
packages/compiler-core/src/codegen.ts
packages/compiler-core/src/transform.ts
packages/compiler-core/src/transforms/hoistStatic.ts
packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts

index 32a6fd611ee0a6a04811ca73b64542c4c958eccb..4ee25d5ac2d74bbc52781ce556317dbb137d8433 100644 (file)
@@ -5,14 +5,15 @@ exports[`compiler: hoistStatic transform hoist element with static key 1`] = `
 const { createElementVNode: _createElementVNode } = _Vue
 
 const _hoisted_1 = /*#__PURE__*/_createElementVNode(\\"div\\", { key: \\"foo\\" }, null, -1 /* HOISTED */)
+const _hoisted_2 = [
+  _hoisted_1
+]
 
 return function render(_ctx, _cache) {
   with (_ctx) {
     const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
 
-    return (_openBlock(), _createElementBlock(\\"div\\", null, [
-      _hoisted_1
-    ]))
+    return (_openBlock(), _createElementBlock(\\"div\\", null, _hoisted_2))
   }
 }"
 `;
@@ -25,14 +26,15 @@ const _hoisted_1 = /*#__PURE__*/_createElementVNode(\\"p\\", null, [
   /*#__PURE__*/_createElementVNode(\\"span\\"),
   /*#__PURE__*/_createElementVNode(\\"span\\")
 ], -1 /* HOISTED */)
+const _hoisted_2 = [
+  _hoisted_1
+]
 
 return function render(_ctx, _cache) {
   with (_ctx) {
     const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
 
-    return (_openBlock(), _createElementBlock(\\"div\\", null, [
-      _hoisted_1
-    ]))
+    return (_openBlock(), _createElementBlock(\\"div\\", null, _hoisted_2))
   }
 }"
 `;
@@ -44,14 +46,15 @@ const { createElementVNode: _createElementVNode, createCommentVNode: _createComm
 const _hoisted_1 = /*#__PURE__*/_createElementVNode(\\"div\\", null, [
   /*#__PURE__*/_createCommentVNode(\\"comment\\")
 ], -1 /* HOISTED */)
+const _hoisted_2 = [
+  _hoisted_1
+]
 
 return function render(_ctx, _cache) {
   with (_ctx) {
     const { createCommentVNode: _createCommentVNode, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
 
-    return (_openBlock(), _createElementBlock(\\"div\\", null, [
-      _hoisted_1
-    ]))
+    return (_openBlock(), _createElementBlock(\\"div\\", null, _hoisted_2))
   }
 }"
 `;
@@ -62,15 +65,16 @@ const { createElementVNode: _createElementVNode } = _Vue
 
 const _hoisted_1 = /*#__PURE__*/_createElementVNode(\\"span\\", null, null, -1 /* HOISTED */)
 const _hoisted_2 = /*#__PURE__*/_createElementVNode(\\"div\\", null, null, -1 /* HOISTED */)
+const _hoisted_3 = [
+  _hoisted_1,
+  _hoisted_2
+]
 
 return function render(_ctx, _cache) {
   with (_ctx) {
     const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
 
-    return (_openBlock(), _createElementBlock(\\"div\\", null, [
-      _hoisted_1,
-      _hoisted_2
-    ]))
+    return (_openBlock(), _createElementBlock(\\"div\\", null, _hoisted_3))
   }
 }"
 `;
@@ -80,14 +84,15 @@ exports[`compiler: hoistStatic transform hoist simple element 1`] = `
 const { createElementVNode: _createElementVNode } = _Vue
 
 const _hoisted_1 = /*#__PURE__*/_createElementVNode(\\"span\\", { class: \\"inline\\" }, \\"hello\\", -1 /* HOISTED */)
+const _hoisted_2 = [
+  _hoisted_1
+]
 
 return function render(_ctx, _cache) {
   with (_ctx) {
     const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
 
-    return (_openBlock(), _createElementBlock(\\"div\\", null, [
-      _hoisted_1
-    ]))
+    return (_openBlock(), _createElementBlock(\\"div\\", null, _hoisted_2))
   }
 }"
 `;
@@ -175,14 +180,15 @@ exports[`compiler: hoistStatic transform prefixIdentifiers hoist nested static t
 const { createElementVNode: _createElementVNode } = _Vue
 
 const _hoisted_1 = /*#__PURE__*/_createElementVNode(\\"span\\", null, \\"foo \\" + /*#__PURE__*/_toDisplayString(1) + \\" \\" + /*#__PURE__*/_toDisplayString(true), -1 /* HOISTED */)
+const _hoisted_2 = [
+  _hoisted_1
+]
 
 return function render(_ctx, _cache) {
   with (_ctx) {
     const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
 
-    return (_openBlock(), _createElementBlock(\\"div\\", null, [
-      _hoisted_1
-    ]))
+    return (_openBlock(), _createElementBlock(\\"div\\", null, _hoisted_2))
   }
 }"
 `;
@@ -192,14 +198,15 @@ exports[`compiler: hoistStatic transform prefixIdentifiers hoist nested static t
 const { createElementVNode: _createElementVNode } = _Vue
 
 const _hoisted_1 = /*#__PURE__*/_createElementVNode(\\"span\\", { foo: 0 }, /*#__PURE__*/_toDisplayString(1), -1 /* HOISTED */)
+const _hoisted_2 = [
+  _hoisted_1
+]
 
 return function render(_ctx, _cache) {
   with (_ctx) {
     const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
 
-    return (_openBlock(), _createElementBlock(\\"div\\", null, [
-      _hoisted_1
-    ]))
+    return (_openBlock(), _createElementBlock(\\"div\\", null, _hoisted_2))
   }
 }"
 `;
@@ -368,6 +375,9 @@ const { createElementVNode: _createElementVNode } = _Vue
 
 const _hoisted_1 = { id: \\"foo\\" }
 const _hoisted_2 = /*#__PURE__*/_createElementVNode(\\"span\\", null, null, -1 /* HOISTED */)
+const _hoisted_3 = [
+  _hoisted_2
+]
 
 return function render(_ctx, _cache) {
   with (_ctx) {
@@ -375,9 +385,7 @@ return function render(_ctx, _cache) {
 
     return (_openBlock(), _createElementBlock(\\"div\\", null, [
       (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(list, (i) => {
-        return (_openBlock(), _createElementBlock(\\"div\\", _hoisted_1, [
-          _hoisted_2
-        ]))
+        return (_openBlock(), _createElementBlock(\\"div\\", _hoisted_1, _hoisted_3))
       }), 256 /* UNKEYED_FRAGMENT */))
     ]))
   }
@@ -393,6 +401,9 @@ const _hoisted_1 = {
   id: \\"foo\\"
 }
 const _hoisted_2 = /*#__PURE__*/_createElementVNode(\\"span\\", null, null, -1 /* HOISTED */)
+const _hoisted_3 = [
+  _hoisted_2
+]
 
 return function render(_ctx, _cache) {
   with (_ctx) {
@@ -400,9 +411,7 @@ return function render(_ctx, _cache) {
 
     return (_openBlock(), _createElementBlock(\\"div\\", null, [
       ok
-        ? (_openBlock(), _createElementBlock(\\"div\\", _hoisted_1, [
-            _hoisted_2
-          ]))
+        ? (_openBlock(), _createElementBlock(\\"div\\", _hoisted_1, _hoisted_3))
         : _createCommentVNode(\\"v-if\\", true)
     ]))
   }
index b2e125afdf5c4e1eb54930362f8ff620bc4bf0ae..04363431b9d0fa5ee2e6291d1e1671dcd3dfbb9f 100644 (file)
@@ -26,6 +26,17 @@ import { createObjectMatcher, genFlagText } from '../testUtils'
 import { transformText } from '../../src/transforms/transformText'
 import { PatchFlags } from '@vue/shared'
 
+const hoistedChildrenArrayMatcher = (startIndex = 1, length = 1) => ({
+  type: NodeTypes.JS_ARRAY_EXPRESSION,
+  elements: new Array(length).fill(0).map((_, i) => ({
+    type: NodeTypes.ELEMENT,
+    codegenNode: {
+      type: NodeTypes.SIMPLE_EXPRESSION,
+      content: `_hoisted_${startIndex + i}`
+    }
+  }))
+})
+
 function transformWithHoist(template: string, options: CompilerOptions = {}) {
   const ast = parse(template)
   transform(ast, {
@@ -75,20 +86,13 @@ describe('compiler: hoistStatic transform', () => {
           type: NodeTypes.TEXT,
           content: `hello`
         }
-      }
+      },
+      hoistedChildrenArrayMatcher()
     ])
     expect(root.codegenNode).toMatchObject({
       tag: `"div"`,
       props: undefined,
-      children: [
-        {
-          type: NodeTypes.ELEMENT,
-          codegenNode: {
-            type: NodeTypes.SIMPLE_EXPRESSION,
-            content: `_hoisted_1`
-          }
-        }
-      ]
+      children: { content: `_hoisted_2` }
     })
     expect(generate(root).code).toMatchSnapshot()
   })
@@ -104,17 +108,12 @@ describe('compiler: hoistStatic transform', () => {
           { type: NodeTypes.ELEMENT, tag: `span` },
           { type: NodeTypes.ELEMENT, tag: `span` }
         ]
-      }
-    ])
-    expect((root.codegenNode as VNodeCall).children).toMatchObject([
-      {
-        type: NodeTypes.ELEMENT,
-        codegenNode: {
-          type: NodeTypes.SIMPLE_EXPRESSION,
-          content: `_hoisted_1`
-        }
-      }
+      },
+      hoistedChildrenArrayMatcher()
     ])
+    expect((root.codegenNode as VNodeCall).children).toMatchObject({
+      content: '_hoisted_2'
+    })
     expect(generate(root).code).toMatchSnapshot()
   })
 
@@ -126,17 +125,12 @@ describe('compiler: hoistStatic transform', () => {
         tag: `"div"`,
         props: undefined,
         children: [{ type: NodeTypes.COMMENT, content: `comment` }]
-      }
-    ])
-    expect((root.codegenNode as VNodeCall).children).toMatchObject([
-      {
-        type: NodeTypes.ELEMENT,
-        codegenNode: {
-          type: NodeTypes.SIMPLE_EXPRESSION,
-          content: `_hoisted_1`
-        }
-      }
+      },
+      hoistedChildrenArrayMatcher()
     ])
+    expect((root.codegenNode as VNodeCall).children).toMatchObject({
+      content: `_hoisted_2`
+    })
     expect(generate(root).code).toMatchSnapshot()
   })
 
@@ -150,24 +144,12 @@ describe('compiler: hoistStatic transform', () => {
       {
         type: NodeTypes.VNODE_CALL,
         tag: `"div"`
-      }
-    ])
-    expect((root.codegenNode as VNodeCall).children).toMatchObject([
-      {
-        type: NodeTypes.ELEMENT,
-        codegenNode: {
-          type: NodeTypes.SIMPLE_EXPRESSION,
-          content: `_hoisted_1`
-        }
       },
-      {
-        type: NodeTypes.ELEMENT,
-        codegenNode: {
-          type: NodeTypes.SIMPLE_EXPRESSION,
-          content: `_hoisted_2`
-        }
-      }
+      hoistedChildrenArrayMatcher(1, 2)
     ])
+    expect((root.codegenNode as VNodeCall).children).toMatchObject({
+      content: '_hoisted_3'
+    })
     expect(generate(root).code).toMatchSnapshot()
   })
 
@@ -213,26 +195,19 @@ describe('compiler: hoistStatic transform', () => {
 
   test('hoist element with static key', () => {
     const root = transformWithHoist(`<div><div key="foo"/></div>`)
-    expect(root.hoists.length).toBe(1)
+    expect(root.hoists.length).toBe(2)
     expect(root.hoists).toMatchObject([
       {
         type: NodeTypes.VNODE_CALL,
         tag: `"div"`,
         props: createObjectMatcher({ key: 'foo' })
-      }
+      },
+      hoistedChildrenArrayMatcher()
     ])
     expect(root.codegenNode).toMatchObject({
       tag: `"div"`,
       props: undefined,
-      children: [
-        {
-          type: NodeTypes.ELEMENT,
-          codegenNode: {
-            type: NodeTypes.SIMPLE_EXPRESSION,
-            content: `_hoisted_1`
-          }
-        }
-      ]
+      children: { content: `_hoisted_2` }
     })
     expect(generate(root).code).toMatchSnapshot()
   })
@@ -348,7 +323,8 @@ describe('compiler: hoistStatic transform', () => {
       {
         type: NodeTypes.VNODE_CALL,
         tag: `"span"`
-      }
+      },
+      hoistedChildrenArrayMatcher(2)
     ])
     expect(
       ((root.children[0] as ElementNode).children[0] as IfNode).codegenNode
@@ -359,11 +335,7 @@ describe('compiler: hoistStatic transform', () => {
         type: NodeTypes.VNODE_CALL,
         tag: `"div"`,
         props: { content: `_hoisted_1` },
-        children: [
-          {
-            codegenNode: { content: `_hoisted_2` }
-          }
-        ]
+        children: { content: `_hoisted_3` }
       }
     })
     expect(generate(root).code).toMatchSnapshot()
@@ -380,7 +352,8 @@ describe('compiler: hoistStatic transform', () => {
       {
         type: NodeTypes.VNODE_CALL,
         tag: `"span"`
-      }
+      },
+      hoistedChildrenArrayMatcher(2)
     ])
     const forBlockCodegen = ((root.children[0] as ElementNode)
       .children[0] as ForNode).codegenNode
@@ -399,11 +372,7 @@ describe('compiler: hoistStatic transform', () => {
       type: NodeTypes.VNODE_CALL,
       tag: `"div"`,
       props: { content: `_hoisted_1` },
-      children: [
-        {
-          codegenNode: { content: `_hoisted_2` }
-        }
-      ]
+      children: { content: `_hoisted_3` }
     })
     expect(generate(root).code).toMatchSnapshot()
   })
@@ -423,6 +392,17 @@ describe('compiler: hoistStatic transform', () => {
       {
         type: NodeTypes.VNODE_CALL,
         tag: `"div"`
+      },
+      {
+        type: NodeTypes.JS_ARRAY_EXPRESSION,
+        elements: [
+          {
+            type: NodeTypes.TEXT_CALL
+          },
+          {
+            type: NodeTypes.ELEMENT
+          }
+        ]
       }
     ])
   })
@@ -443,20 +423,16 @@ describe('compiler: hoistStatic transform', () => {
           children: {
             type: NodeTypes.COMPOUND_EXPRESSION
           }
-        }
+        },
+        hoistedChildrenArrayMatcher()
       ])
       expect(root.codegenNode).toMatchObject({
         tag: `"div"`,
         props: undefined,
-        children: [
-          {
-            type: NodeTypes.ELEMENT,
-            codegenNode: {
-              type: NodeTypes.SIMPLE_EXPRESSION,
-              content: `_hoisted_1`
-            }
-          }
-        ]
+        children: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: `_hoisted_2`
+        }
       })
       expect(generate(root).code).toMatchSnapshot()
     })
@@ -482,20 +458,16 @@ describe('compiler: hoistStatic transform', () => {
               constType: ConstantTypes.CAN_STRINGIFY
             }
           }
-        }
+        },
+        hoistedChildrenArrayMatcher()
       ])
       expect(root.codegenNode).toMatchObject({
         tag: `"div"`,
         props: undefined,
-        children: [
-          {
-            type: NodeTypes.ELEMENT,
-            codegenNode: {
-              type: NodeTypes.SIMPLE_EXPRESSION,
-              content: `_hoisted_1`
-            }
-          }
-        ]
+        children: {
+          type: NodeTypes.SIMPLE_EXPRESSION,
+          content: `_hoisted_2`
+        }
       })
       expect(generate(root).code).toMatchSnapshot()
     })
index 2cec62d732d85eb2e8e55d24b404493d8fc20215..eaf48666b5e298ed72bd50003fa6848e7d6874b7 100644 (file)
@@ -286,6 +286,7 @@ export interface VNodeCall extends Node {
     | TemplateTextChildNode // single text child
     | SlotsExpression // component slots
     | ForRenderListExpression // v-for fragment call
+    | SimpleExpressionNode // hoisted
     | undefined
   patchFlag: string | undefined
   dynamicProps: string | SimpleExpressionNode | undefined
@@ -338,7 +339,7 @@ export interface Property extends Node {
 
 export interface ArrayExpression extends Node {
   type: NodeTypes.JS_ARRAY_EXPRESSION
-  elements: Array<string | JSChildNode>
+  elements: Array<string | Node>
 }
 
 export interface FunctionExpression extends Node {
index 52f2ead4ccf4d0522152e19e80b8e9dd06f4a7da..d930c36cd63d54de9920f391b39c827ea0cb5cda 100644 (file)
@@ -840,7 +840,7 @@ function genObjectExpression(node: ObjectExpression, context: CodegenContext) {
 }
 
 function genArrayExpression(node: ArrayExpression, context: CodegenContext) {
-  genNodeListAsArray(node.elements, context)
+  genNodeListAsArray(node.elements as CodegenNode[], context)
 }
 
 function genFunctionExpression(
index bc03d35135f4954fd0da7008edf7b4dcffd6b911..44e06fc22ca7934563da9a6084437cb97e59cd9d 100644 (file)
@@ -16,7 +16,8 @@ import {
   createCacheExpression,
   TemplateLiteral,
   createVNodeCall,
-  ConstantTypes
+  ConstantTypes,
+  ArrayExpression
 } from './ast'
 import {
   isString,
@@ -113,7 +114,7 @@ export interface TransformContext
   onNodeRemoved(): void
   addIdentifiers(exp: ExpressionNode | string): void
   removeIdentifiers(exp: ExpressionNode | string): void
-  hoist(exp: string | JSChildNode): SimpleExpressionNode
+  hoist(exp: string | JSChildNode | ArrayExpression): SimpleExpressionNode
   cache<T extends JSChildNode>(exp: T, isVNode?: boolean): CacheExpression | T
   constantCache: Map<TemplateChildNode, ConstantTypes>
 
index ee18b550fc821edd29913e4f5efdc02d554e8bb6..1ea895acedfe2c519a4b6f438e7ffc923858f31a 100644 (file)
@@ -11,10 +11,11 @@ import {
   VNodeCall,
   ParentNode,
   JSChildNode,
-  CallExpression
+  CallExpression,
+  createArrayExpression
 } from '../ast'
 import { TransformContext } from '../transform'
-import { PatchFlags, isString, isSymbol } from '@vue/shared'
+import { PatchFlags, isString, isSymbol, isArray } from '@vue/shared'
 import { getVNodeBlockHelper, getVNodeHelper, isSlotOutlet } from '../utils'
 import {
   OPEN_BLOCK,
@@ -51,7 +52,6 @@ function walk(
   context: TransformContext,
   doNotHoistNode: boolean = false
 ) {
-  let hasHoistedNode = false
   // Some transforms, e.g. transformAssetUrls from @vue/compiler-sfc, replaces
   // static bindings with expressions. These expressions are guaranteed to be
   // constant so they are still eligible for hoisting, but they are only
@@ -63,6 +63,9 @@ function walk(
   let canStringify = true
 
   const { children } = node
+  const originalCount = children.length
+  let hoistedCount = 0
+
   for (let i = 0; i < children.length; i++) {
     const child = children[i]
     // only plain elements & text calls are eligible for hoisting.
@@ -81,7 +84,7 @@ function walk(
           ;(child.codegenNode as VNodeCall).patchFlag =
             PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``)
           child.codegenNode = context.hoist(child.codegenNode!)
-          hasHoistedNode = true
+          hoistedCount++
           continue
         }
       } else {
@@ -115,7 +118,7 @@ function walk(
         }
         if (contentType >= ConstantTypes.CAN_HOIST) {
           child.codegenNode = context.hoist(child.codegenNode)
-          hasHoistedNode = true
+          hoistedCount++
         }
       }
     }
@@ -145,9 +148,24 @@ function walk(
     }
   }
 
-  if (canStringify && hasHoistedNode && context.transformHoist) {
+  if (canStringify && hoistedCount && context.transformHoist) {
     context.transformHoist(children, context, node)
   }
+
+  // all children were hoisted - the entire children array is hoistable.
+  if (
+    hoistedCount &&
+    hoistedCount === originalCount &&
+    node.type === NodeTypes.ELEMENT &&
+    node.tagType === ElementTypes.ELEMENT &&
+    node.codegenNode &&
+    node.codegenNode.type === NodeTypes.VNODE_CALL &&
+    isArray(node.codegenNode.children)
+  ) {
+    node.codegenNode.children = context.hoist(
+      createArrayExpression(node.codegenNode.children)
+    )
+  }
 }
 
 export function getConstantType(
index 612f4f0138a94a6a04b11d6b53cf63799ef49951..b9e7d81208f5c5ef1615a391ae1126e61948f15e 100644 (file)
@@ -30,7 +30,6 @@ describe('stringify static html', () => {
     const { ast } = compileWithStringify(
       `<div><div><div>hello</div><div>hello</div></div></div>`
     )
-    expect(ast.hoists.length).toBe(1)
     // should be a normal vnode call
     expect(ast.hoists[0]!.type).toBe(NodeTypes.VNODE_CALL)
   })
@@ -42,21 +41,25 @@ describe('stringify static html', () => {
         StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
       )}</div></div>`
     )
-    expect(ast.hoists.length).toBe(1)
     // should be optimized now
-    expect(ast.hoists[0]).toMatchObject({
-      type: NodeTypes.JS_CALL_EXPRESSION,
-      callee: CREATE_STATIC,
-      arguments: [
-        JSON.stringify(
-          `<div>${repeat(
-            `<span class="foo"></span>`,
-            StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
-          )}</div>`
-        ),
-        '1'
-      ]
-    })
+    expect(ast.hoists).toMatchObject([
+      {
+        type: NodeTypes.JS_CALL_EXPRESSION,
+        callee: CREATE_STATIC,
+        arguments: [
+          JSON.stringify(
+            `<div>${repeat(
+              `<span class="foo"></span>`,
+              StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
+            )}</div>`
+          ),
+          '1'
+        ]
+      }, // the children array is hoisted as well
+      {
+        type: NodeTypes.JS_ARRAY_EXPRESSION
+      }
+    ])
   })
 
   test('should work on eligible content (elements > 20)', () => {
@@ -66,21 +69,26 @@ describe('stringify static html', () => {
         StringifyThresholds.NODE_COUNT
       )}</div></div>`
     )
-    expect(ast.hoists.length).toBe(1)
     // should be optimized now
-    expect(ast.hoists[0]).toMatchObject({
-      type: NodeTypes.JS_CALL_EXPRESSION,
-      callee: CREATE_STATIC,
-      arguments: [
-        JSON.stringify(
-          `<div>${repeat(
-            `<span></span>`,
-            StringifyThresholds.NODE_COUNT
-          )}</div>`
-        ),
-        '1'
-      ]
-    })
+    expect(ast.hoists).toMatchObject([
+      {
+        type: NodeTypes.JS_CALL_EXPRESSION,
+        callee: CREATE_STATIC,
+        arguments: [
+          JSON.stringify(
+            `<div>${repeat(
+              `<span></span>`,
+              StringifyThresholds.NODE_COUNT
+            )}</div>`
+          ),
+          '1'
+        ]
+      },
+      // the children array is hoisted as well
+      {
+        type: NodeTypes.JS_ARRAY_EXPRESSION
+      }
+    ])
   })
 
   test('should work for multiple adjacent nodes', () => {
@@ -90,25 +98,30 @@ describe('stringify static html', () => {
         StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
       )}</div>`
     )
-    // should have 5 hoisted nodes, but the other 4 should be null
-    expect(ast.hoists.length).toBe(5)
-    for (let i = 1; i < 5; i++) {
-      expect(ast.hoists[i]).toBe(null)
-    }
-    // should be optimized now
-    expect(ast.hoists[0]).toMatchObject({
-      type: NodeTypes.JS_CALL_EXPRESSION,
-      callee: CREATE_STATIC,
-      arguments: [
-        JSON.stringify(
-          repeat(
-            `<span class="foo"></span>`,
-            StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
-          )
-        ),
-        '5'
-      ]
-    })
+    // should have 6 hoisted nodes (including the entire array),
+    // but 2~5 should be null because they are merged into 1
+    expect(ast.hoists).toMatchObject([
+      {
+        type: NodeTypes.JS_CALL_EXPRESSION,
+        callee: CREATE_STATIC,
+        arguments: [
+          JSON.stringify(
+            repeat(
+              `<span class="foo"></span>`,
+              StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
+            )
+          ),
+          '5'
+        ]
+      },
+      null,
+      null,
+      null,
+      null,
+      {
+        type: NodeTypes.JS_ARRAY_EXPRESSION
+      }
+    ])
   })
 
   test('serializing constant bindings', () => {
@@ -118,21 +131,25 @@ describe('stringify static html', () => {
         StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
       )}</div></div>`
     )
-    expect(ast.hoists.length).toBe(1)
     // should be optimized now
-    expect(ast.hoists[0]).toMatchObject({
-      type: NodeTypes.JS_CALL_EXPRESSION,
-      callee: CREATE_STATIC,
-      arguments: [
-        JSON.stringify(
-          `<div style="color:red;">${repeat(
-            `<span class="foo bar">1 + false</span>`,
-            StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
-          )}</div>`
-        ),
-        '1'
-      ]
-    })
+    expect(ast.hoists).toMatchObject([
+      {
+        type: NodeTypes.JS_CALL_EXPRESSION,
+        callee: CREATE_STATIC,
+        arguments: [
+          JSON.stringify(
+            `<div style="color:red;">${repeat(
+              `<span class="foo bar">1 + false</span>`,
+              StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
+            )}</div>`
+          ),
+          '1'
+        ]
+      },
+      {
+        type: NodeTypes.JS_ARRAY_EXPRESSION
+      }
+    ])
   })
 
   test('escape', () => {
@@ -143,21 +160,25 @@ describe('stringify static html', () => {
         StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
       )}</div></div>`
     )
-    expect(ast.hoists.length).toBe(1)
     // should be optimized now
-    expect(ast.hoists[0]).toMatchObject({
-      type: NodeTypes.JS_CALL_EXPRESSION,
-      callee: CREATE_STATIC,
-      arguments: [
-        JSON.stringify(
-          `<div>${repeat(
-            `<span class="foo&gt;ar">1 + &lt;</span>` + `<span>&amp;</span>`,
-            StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
-          )}</div>`
-        ),
-        '1'
-      ]
-    })
+    expect(ast.hoists).toMatchObject([
+      {
+        type: NodeTypes.JS_CALL_EXPRESSION,
+        callee: CREATE_STATIC,
+        arguments: [
+          JSON.stringify(
+            `<div>${repeat(
+              `<span class="foo&gt;ar">1 + &lt;</span>` + `<span>&amp;</span>`,
+              StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
+            )}</div>`
+          ),
+          '1'
+        ]
+      },
+      {
+        type: NodeTypes.JS_ARRAY_EXPRESSION
+      }
+    ])
   })
 
   test('should bail on runtime constant v-bind bindings', () => {
@@ -192,13 +213,16 @@ describe('stringify static html', () => {
         ]
       }
     )
-    // the expression and the tree are still hoistable
-    expect(ast.hoists.length).toBe(1)
-    // ...but the hoisted tree should not be stringified
-    expect(ast.hoists[0]).toMatchObject({
-      // if it's stringified it will be NodeTypes.CALL_EXPRESSION
-      type: NodeTypes.VNODE_CALL
-    })
+    expect(ast.hoists).toMatchObject([
+      {
+        // the expression and the tree are still hoistable
+        // but if it's stringified it will be NodeTypes.CALL_EXPRESSION
+        type: NodeTypes.VNODE_CALL
+      },
+      {
+        type: NodeTypes.JS_ARRAY_EXPRESSION
+      }
+    ])
   })
 
   // #1128
@@ -209,10 +233,14 @@ describe('stringify static html', () => {
         StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
       )}</div></div>`
     )
-    expect(ast.hoists.length).toBe(1)
-    expect(ast.hoists[0]).toMatchObject({
-      type: NodeTypes.VNODE_CALL // not CALL_EXPRESSION
-    })
+    expect(ast.hoists).toMatchObject([
+      {
+        type: NodeTypes.VNODE_CALL // not CALL_EXPRESSION
+      },
+      {
+        type: NodeTypes.JS_ARRAY_EXPRESSION
+      }
+    ])
 
     const { ast: ast2 } = compileWithStringify(
       `<div><div><input :indeterminate="true">${repeat(
@@ -220,10 +248,14 @@ describe('stringify static html', () => {
         StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
       )}</div></div>`
     )
-    expect(ast2.hoists.length).toBe(1)
-    expect(ast2.hoists[0]).toMatchObject({
-      type: NodeTypes.VNODE_CALL // not CALL_EXPRESSION
-    })
+    expect(ast2.hoists).toMatchObject([
+      {
+        type: NodeTypes.VNODE_CALL // not CALL_EXPRESSION
+      },
+      {
+        type: NodeTypes.JS_ARRAY_EXPRESSION
+      }
+    ])
   })
 
   test('should bail on non attribute bindings', () => {
@@ -233,10 +265,14 @@ describe('stringify static html', () => {
         StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
       )}<input indeterminate></div></div>`
     )
-    expect(ast.hoists.length).toBe(1)
-    expect(ast.hoists[0]).toMatchObject({
-      type: NodeTypes.VNODE_CALL // not CALL_EXPRESSION
-    })
+    expect(ast.hoists).toMatchObject([
+      {
+        type: NodeTypes.VNODE_CALL // not CALL_EXPRESSION
+      },
+      {
+        type: NodeTypes.JS_ARRAY_EXPRESSION
+      }
+    ])
 
     const { ast: ast2 } = compileWithStringify(
       `<div><div>${repeat(
@@ -244,10 +280,14 @@ describe('stringify static html', () => {
         StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
       )}<input :indeterminate="true"></div></div>`
     )
-    expect(ast2.hoists.length).toBe(1)
-    expect(ast2.hoists[0]).toMatchObject({
-      type: NodeTypes.VNODE_CALL // not CALL_EXPRESSION
-    })
+    expect(ast2.hoists).toMatchObject([
+      {
+        type: NodeTypes.VNODE_CALL // not CALL_EXPRESSION
+      },
+      {
+        type: NodeTypes.JS_ARRAY_EXPRESSION
+      }
+    ])
   })
 
   test('should bail on tags that has placement constraints (eg.tables related tags)', () => {
@@ -257,10 +297,14 @@ describe('stringify static html', () => {
         StringifyThresholds.ELEMENT_WITH_BINDING_COUNT
       )}</tbody></table>`
     )
-    expect(ast.hoists.length).toBe(1)
-    expect(ast.hoists[0]).toMatchObject({
-      type: NodeTypes.VNODE_CALL // not CALL_EXPRESSION
-    })
+    expect(ast.hoists).toMatchObject([
+      {
+        type: NodeTypes.VNODE_CALL // not CALL_EXPRESSION
+      },
+      {
+        type: NodeTypes.JS_ARRAY_EXPRESSION
+      }
+    ])
   })
 
   test('should bail inside slots', () => {