Skip to content

Commit

Permalink
perf: support only attaching slot scope ids when necessary
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
yyx990803 committed Mar 5, 2021
1 parent f74b16c commit 02cbbb7
Show file tree
Hide file tree
Showing 11 changed files with 110 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 />`
Expand Down
6 changes: 6 additions & 0 deletions packages/compiler-core/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }`
Expand Down
2 changes: 2 additions & 0 deletions packages/compiler-core/src/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ export function createTransformContext(
isCustomElement = NOOP,
expressionPlugins = [],
scopeId = null,
slotted = true,
ssr = false,
ssrCssVars = ``,
bindingMetadata = EMPTY_OBJ,
Expand All @@ -150,6 +151,7 @@ export function createTransformContext(
isCustomElement,
expressionPlugins,
scopeId,
slotted,
ssr,
ssrCssVars,
bindingMetadata,
Expand Down
10 changes: 10 additions & 0 deletions packages/compiler-core/src/transforms/transformSlotOutlet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
16 changes: 16 additions & 0 deletions packages/compiler-sfc/__tests__/parse.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions packages/compiler-sfc/src/compileTemplate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export interface SFCTemplateCompileOptions {
filename: string
id: string
scoped?: boolean
slotted?: boolean
isProd?: boolean
ssr?: boolean
ssrCssVars?: string[]
Expand Down Expand Up @@ -158,6 +159,7 @@ function doCompileTemplate({
filename,
id,
scoped,
slotted,
inMap,
source,
ssr = false,
Expand Down Expand Up @@ -204,6 +206,7 @@ function doCompileTemplate({
? genCssVarsFromList(ssrCssVars, shortId, isProd)
: '',
scopeId: scoped ? longId : undefined,
slotted,
...compilerOptions,
nodeTransforms: nodeTransforms.concat(compilerOptions.nodeTransforms || []),
filename,
Expand Down
10 changes: 9 additions & 1 deletion packages/compiler-sfc/src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -100,7 +103,8 @@ export function parse(
scriptSetup: null,
styles: [],
customBlocks: [],
cssVars: []
cssVars: [],
slotted: false
}

const errors: (CompilerError | SyntaxError)[] = []
Expand Down Expand Up @@ -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
Expand Down
27 changes: 21 additions & 6 deletions packages/compiler-ssr/__tests__/ssrSlotOutlet.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}"
`)
})
Expand All @@ -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)
}"
`)
})
Expand All @@ -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)
}"
`)
})
Expand All @@ -40,7 +40,7 @@ describe('ssr: <slot>', () => {
_ssrRenderSlot(_ctx.$slots, \\"foo\\", {
p: 1,
bar: \\"2\\"
}, null, _push, _parent, null)
}, null, _push, _parent)
}"
`)
})
Expand All @@ -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)
}"
`)
})
Expand All @@ -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>`, {
Expand All @@ -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)
]
}
}),
Expand Down
36 changes: 22 additions & 14 deletions packages/compiler-ssr/src/transforms/ssrTransformSlotOutlet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}
}
Expand All @@ -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!)
Expand Down
16 changes: 9 additions & 7 deletions packages/runtime-core/__tests__/scopeId.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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)
])
})
}
}
Expand All @@ -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>`
)

Expand All @@ -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>`
)
Expand Down
6 changes: 3 additions & 3 deletions packages/runtime-core/src/helpers/renderSlot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down Expand Up @@ -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--
Expand Down

0 comments on commit 02cbbb7

Please sign in to comment.