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.
This commit is contained in:
parent
f74b16ccfe
commit
02cbbb718c
@ -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>`, () => {
|
test(`error on unexpected custom directive on <slot>`, () => {
|
||||||
const onError = jest.fn()
|
const onError = jest.fn()
|
||||||
const source = `<slot v-foo />`
|
const source = `<slot v-foo />`
|
||||||
|
@ -199,6 +199,12 @@ export interface TransformOptions extends SharedTransformCodegenOptions {
|
|||||||
* SFC scoped styles ID
|
* SFC scoped styles ID
|
||||||
*/
|
*/
|
||||||
scopeId?: string | null
|
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
|
* SFC `<style vars>` injection string
|
||||||
* Should already be an object expression, e.g. `{ 'xxxx-color': color }`
|
* Should already be an object expression, e.g. `{ 'xxxx-color': color }`
|
||||||
|
@ -128,6 +128,7 @@ export function createTransformContext(
|
|||||||
isCustomElement = NOOP,
|
isCustomElement = NOOP,
|
||||||
expressionPlugins = [],
|
expressionPlugins = [],
|
||||||
scopeId = null,
|
scopeId = null,
|
||||||
|
slotted = true,
|
||||||
ssr = false,
|
ssr = false,
|
||||||
ssrCssVars = ``,
|
ssrCssVars = ``,
|
||||||
bindingMetadata = EMPTY_OBJ,
|
bindingMetadata = EMPTY_OBJ,
|
||||||
@ -150,6 +151,7 @@ export function createTransformContext(
|
|||||||
isCustomElement,
|
isCustomElement,
|
||||||
expressionPlugins,
|
expressionPlugins,
|
||||||
scopeId,
|
scopeId,
|
||||||
|
slotted,
|
||||||
ssr,
|
ssr,
|
||||||
ssrCssVars,
|
ssrCssVars,
|
||||||
bindingMetadata,
|
bindingMetadata,
|
||||||
|
@ -34,6 +34,16 @@ export const transformSlotOutlet: NodeTransform = (node, context) => {
|
|||||||
slotArgs.push(createFunctionExpression([], children, false, false, loc))
|
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(
|
node.codegenNode = createCallExpression(
|
||||||
context.helper(RENDER_SLOT),
|
context.helper(RENDER_SLOT),
|
||||||
slotArgs,
|
slotArgs,
|
||||||
|
@ -170,6 +170,22 @@ h1 { color: red }
|
|||||||
expect(errors.length).toBe(0)
|
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', () => {
|
test('error tolerance', () => {
|
||||||
const { errors } = parse(`<template>`)
|
const { errors } = parse(`<template>`)
|
||||||
expect(errors.length).toBe(1)
|
expect(errors.length).toBe(1)
|
||||||
|
@ -45,6 +45,7 @@ export interface SFCTemplateCompileOptions {
|
|||||||
filename: string
|
filename: string
|
||||||
id: string
|
id: string
|
||||||
scoped?: boolean
|
scoped?: boolean
|
||||||
|
slotted?: boolean
|
||||||
isProd?: boolean
|
isProd?: boolean
|
||||||
ssr?: boolean
|
ssr?: boolean
|
||||||
ssrCssVars?: string[]
|
ssrCssVars?: string[]
|
||||||
@ -158,6 +159,7 @@ function doCompileTemplate({
|
|||||||
filename,
|
filename,
|
||||||
id,
|
id,
|
||||||
scoped,
|
scoped,
|
||||||
|
slotted,
|
||||||
inMap,
|
inMap,
|
||||||
source,
|
source,
|
||||||
ssr = false,
|
ssr = false,
|
||||||
@ -204,6 +206,7 @@ function doCompileTemplate({
|
|||||||
? genCssVarsFromList(ssrCssVars, shortId, isProd)
|
? genCssVarsFromList(ssrCssVars, shortId, isProd)
|
||||||
: '',
|
: '',
|
||||||
scopeId: scoped ? longId : undefined,
|
scopeId: scoped ? longId : undefined,
|
||||||
|
slotted,
|
||||||
...compilerOptions,
|
...compilerOptions,
|
||||||
nodeTransforms: nodeTransforms.concat(compilerOptions.nodeTransforms || []),
|
nodeTransforms: nodeTransforms.concat(compilerOptions.nodeTransforms || []),
|
||||||
filename,
|
filename,
|
||||||
|
@ -59,6 +59,9 @@ export interface SFCDescriptor {
|
|||||||
styles: SFCStyleBlock[]
|
styles: SFCStyleBlock[]
|
||||||
customBlocks: SFCBlock[]
|
customBlocks: SFCBlock[]
|
||||||
cssVars: string[]
|
cssVars: string[]
|
||||||
|
// whether the SFC uses :slotted() modifier.
|
||||||
|
// this is used as a compiler optimization hint.
|
||||||
|
slotted: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SFCParseResult {
|
export interface SFCParseResult {
|
||||||
@ -100,7 +103,8 @@ export function parse(
|
|||||||
scriptSetup: null,
|
scriptSetup: null,
|
||||||
styles: [],
|
styles: [],
|
||||||
customBlocks: [],
|
customBlocks: [],
|
||||||
cssVars: []
|
cssVars: [],
|
||||||
|
slotted: false
|
||||||
}
|
}
|
||||||
|
|
||||||
const errors: (CompilerError | SyntaxError)[] = []
|
const errors: (CompilerError | SyntaxError)[] = []
|
||||||
@ -231,6 +235,10 @@ export function parse(
|
|||||||
warnExperimental(`v-bind() CSS variable injection`, 231)
|
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 = {
|
const result = {
|
||||||
descriptor,
|
descriptor,
|
||||||
errors
|
errors
|
||||||
|
@ -6,7 +6,7 @@ describe('ssr: <slot>', () => {
|
|||||||
"const { ssrRenderSlot: _ssrRenderSlot } = require(\\"@vue/server-renderer\\")
|
"const { ssrRenderSlot: _ssrRenderSlot } = require(\\"@vue/server-renderer\\")
|
||||||
|
|
||||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
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\\")
|
"const { ssrRenderSlot: _ssrRenderSlot } = require(\\"@vue/server-renderer\\")
|
||||||
|
|
||||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
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\\")
|
"const { ssrRenderSlot: _ssrRenderSlot } = require(\\"@vue/server-renderer\\")
|
||||||
|
|
||||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
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\\", {
|
_ssrRenderSlot(_ctx.$slots, \\"foo\\", {
|
||||||
p: 1,
|
p: 1,
|
||||||
bar: \\"2\\"
|
bar: \\"2\\"
|
||||||
}, null, _push, _parent, null)
|
}, null, _push, _parent)
|
||||||
}"
|
}"
|
||||||
`)
|
`)
|
||||||
})
|
})
|
||||||
@ -53,7 +53,7 @@ describe('ssr: <slot>', () => {
|
|||||||
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
return function ssrRender(_ctx, _push, _parent, _attrs) {
|
||||||
_ssrRenderSlot(_ctx.$slots, \\"default\\", {}, () => {
|
_ssrRenderSlot(_ctx.$slots, \\"default\\", {}, () => {
|
||||||
_push(\`some \${_ssrInterpolate(_ctx.fallback)} content\`)
|
_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 () => {
|
test('with forwarded scopeId', async () => {
|
||||||
expect(
|
expect(
|
||||||
compile(`<Comp><slot/></Comp>`, {
|
compile(`<Comp><slot/></Comp>`, {
|
||||||
@ -90,7 +105,7 @@ describe('ssr: <slot>', () => {
|
|||||||
_ssrRenderSlot(_ctx.$slots, \\"default\\", {}, null, _push, _parent, \\"hello-s\\" + _scopeId)
|
_ssrRenderSlot(_ctx.$slots, \\"default\\", {}, null, _push, _parent, \\"hello-s\\" + _scopeId)
|
||||||
} else {
|
} else {
|
||||||
return [
|
return [
|
||||||
_renderSlot(_ctx.$slots, \\"default\\")
|
_renderSlot(_ctx.$slots, \\"default\\", {}, undefined, true)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
@ -15,18 +15,25 @@ import {
|
|||||||
export const ssrTransformSlotOutlet: NodeTransform = (node, context) => {
|
export const ssrTransformSlotOutlet: NodeTransform = (node, context) => {
|
||||||
if (isSlotOutlet(node)) {
|
if (isSlotOutlet(node)) {
|
||||||
const { slotName, slotProps } = processSlotOutlet(node, context)
|
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(
|
node.ssrCodegenNode = createCallExpression(
|
||||||
context.helper(SSR_RENDER_SLOT),
|
context.helper(SSR_RENDER_SLOT),
|
||||||
[
|
args
|
||||||
`_ctx.$slots`,
|
|
||||||
slotName,
|
|
||||||
slotProps || `{}`,
|
|
||||||
// fallback content placeholder. will be replaced in the process phase
|
|
||||||
`null`,
|
|
||||||
`_push`,
|
|
||||||
`_parent`,
|
|
||||||
context.scopeId ? `"${context.scopeId}-s"` : `null`
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -45,11 +52,12 @@ export function ssrProcessSlotOutlet(
|
|||||||
renderCall.arguments[3] = fallbackRenderFn
|
renderCall.arguments[3] = fallbackRenderFn
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forwarded <slot/>. Add slot scope id
|
// Forwarded <slot/>. Merge slot scope ids
|
||||||
if (context.withSlotScopeId) {
|
if (context.withSlotScopeId) {
|
||||||
const scopeId = renderCall.arguments[6] as string
|
const slotScopeId = renderCall.arguments[6]
|
||||||
renderCall.arguments[6] =
|
renderCall.arguments[6] = slotScopeId
|
||||||
scopeId === `null` ? `_scopeId` : `${scopeId} + _scopeId`
|
? `${slotScopeId as string} + _scopeId`
|
||||||
|
: `_scopeId`
|
||||||
}
|
}
|
||||||
|
|
||||||
context.pushStatement(node.ssrCodegenNode!)
|
context.pushStatement(node.ssrCodegenNode!)
|
||||||
|
@ -40,7 +40,7 @@ describe('scopeId runtime support', () => {
|
|||||||
const Child = {
|
const Child = {
|
||||||
__scopeId: 'child',
|
__scopeId: 'child',
|
||||||
render(this: any) {
|
render(this: any) {
|
||||||
return h('div', renderSlot(this.$slots, 'default'))
|
return h('div', renderSlot(this.$slots, 'default', {}, undefined, true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const Child2 = {
|
const Child2 = {
|
||||||
@ -92,7 +92,9 @@ describe('scopeId runtime support', () => {
|
|||||||
render(this: any) {
|
render(this: any) {
|
||||||
// <Wrapper><slot/></Wrapper>
|
// <Wrapper><slot/></Wrapper>
|
||||||
return h(Wrapper, null, {
|
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)
|
render(h(Root), root)
|
||||||
expect(serializeInner(root)).toBe(
|
expect(serializeInner(root)).toBe(
|
||||||
`<div class="wrapper" wrapper slotted root>` +
|
`<div class="wrapper" wrapper slotted root>` +
|
||||||
`<div root wrapper-s slotted-s>hoisted</div>` +
|
`<div root slotted-s>hoisted</div>` +
|
||||||
`<div root wrapper-s slotted-s>dynamic</div>` +
|
`<div root slotted-s>dynamic</div>` +
|
||||||
`</div>`
|
`</div>`
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -144,9 +146,9 @@ describe('scopeId runtime support', () => {
|
|||||||
render(h(Root2), root2)
|
render(h(Root2), root2)
|
||||||
expect(serializeInner(root2)).toBe(
|
expect(serializeInner(root2)).toBe(
|
||||||
`<div class="wrapper" wrapper slotted root>` +
|
`<div class="wrapper" wrapper slotted root>` +
|
||||||
`<div class="wrapper" wrapper root wrapper-s slotted-s>` +
|
`<div class="wrapper" wrapper root slotted-s>` +
|
||||||
`<div root wrapper-s>hoisted</div>` +
|
`<div root>hoisted</div>` +
|
||||||
`<div root wrapper-s>dynamic</div>` +
|
`<div root>dynamic</div>` +
|
||||||
`</div>` +
|
`</div>` +
|
||||||
`</div>`
|
`</div>`
|
||||||
)
|
)
|
||||||
|
@ -25,7 +25,8 @@ export function renderSlot(
|
|||||||
props: Data = {},
|
props: Data = {},
|
||||||
// this is not a user-facing function, so the fallback is always generated by
|
// 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
|
// the compiler and guaranteed to be a function returning an array
|
||||||
fallback?: () => VNodeArrayChildren
|
fallback?: () => VNodeArrayChildren,
|
||||||
|
hasSlotted?: boolean
|
||||||
): VNode {
|
): VNode {
|
||||||
let slot = slots[name]
|
let slot = slots[name]
|
||||||
|
|
||||||
@ -53,8 +54,7 @@ export function renderSlot(
|
|||||||
? PatchFlags.STABLE_FRAGMENT
|
? PatchFlags.STABLE_FRAGMENT
|
||||||
: PatchFlags.BAIL
|
: PatchFlags.BAIL
|
||||||
)
|
)
|
||||||
// TODO (optimization) only add slot scope id if :slotted is used
|
if (hasSlotted && rendered.scopeId) {
|
||||||
if (rendered.scopeId) {
|
|
||||||
rendered.slotScopeIds = [rendered.scopeId + '-s']
|
rendered.slotScopeIds = [rendered.scopeId + '-s']
|
||||||
}
|
}
|
||||||
isRenderingCompiledSlot--
|
isRenderingCompiledSlot--
|
||||||
|
Loading…
Reference in New Issue
Block a user