]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
perf: support only attaching slot scope ids when necessary
authorEvan You <yyx990803@gmail.com>
Fri, 5 Mar 2021 17:12:49 +0000 (12:12 -0500)
committerEvan You <yyx990803@gmail.com>
Fri, 5 Mar 2021 23:28:12 +0000 (18:28 -0500)
This is done by adding the `slotted: false` option to:

- compiler-dom
- compiler-ssr
- compiler-sfc (forwarded to template compiler)

At runtime, only slotted component will render slot fragments with
slot scope Ids. For SSR, only slotted component will add slot scope Ids
to rendered slot content. This should improve both runtime performance
and reduce SSR rendered markup size.

Note: requires SFC tooling (e.g. `vue-loader` and `vite`) to pass on
the `slotted` option from the SFC descriptoer to the `compileTemplate`
call.

packages/compiler-core/__tests__/transforms/transformSlotOutlet.spec.ts
packages/compiler-core/src/options.ts
packages/compiler-core/src/transform.ts
packages/compiler-core/src/transforms/transformSlotOutlet.ts
packages/compiler-sfc/__tests__/parse.spec.ts
packages/compiler-sfc/src/compileTemplate.ts
packages/compiler-sfc/src/parse.ts
packages/compiler-ssr/__tests__/ssrSlotOutlet.spec.ts
packages/compiler-ssr/src/transforms/ssrTransformSlotOutlet.ts
packages/runtime-core/__tests__/scopeId.spec.ts
packages/runtime-core/src/helpers/renderSlot.ts

index 82a5c8e50e1bcd5f865e55cdc8b2b7f982c79a4c..1a4ea3da8bf0c5e8914c1bfbe72286ef54283094 100644 (file)
@@ -339,6 +339,15 @@ describe('compiler: transform <slot> outlets', () => {
     })
   })
 
