mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-25 15:15:19 +00:00
perf(nuxt): tree-shake client-only components from ssr bundle (#5750)
Co-authored-by: Pooya Parsa <pooya@pi0.io>
This commit is contained in:
parent
433298f1e5
commit
df04a665ce
@ -174,6 +174,10 @@ Use a slot as fallback until `<ClientOnly>` is mounted on client side.
|
||||
</template>
|
||||
```
|
||||
|
||||
::alert{type=warning}
|
||||
Make sure not to _nest_ `<ClientOnly>` components or other client-only components. Nuxt performs an optimization to remove the contents of these components from the server-side render, which can break in this case.
|
||||
::
|
||||
|
||||
## Library Authors
|
||||
|
||||
Making Vue component libraries with automatic tree-shaking and component registration is super easy ✨
|
||||
|
@ -5,6 +5,7 @@ import type { Component, ComponentsDir, ComponentsOptions } from '@nuxt/schema'
|
||||
import { componentsPluginTemplate, componentsTemplate, componentsTypeTemplate } from './templates'
|
||||
import { scanComponents } from './scan'
|
||||
import { loaderPlugin } from './loader'
|
||||
import { TreeShakeTemplatePlugin } from './tree-shake'
|
||||
|
||||
const isPureObjectOrString = (val: any) => (!Array.isArray(val) && typeof val === 'object') || typeof val === 'string'
|
||||
const isDirectory = (p: string) => { try { return statSync(p).isDirectory() } catch (_e) { return false } }
|
||||
@ -136,6 +137,7 @@ export default defineNuxtModule<ComponentsOptions>({
|
||||
getComponents,
|
||||
mode: isClient ? 'client' : 'server'
|
||||
}))
|
||||
config.plugins.push(TreeShakeTemplatePlugin.vite({ sourcemap: nuxt.options.sourcemap, getComponents }))
|
||||
})
|
||||
nuxt.hook('webpack:config', (configs) => {
|
||||
configs.forEach((config) => {
|
||||
@ -145,6 +147,7 @@ export default defineNuxtModule<ComponentsOptions>({
|
||||
getComponents,
|
||||
mode: config.name === 'client' ? 'client' : 'server'
|
||||
}))
|
||||
config.plugins.push(TreeShakeTemplatePlugin.webpack({ sourcemap: nuxt.options.sourcemap, getComponents }))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
48
packages/nuxt/src/components/tree-shake.ts
Normal file
48
packages/nuxt/src/components/tree-shake.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { parseURL } from 'ufo'
|
||||
import MagicString from 'magic-string'
|
||||
import { createUnplugin } from 'unplugin'
|
||||
import type { Component } from '@nuxt/schema'
|
||||
|
||||
interface TreeShakeTemplatePluginOptions {
|
||||
sourcemap?: boolean
|
||||
getComponents(): Component[]
|
||||
}
|
||||
|
||||
export const TreeShakeTemplatePlugin = createUnplugin((options: TreeShakeTemplatePluginOptions) => {
|
||||
const regexpMap = new WeakMap<Component[], RegExp>()
|
||||
return {
|
||||
name: 'nuxt:tree-shake-template',
|
||||
enforce: 'pre',
|
||||
transformInclude (id) {
|
||||
const { pathname } = parseURL(decodeURIComponent(pathToFileURL(id).href))
|
||||
return pathname.endsWith('.vue')
|
||||
},
|
||||
transform (code, id) {
|
||||
const components = options.getComponents()
|
||||
|
||||
if (!regexpMap.has(components)) {
|
||||
const clientOnlyComponents = components
|
||||
.filter(c => c.mode === 'client' && !components.some(other => other.mode !== 'client' && other.pascalName === c.pascalName))
|
||||
.map(c => `${c.pascalName}|${c.kebabName}`)
|
||||
.concat('ClientOnly|client-only')
|
||||
.map(component => `<(${component})[^>]*>[\\s\\S]*?<\\/(${component})>`)
|
||||
|
||||
regexpMap.set(components, new RegExp(`(${clientOnlyComponents.join('|')})`, 'g'))
|
||||
}
|
||||
|
||||
const COMPONENTS_RE = regexpMap.get(components)
|
||||
const s = new MagicString(code)
|
||||
|
||||
// Do not render client-only slots on SSR, but preserve attributes
|
||||
s.replace(COMPONENTS_RE, r => r.replace(/<([^ >]*)[ >][\s\S]*$/, '<$1 />'))
|
||||
|
||||
if (s.hasChanged()) {
|
||||
return {
|
||||
code: s.toString(),
|
||||
map: options.sourcemap && s.generateMap({ source: id, includeContent: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
12
test/fixtures/basic/pages/client.vue
vendored
12
test/fixtures/basic/pages/client.vue
vendored
@ -12,5 +12,17 @@ onBeforeUnmount(() => import('~/components/BreaksServer'))
|
||||
<template>
|
||||
<div>
|
||||
This page should not crash when rendered.
|
||||
<ClientOnly class="something">
|
||||
test
|
||||
<BreaksServer />
|
||||
<BreaksServer>Some slot content</BreaksServer>
|
||||
</ClientOnly>
|
||||
This should render.
|
||||
<div>
|
||||
<ClientOnly class="another">
|
||||
test
|
||||
<BreaksServer />
|
||||
</ClientOnly>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
Loading…
Reference in New Issue
Block a user