Skip to content

Commit 02cbbb7

Browse files
committed
perf: support only attaching slot scope ids when necessary
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.
1 parent f74b16c commit 02cbbb7

File tree

11 files changed

+110
-31
lines changed

11 files changed

+110
-31
lines changed

packages/compiler-core/__tests__/transforms/transformSlotOutlet.spec.ts

+9
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,15 @@ describe('compiler: transform <slot> outlets', () => {
339339
})
340340
})
341341

342+
test('slot with slotted: true', async () => {
343+
const ast = parseWithSlots(`<slot/>`, { slotted: true })
344+
expect((ast.children[0] as ElementNode).codegenNode).toMatchObject({
345+
type: NodeTypes.JS_CALL_EXPRESSION,
346+
callee: RENDER_SLOT,
347+
arguments: [`$slots`, `"default"`, `{}`, `undefined`, `true`]
348+
})
349+
})
350+
342351
test(`error on unexpected custom directive on <slot>`, () => {
343352
const onError = jest.fn()
344353
const source = `<slot v-foo />`

packages/compiler-core/src/options.ts

+6
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,12 @@ export interface TransformOptions extends SharedTransformCodegenOptions {
199199
* SFC scoped styles ID
200200
*/
201201
scopeId?: string | null
202+
/**
203+
* Indicates this SFC template has used :slotted in its styles
204+
* Defaults to `true` for backwards compatibility - SFC tooling should set it
205+
* to `false` if no `:slotted` usage is detected in `<style>`
206+
*/
207+
slotted?: boolean
202208
/**
203209
* SFC `<style vars>` injection string
204210
* Should already be an object expression, e.g. `{ 'xxxx-color': color }`

packages/compiler-core/src/transform.ts

+2
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ export function createTransformContext(
128128
isCustomElement = NOOP,
129129
expressionPlugins = [],
130130
scopeId = null,
131+
slotted = true,
131132
ssr = false,
132133
ssrCssVars = ``,
133134
bindingMetadata = EMPTY_OBJ,
@@ -150,6 +151,7 @@ export function createTransformContext(
150151
isCustomElement,
151152
expressionPlugins,
152153
scopeId,
154+
slotted,
153155
ssr,
154156
ssrCssVars,
155157
bindingMetadata,

packages/compiler-core/src/transforms/transformSlotOutlet.ts

+10
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ export const transformSlotOutlet: NodeTransform = (node, context) => {
3434
slotArgs.push(createFunctionExpression([], children, false, false, loc))
3535
}
3636

37+
if (context.slotted) {
38+
if (!slotProps) {
39+
slotArgs.push(`{}`)
40+
}
41+
if (!children.length) {
42+
slotArgs.push(`undefined`)
43+
}
44+
slotArgs.push(`true`)
45+
}
46+
3747
node.codegenNode = createCallExpression(
3848
context.helper(RENDER_SLOT),
3949
slotArgs,

packages/compiler-sfc/__tests__/parse.spec.ts

+16
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,22 @@ h1 { color: red }
170170
expect(errors.length).toBe(0)
171171
})
172172

173+
test('slotted detection', async () => {
174+
expect(parse(`<template>hi</template>`).descriptor.slotted).toBe(false)
175+
expect(
176+
parse(`<template>hi</template><style>h1{color:red;}</style>`).descriptor
177+
.slotted
178+
).toBe(false)
179+
expect(
180+
parse(`<template>hi</template><style>:slotted(h1){color:red;}</style>`)
181+
.descriptor.slotted
182+
).toBe(true)
183+
expect(
184+
parse(`<template>hi</template><style>::v-slotted(h1){color:red;}</style>`)
185+
.descriptor.slotted
186+
).toBe(true)
187+
})
188+
173189
test('error tolerance', () => {
174190
const { errors } = parse(`<template>`)
175191
expect(errors.length).toBe(1)

packages/compiler-sfc/src/compileTemplate.ts

+3
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export interface SFCTemplateCompileOptions {
4545
filename: string
4646
id: string
4747
scoped?: boolean
48+
slotted?: boolean
4849
isProd?: boolean
4950
ssr?: boolean
5051
ssrCssVars?: string[]
@@ -158,6 +159,7 @@ function doCompileTemplate({
158159
filename,
159160
id,
160161
scoped,
162+
slotted,
161163
inMap,
162164
source,
163165
ssr = false,
@@ -204,6 +206,7 @@ function doCompileTemplate({
204206
? genCssVarsFromList(ssrCssVars, shortId, isProd)
205207
: '',
206208
scopeId: scoped ? longId : undefined,
209+
slotted,
207210
...compilerOptions,
208211
nodeTransforms: nodeTransforms.concat(compilerOptions.nodeTransforms || []),
209212
filename,

packages/compiler-sfc/src/parse.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ export interface SFCDescriptor {
5959
styles: SFCStyleBlock[]
6060
customBlocks: SFCBlock[]
6161
cssVars: string[]
62+
// whether the SFC uses :slotted() modifier.
63+
// this is used as a compiler optimization hint.
64+
slotted: boolean
6265
}
6366

6467
export interface SFCParseResult {
@@ -100,7 +103,8 @@ export function parse(
100103
scriptSetup: null,
101104
styles: [],
102105
customBlocks: [],
103-
cssVars: []
106+
cssVars: [],
107+
slotted: false
104108
}
105109

106110
const errors: (CompilerError | SyntaxError)[] = []
@@ -231,6 +235,10 @@ export function parse(
231235
warnExperimental(`v-bind() CSS variable injection`, 231)
232236
}
233237

238+
// check if the SFC uses :slotted
239+
const slottedRE = /(?:::v-|:)slotted\(/
240+
descriptor.slotted = descriptor.styles.some(s => slottedRE.test(s.content))
241+
234242
const result = {
235243
descriptor,
236244
errors

packages/compiler-ssr/__tests__/ssrSlotOutlet.spec.ts

+21-6
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ describe('ssr: <slot>', () => {
66
"const { ssrRenderSlot: _ssrRenderSlot } = require(\\"@vue/server-renderer\\")
77
88
return function ssrRender(_ctx, _push, _parent, _attrs) {
9-
_ssrRenderSlot(_ctx.$slots, \\"default\\", {}, null, _push, _parent, null)
9+
_ssrRenderSlot(_ctx.$slots, \\"default\\", {}, null, _push, _parent)
1010
}"
1111
`)
1212
})
@@ -16,7 +16,7 @@ describe('ssr: <slot>', () => {
1616
"const { ssrRenderSlot: _ssrRenderSlot } = require(\\"@vue/server-renderer\\")
1717
1818
return function ssrRender(_ctx, _push, _parent, _attrs) {
19-
_ssrRenderSlot(_ctx.$slots, \\"foo\\", {}, null, _push, _parent, null)
19+
_ssrRenderSlot(_ctx.$slots, \\"foo\\", {}, null, _push, _parent)
2020
}"
2121
`)
2222
})
@@ -26,7 +26,7 @@ describe('ssr: <slot>', () => {
2626
"const { ssrRenderSlot: _ssrRenderSlot } = require(\\"@vue/server-renderer\\")
2727
2828
return function ssrRender(_ctx, _push, _parent, _attrs) {
29-
_ssrRenderSlot(_ctx.$slots, _ctx.bar.baz, {}, null, _push, _parent, null)
29+
_ssrRenderSlot(_ctx.$slots, _ctx.bar.baz, {}, null, _push, _parent)
3030
}"
3131
`)
3232
})
@@ -40,7 +40,7 @@ describe('ssr: <slot>', () => {
4040
_ssrRenderSlot(_ctx.$slots, \\"foo\\", {
4141
p: 1,
4242
bar: \\"2\\"
43-
}, null, _push, _parent, null)
43+
}, null, _push, _parent)
4444
}"
4545
`)
4646
})
@@ -53,7 +53,7 @@ describe('ssr: <slot>', () => {
5353
return function ssrRender(_ctx, _push, _parent, _attrs) {
5454
_ssrRenderSlot(_ctx.$slots, \\"default\\", {}, () => {
5555
_push(\`some \${_ssrInterpolate(_ctx.fallback)} content\`)
56-
}, _push, _parent, null)
56+
}, _push, _parent)
5757
}"
5858
`)
5959
})
@@ -72,6 +72,21 @@ describe('ssr: <slot>', () => {
7272
`)
7373
})
7474

75+
test('with scopeId + slotted:false', async () => {
76+
expect(
77+
compile(`<slot/>`, {
78+
scopeId: 'hello',
79+
slotted: false
80+
}).code
81+
).toMatchInlineSnapshot(`
82+
"const { ssrRenderSlot: _ssrRenderSlot } = require(\\"@vue/server-renderer\\")
83+
84+
return function ssrRender(_ctx, _push, _parent, _attrs) {
85+
_ssrRenderSlot(_ctx.$slots, \\"default\\", {}, null, _push, _parent)
86+
}"
87+
`)
88+
})
89+
7590
test('with forwarded scopeId', async () => {
7691
expect(
7792
compile(`<Comp><slot/></Comp>`, {
@@ -90,7 +105,7 @@ describe('ssr: <slot>', () => {
90105
_ssrRenderSlot(_ctx.$slots, \\"default\\", {}, null, _push, _parent, \\"hello-s\\" + _scopeId)
91106
} else {
92107
return [
93-
_renderSlot(_ctx.$slots, \\"default\\")
108+
_renderSlot(_ctx.$slots, \\"default\\", {}, undefined, true)
94109
]
95110
}
96111
}),

packages/compiler-ssr/src/transforms/ssrTransformSlotOutlet.ts

+22-14
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,25 @@ import {
1515
export const ssrTransformSlotOutlet: NodeTransform = (node, context) => {
1616
if (isSlotOutlet(node)) {
1717
const { slotName, slotProps } = processSlotOutlet(node, context)
18+
19+
const args = [
20+
`_ctx.$slots`,
21+
slotName,
22+
slotProps || `{}`,
23+
// fallback content placeholder. will be replaced in the process phase
24+
`null`,
25+
`_push`,
26+
`_parent`
27+
]
28+
29+
// inject slot scope id if current template uses :slotted
30+
if (context.scopeId && context.slotted !== false) {
31+
args.push(`"${context.scopeId}-s"`)
32+
}
33+
1834
node.ssrCodegenNode = createCallExpression(
1935
context.helper(SSR_RENDER_SLOT),
20-
[
21-
`_ctx.$slots`,
22-
slotName,
23-
slotProps || `{}`,
24-
// fallback content placeholder. will be replaced in the process phase
25-
`null`,
26-
`_push`,
27-
`_parent`,
28-
context.scopeId ? `"${context.scopeId}-s"` : `null`
29-
]
36+
args
3037
)
3138
}
3239
}
@@ -45,11 +52,12 @@ export function ssrProcessSlotOutlet(
4552
renderCall.arguments[3] = fallbackRenderFn
4653
}
4754

48-
// Forwarded <slot/>. Add slot scope id
55+
// Forwarded <slot/>. Merge slot scope ids
4956
if (context.withSlotScopeId) {
50-
const scopeId = renderCall.arguments[6] as string
51-
renderCall.arguments[6] =
52-
scopeId === `null` ? `_scopeId` : `${scopeId} + _scopeId`
57+
const slotScopeId = renderCall.arguments[6]
58+
renderCall.arguments[6] = slotScopeId
59+
? `${slotScopeId as string} + _scopeId`
60+
: `_scopeId`
5361
}
5462

5563
context.pushStatement(node.ssrCodegenNode!)

packages/runtime-core/__tests__/scopeId.spec.ts

+9-7
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ describe('scopeId runtime support', () => {
4040
const Child = {
4141
__scopeId: 'child',
4242
render(this: any) {
43-
return h('div', renderSlot(this.$slots, 'default'))
43+
return h('div', renderSlot(this.$slots, 'default', {}, undefined, true))
4444
}
4545
}
4646
const Child2 = {
@@ -92,7 +92,9 @@ describe('scopeId runtime support', () => {
9292
render(this: any) {
9393
// <Wrapper><slot/></Wrapper>
9494
return h(Wrapper, null, {
95-
default: withCtx(() => [renderSlot(this.$slots, 'default')])
95+
default: withCtx(() => [
96+
renderSlot(this.$slots, 'default', {}, undefined, true)
97+
])
9698
})
9799
}
98100
}
@@ -118,8 +120,8 @@ describe('scopeId runtime support', () => {
118120
render(h(Root), root)
119121
expect(serializeInner(root)).toBe(
120122
`<div class="wrapper" wrapper slotted root>` +
121-
`<div root wrapper-s slotted-s>hoisted</div>` +
122-
`<div root wrapper-s slotted-s>dynamic</div>` +
123+
`<div root slotted-s>hoisted</div>` +
124+
`<div root slotted-s>dynamic</div>` +
123125
`</div>`
124126
)
125127

@@ -144,9 +146,9 @@ describe('scopeId runtime support', () => {
144146
render(h(Root2), root2)
145147
expect(serializeInner(root2)).toBe(
146148
`<div class="wrapper" wrapper slotted root>` +
147-
`<div class="wrapper" wrapper root wrapper-s slotted-s>` +
148-
`<div root wrapper-s>hoisted</div>` +
149-
`<div root wrapper-s>dynamic</div>` +
149+
`<div class="wrapper" wrapper root slotted-s>` +
150+
`<div root>hoisted</div>` +
151+
`<div root>dynamic</div>` +
150152
`</div>` +
151153
`</div>`
152154
)

packages/runtime-core/src/helpers/renderSlot.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ export function renderSlot(
2525
props: Data = {},
2626
// this is not a user-facing function, so the fallback is always generated by
2727
// the compiler and guaranteed to be a function returning an array
28-
fallback?: () => VNodeArrayChildren
28+
fallback?: () => VNodeArrayChildren,
29+
hasSlotted?: boolean
2930
): VNode {
3031
let slot = slots[name]
3132

@@ -53,8 +54,7 @@ export function renderSlot(
5354
? PatchFlags.STABLE_FRAGMENT
5455
: PatchFlags.BAIL
5556
)
56-
// TODO (optimization) only add slot scope id if :slotted is used
57-
if (rendered.scopeId) {
57+
if (hasSlotted && rendered.scopeId) {
5858
rendered.slotScopeIds = [rendered.scopeId + '-s']
5959
}
6060
isRenderingCompiledSlot--

0 commit comments

Comments
 (0)