+  test('slot with slotted: true', async () => {
+    const ast = parseWithSlots(`<slot/>`, { slotted: true })
+    expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
+      type: NodeTypes.JS_CALL_EXPRESSION,
+      callee: RENDER_SLOT,
+      arguments: [`$slots`, `"default"`, `{}`, `undefined`, `true`]
+    })
+  })
+
   test(`error on unexpected custom directive on <slot>`, () => {
     const onError = jest.fn()
     const source = `<slot v-foo />`
index 2850da196b1328f451c7ad55f110f44b1c9afb15..d8d2573a6e8ea90855deef73c62f4d23fab02af7 100644 (file)
@@ -199,6 +199,12 @@ export interface TransformOptions extends SharedTransformCodegenOptions {
    * SFC scoped styles ID
    */
   scopeId?: string | null
+  /**
+   * Indicates this SFC template has used :slotted in its styles
+   * Defaults to `true` for backwards compatibility - SFC tooling should set it
+   * to `false` if no `:slotted` usage is detected in `<style>`
+   */
+  slotted?: boolean
   /**
    * SFC `<style vars>` injection string
    * Should already be an object expression, e.g. `{ 'xxxx-color': color }`
index 87daf363eb43d9d273f2ebd8bb5d7d8c0e8f1a30..22a8f303c6ac24a7242b6896f85ffbd9c843833c 100644 (file)
@@ -128,6 +128,7 @@ export function createTransformContext(
     isCustomElement = NOOP,
     expressionPlugins = [],
     scopeId = null,
+    slotted = true,
     ssr = false,
     ssrCssVars = ``,
     bindingMetadata = EMPTY_OBJ,
@@ -150,6 +151,7 @@ export function createTransformContext(
     isCustomElement,
     expressionPlugins,
     scopeId,
+    slotted,
     ssr,
     ssrCssVars,
     bindingMetadata,
index 6de8b134d7e8684d30cc814f851a33f51dd450be..5dc22257edf44f5fb744bab00f98adb521ee6f94 100644 (file)
@@ -34,6 +34,16 @@ export const transformSlotOutlet: NodeTransform = (node, context) => {
       slotArgs.push(createFunctionExpression([], children, false, false, loc))
     }
 
+    if (context.slotted) {
+      if (!slotProps) {
+        slotArgs.push(`{}`)
+      }
+      if (!children.length) {
+        slotArgs.push(`undefined`)
+      }
+      slotArgs.push(`true`)
+    }
+
     node.codegenNode = createCallExpression(
       context.helper(RENDER_SLOT),
       slotArgs,
index aa4b5b48be5c2bfb5e36222f8c7e2f4ce318c4d2..95119fab0547c054a470027c0b0ad08fe8f8a3b1 100644 (file)
@@ -170,6 +170,22 @@ h1 { color: red }
     expect(errors.length).toBe(0)
   })
 
+  test('slotted detection', async () => {
+    expect(parse(`<template>hi</template>`).descriptor.slotted).toBe(false)
+    expect(
+      parse(`<template>hi</template><style>h1{color:red;}</style>`).descriptor
+        .slotted
+    ).toBe(false)
+    expect(
+      parse(`<template>hi</template><style>:slotted(h1){color:red;}</style>`)
+        .descriptor.slotted
+    ).toBe(true)
+    expect(
+      parse(`<template>hi</template><style>::v-slotted(h1){color:red;}</style>`)
+        .descriptor.slotted
+    ).toBe(true)
+  })
+
   test('error tolerance', () => {
     const { errors } = parse(`<template>`)
     expect(errors.length).toBe(1)
index 36a86461386fe14dbe3620b68ee9f945b9c123bd..45fb36b7fb7914b9789e6e126c2d97d9003cd208 100644 (file)
@@ -45,6 +45,7 @@ export interface SFCTemplateCompileOptions {
   filename: string
   id: string
   scoped?: boolean
+  slotted?: boolean
   isProd?: boolean
   ssr?: boolean
   ssrCssVars?: string[]
@@ -158,6 +159,7 @@ function doCompileTemplate({
   filename,
   id,
   scoped,
+  slotted,
   inMap,
   source,
   ssr = false,
@@ -204,6 +206,7 @@ function doCompileTemplate({
         ? genCssVarsFromList(ssrCssVars, shortId, isProd)
         : '',
     scopeId: scoped ? longId : undefined,
+    slotted,
     ...compilerOptions,
     nodeTransforms: nodeTransforms.concat(compilerOptions.nodeTransforms || []),
     filename,
index 303babe18f02f6ac6301620c1269ef8c08ebc501..98359491a48688ec81dd962b2e0673646ab8363c 100644 (file)
@@ -59,6 +59,9 @@ export interface SFCDescriptor {
   styles: SFCStyleBlock[]
   customBlocks: SFCBlock[]
   cssVars: string[]
+  // whether the SFC uses :slotted() modifier.
+  // this is used as a compiler optimization hint.
+  slotted: boolean
 }
 
 export interface SFCParseResult {
@@ -100,7 +103,8 @@ export function parse(
     scriptSetup: null,
     styles: [],
     customBlocks: [],
-    cssVars: []
+    cssVars: [],
+    slotted: false
   }
 
   const errors: (CompilerError | SyntaxError)[] = []
@@ -231,6 +235,10 @@ export function parse(
     warnExperimental(`v-bind() CSS variable injection`, 231)
   }
 
+  // check if the SFC uses :slotted
+  const slottedRE = /(?:::v-|:)slotted\(/
+  descriptor.slotted = descriptor.styles.some(s => slottedRE.test(s.content))
+
   const result = {
     descriptor,
     errors
index 2219ff07725934775506aa4052056c15abd8a4e4..415412e8354ede0feed7ac28b6da236d4f3cfbc0 100644 (file)
@@ -6,7 +6,7 @@ describe('ssr: <slot>', () => {
       "const { ssrRenderSlot: _ssrRenderSlot } = require(\\"@vue/server-renderer\\")
 
       return function ssrRender(_ctx, _push, _parent, _attrs) {
-        _ssrRenderSlot(_ctx.$slots, \\"default\\", {}, null, _push, _parent, null)
+        _ssrRenderSlot(_ctx.$slots, \\"default\\", {}, null, _push, _parent)
       }"
     `)
   })
@@ -16,7 +16,7 @@ describe('ssr: <slot>', () => {
       "const { ssrRenderSlot: _ssrRenderSlot } = require(\\"@vue/server-renderer\\")
 
       return function ssrRender(_ctx, _push, _parent, _attrs) {
-        _ssrRenderSlot(_ctx.$slots, \\"foo\\", {}, null, _push, _parent, null)
+        _ssrRenderSlot(_ctx.$slots, \\"foo\\", {}, null, _push, _parent)
       }"
     `)
   })
@@ -26,7 +26,7 @@ describe('ssr: <slot>', () => {
       "const { ssrRenderSlot: _ssrRenderSlot } = require(\\"@vue/server-renderer\\")
 
       return function ssrRender(_ctx, _push, _parent, _attrs) {
-        _ssrRenderSlot(_ctx.$slots, _ctx.bar.baz, {}, null, _push, _parent, null)
+        _ssrRenderSlot(_ctx.$slots, _ctx.bar.baz, {}, null, _push, _parent)
       }"
     `)
   })
@@ -40,7 +40,7 @@ describe('ssr: <slot>', () => {
         _ssrRenderSlot(_ctx.$slots, \\"foo\\", {
           p: 1,
           bar: \\"2\\"
-        }, null, _push, _parent, null)
+        }, null, _push, _parent)
       }"
     `)
   })
@@ -53,7 +53,7 @@ describe('ssr: <slot>', () => {
       return function ssrRender(_ctx, _push, _parent, _attrs) {
         _ssrRenderSlot(_ctx.$slots, \\"default\\", {}, () => {
           _push(\`some \${_ssrInterpolate(_ctx.fallback)} content\`)
-        }, _push, _parent, null)
+        }, _push, _parent)
       }"
     `)
   })
@@ -72,6 +72,21 @@ describe('ssr: <slot>', () => {
     `)
   })
 
+  test('with scopeId + slotted:false', async () => {
+    expect(
+      compile(`<slot/>`, {
+        scopeId: 'hello',
+        slotted: false
+      }).code
+    ).toMatchInlineSnapshot(`
+      "const { ssrRenderSlot: _ssrRenderSlot } = require(\\"@vue/server-renderer\\")
+
+      return function ssrRender(_ctx, _push, _parent, _attrs) {
+        _ssrRenderSlot(_ctx.$slots, \\"default\\", {}, null, _push, _parent)
+      }"
+    `)
+  })
+
   test('with forwarded scopeId', async () => {
     expect(
       compile(`<Comp><slot/></Comp>`, {
@@ -90,7 +105,7 @@ describe('ssr: <slot>', () => {
               _ssrRenderSlot(_ctx.$slots, \\"default\\", {}, null, _push, _parent, \\"hello-s\\" + _scopeId)
             } else {
               return [
-                _renderSlot(_ctx.$slots, \\"default\\")
+                _renderSlot(_ctx.$slots, \\"default\\", {}, undefined, true)
               ]
             }
           }),
index b2b2de4f5fdaaba7d67a007bfff390d7a5a63ef5..98020f6c179ae944dd723b64607275d8475cd21a 100644 (file)
@@ -15,18 +15,25 @@ import {
 export const ssrTransformSlotOutlet: NodeTransform = (node, context) => {
   if (isSlotOutlet(node)) {
     const { slotName, slotProps } = processSlotOutlet(node, context)
+
+    const args = [
+      `_ctx.$slots`,
+      slotName,
+      slotProps || `{}`,
+      // fallback content placeholder. will be replaced in the process phase
+      `null`,
+      `_push`,
+      `_parent`
+    ]
+
+    // inject slot scope id if current template uses :slotted
+    if (context.scopeId && context.slotted !== false) {
+      args.push(`"${context.scopeId}-s"`)
+    }
+
     node.ssrCodegenNode = createCallExpression(
       context.helper(SSR_RENDER_SLOT),
-      [
-        `_ctx.$slots`,
-        slotName,
-        slotProps || `{}`,
-        // fallback content placeholder. will be replaced in the process phase
-        `null`,
-        `_push`,
-        `_parent`,
-        context.scopeId ? `"${context.scopeId}-s"` : `null`
-      ]
+      args
     )
   }
 }
@@ -45,11 +52,12 @@ export function ssrProcessSlotOutlet(
     renderCall.arguments[3] = fallbackRenderFn
   }
 
-  // Forwarded <slot/>. Add slot scope id
+  // Forwarded <slot/>. Merge slot scope ids
   if (context.withSlotScopeId) {
-    const scopeId = renderCall.arguments[6] as string
-    renderCall.arguments[6] =
-      scopeId === `null` ? `_scopeId` : `${scopeId} + _scopeId`
+    const slotScopeId = renderCall.arguments[6]
+    renderCall.arguments[6] = slotScopeId
+      ? `${slotScopeId as string} + _scopeId`
+      : `_scopeId`
   }
 
   context.pushStatement(node.ssrCodegenNode!)
index e81af6c56fa0c6ae8140a79d91363ffdd7a08b39..4b565275fbd52606d8bf99f99e1da5c2ca04fe5a 100644 (file)
@@ -40,7 +40,7 @@ describe('scopeId runtime support', () => {
     const Child = {
       __scopeId: 'child',
       render(this: any) {
-        return h('div', renderSlot(this.$slots, 'default'))
+        return h('div', renderSlot(this.$slots, 'default', {}, undefined, true))
       }
     }
     const Child2 = {
@@ -92,7 +92,9 @@ describe('scopeId runtime support', () => {
       render(this: any) {
         // <Wrapper><slot/></Wrapper>
         return h(Wrapper, null, {
-          default: withCtx(() => [renderSlot(this.$slots, 'default')])
+          default: withCtx(() => [
+            renderSlot(this.$slots, 'default', {}, undefined, true)
+          ])
         })
       }
     }
@@ -118,8 +120,8 @@ describe('scopeId runtime support', () => {
     render(h(Root), root)
     expect(serializeInner(root)).toBe(
       `<div class="wrapper" wrapper slotted root>` +
-        `<div root wrapper-s slotted-s>hoisted</div>` +
-        `<div root wrapper-s slotted-s>dynamic</div>` +
+        `<div root slotted-s>hoisted</div>` +
+        `<div root slotted-s>dynamic</div>` +
         `</div>`
     )
 
@@ -144,9 +146,9 @@ describe('scopeId runtime support', () => {
     render(h(Root2), root2)
     expect(serializeInner(root2)).toBe(
       `<div class="wrapper" wrapper slotted root>` +
-        `<div class="wrapper" wrapper root wrapper-s slotted-s>` +
-        `<div root wrapper-s>hoisted</div>` +
-        `<div root wrapper-s>dynamic</div>` +
+        `<div class="wrapper" wrapper root slotted-s>` +
+        `<div root>hoisted</div>` +
+        `<div root>dynamic</div>` +
         `</div>` +
         `</div>`
     )
index 56cdd3dcd450d4c4b465557da42b079563f1b64f..08b14558b1c0891c31ccce5f3b6e3fa69e3c44ba 100644 (file)
@@ -25,7 +25,8 @@ export function renderSlot(
   props: Data = {},
   // this is not a user-facing function, so the fallback is always generated by
   // the compiler and guaranteed to be a function returning an array
-  fallback?: () => VNodeArrayChildren
+  fallback?: () => VNodeArrayChildren,
+  hasSlotted?: boolean
 ): VNode {
   let slot = slots[name]
 
@@ -53,8 +54,7 @@ export function renderSlot(
       ? PatchFlags.STABLE_FRAGMENT
       : PatchFlags.BAIL
   )
-  // TODO (optimization) only add slot scope id if :slotted is used
-  if (rendered.scopeId) {
+  if (hasSlotted && rendered.scopeId) {
     rendered.slotScopeIds = [rendered.scopeId + '-s']
   }
   isRenderingCompiledSlot--