perf(nuxt): don't include side-effects from #components (#19008)

This commit is contained in:
Daniel Roe 2023-02-16 15:00:40 +00:00 committed by GitHub
parent c8b49a3253
commit 1e8b27f36c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 91 additions and 7 deletions

View File

@ -72,7 +72,7 @@ export const componentsTemplate: NuxtTemplate<ComponentsTemplateContext> = {
} else { } else {
definitions.push(genExport(c.filePath, [{ name: c.export, as: c.pascalName }])) definitions.push(genExport(c.filePath, [{ name: c.export, as: c.pascalName }]))
} }
definitions.push(`export const Lazy${c.pascalName} = defineAsyncComponent(${genDynamicImport(c.filePath, { comment })}.then(c => ${isClient ? `createClientOnly(${exp})` : exp}))`) definitions.push(`export const Lazy${c.pascalName} = /* #__PURE__ */ defineAsyncComponent(${genDynamicImport(c.filePath, { comment })}.then(c => ${isClient ? `createClientOnly(${exp})` : exp}))`)
return definitions return definitions
}) })
return [ return [
@ -96,7 +96,7 @@ export const componentsIslandsTemplate: NuxtTemplate<ComponentsTemplateContext>
(c) => { (c) => {
const exp = c.export === 'default' ? 'c.default || c' : `c['${c.export}']` const exp = c.export === 'default' ? 'c.default || c' : `c['${c.export}']`
const comment = createImportMagicComments(c) const comment = createImportMagicComments(c)
return `export const ${c.pascalName} = defineAsyncComponent(${genDynamicImport(c.filePath, { comment })}.then(c => ${exp}))` return `export const ${c.pascalName} = /* #__PURE__ */ defineAsyncComponent(${genDynamicImport(c.filePath, { comment })}.then(c => ${exp}))`
} }
).join('\n') ).join('\n')
} }

View File

@ -46,6 +46,7 @@
"postcss-url": "^10.1.3", "postcss-url": "^10.1.3",
"rollup": "^3.15.0", "rollup": "^3.15.0",
"rollup-plugin-visualizer": "^5.9.0", "rollup-plugin-visualizer": "^5.9.0",
"strip-literal": "^1.0.1",
"ufo": "^1.0.1", "ufo": "^1.0.1",
"unplugin": "^1.1.0", "unplugin": "^1.1.0",
"vite": "~4.1.1", "vite": "~4.1.1",

View File

@ -15,6 +15,7 @@ import { chunkErrorPlugin } from './plugins/chunk-error'
import type { ViteBuildContext, ViteOptions } from './vite' import type { ViteBuildContext, ViteOptions } from './vite'
import { devStyleSSRPlugin } from './plugins/dev-ssr-css' import { devStyleSSRPlugin } from './plugins/dev-ssr-css'
import { runtimePathsPlugin } from './plugins/paths' import { runtimePathsPlugin } from './plugins/paths'
import { pureAnnotationsPlugin } from './plugins/pure-annotations'
import { viteNodePlugin } from './vite-node' import { viteNodePlugin } from './vite-node'
export async function buildClient (ctx: ViteBuildContext) { export async function buildClient (ctx: ViteBuildContext) {
@ -69,7 +70,11 @@ export async function buildClient (ctx: ViteBuildContext) {
runtimePathsPlugin({ runtimePathsPlugin({
sourcemap: ctx.nuxt.options.sourcemap.client sourcemap: ctx.nuxt.options.sourcemap.client
}), }),
viteNodePlugin(ctx) viteNodePlugin(ctx),
pureAnnotationsPlugin.vite({
sourcemap: ctx.nuxt.options.sourcemap.client,
functions: ['defineComponent', 'defineAsyncComponent', 'defineNuxtLink', 'createClientOnly']
})
], ],
appType: 'custom', appType: 'custom',
server: { server: {

View File

@ -0,0 +1,52 @@
import { pathToFileURL } from 'node:url'
import MagicString from 'magic-string'
import { parseQuery, parseURL } from 'ufo'
import { createUnplugin } from 'unplugin'
import { stripLiteral } from 'strip-literal'
export interface PureAnnotationsOptions {
sourcemap: boolean
functions: string[]
}
export const pureAnnotationsPlugin = createUnplugin((options: PureAnnotationsOptions) => {
const FUNCTION_RE = new RegExp(`(?<!\\/\\* #__PURE__ \\*\\/ )\\b(${options.functions.join('|')})\\s*\\(`, 'g')
const FUNCTION_RE_SINGLE = new RegExp(`(?<!\\/\\* #__PURE__ \\*\\/ )\\b(${options.functions.join('|')})\\s*\\(`)
return {
name: 'nuxt:pure-annotations',
enforce: 'post',
transformInclude (id) {
const { pathname, search } = parseURL(decodeURIComponent(pathToFileURL(id).href))
const { type } = parseQuery(search)
// vue files
if (pathname.endsWith('.vue') && (type === 'script' || !search)) {
return true
}
// js files
if (pathname.match(/\.((c|m)?j|t)sx?$/g)) {
return true
}
},
transform (code, id) {
if (!FUNCTION_RE_SINGLE.test(code)) { return }
const s = new MagicString(code)
const strippedCode = stripLiteral(code)
for (const match of strippedCode.matchAll(FUNCTION_RE)) {
s.overwrite(match.index!, match.index! + match[0].length, '/* #__PURE__ */ ' + match[0])
}
if (s.hasChanged()) {
return {
code: s.toString(),
map: options.sourcemap
? s.generateMap({ source: id, includeContent: true })
: undefined
}
}
}
}
})

View File

@ -22,6 +22,20 @@ export function ssrStylesPlugin (options: SSRStylePluginOptions): Plugin {
return { return {
name: 'ssr-styles', name: 'ssr-styles',
resolveId: {
order: 'pre',
async handler (id, importer, options) {
if (!id.endsWith('.vue')) { return }
const res = await this.resolve(id, importer, { ...options, skipSelf: true })
if (res) {
return {
...res,
moduleSideEffects: false
}
}
}
},
generateBundle (outputOptions) { generateBundle (outputOptions) {
const emitted: Record<string, string> = {} const emitted: Record<string, string> = {}
for (const file in cssMap) { for (const file in cssMap) {

View File

@ -8,6 +8,7 @@ import type { ViteBuildContext, ViteOptions } from './vite'
import { cacheDirPlugin } from './plugins/cache-dir' import { cacheDirPlugin } from './plugins/cache-dir'
import { initViteNodeServer } from './vite-node' import { initViteNodeServer } from './vite-node'
import { ssrStylesPlugin } from './plugins/ssr-styles' import { ssrStylesPlugin } from './plugins/ssr-styles'
import { pureAnnotationsPlugin } from './plugins/pure-annotations'
import { writeManifest } from './manifest' import { writeManifest } from './manifest'
import { transpile } from './utils/transpile' import { transpile } from './utils/transpile'
@ -110,7 +111,11 @@ export async function buildServer (ctx: ViteBuildContext) {
plugins: [ plugins: [
cacheDirPlugin(ctx.nuxt.options.rootDir, 'server'), cacheDirPlugin(ctx.nuxt.options.rootDir, 'server'),
vuePlugin(ctx.config.vue), vuePlugin(ctx.config.vue),
viteJsxPlugin(ctx.config.vueJsx) viteJsxPlugin(ctx.config.vueJsx),
pureAnnotationsPlugin.vite({
sourcemap: ctx.nuxt.options.sourcemap.server,
functions: ['defineComponent', 'defineAsyncComponent', 'defineNuxtLink', 'createClientOnly']
})
] ]
} as ViteOptions) } as ViteOptions)

View File

@ -614,6 +614,7 @@ importers:
postcss-url: ^10.1.3 postcss-url: ^10.1.3
rollup: ^3.15.0 rollup: ^3.15.0
rollup-plugin-visualizer: ^5.9.0 rollup-plugin-visualizer: ^5.9.0
strip-literal: ^1.0.1
ufo: ^1.0.1 ufo: ^1.0.1
unbuild: ^1.1.1 unbuild: ^1.1.1
unplugin: ^1.1.0 unplugin: ^1.1.0
@ -650,6 +651,7 @@ importers:
postcss-url: 10.1.3_postcss@8.4.21 postcss-url: 10.1.3_postcss@8.4.21
rollup: 3.15.0 rollup: 3.15.0
rollup-plugin-visualizer: 5.9.0_rollup@3.15.0 rollup-plugin-visualizer: 5.9.0_rollup@3.15.0
strip-literal: 1.0.1
ufo: 1.0.1 ufo: 1.0.1
unplugin: 1.1.0 unplugin: 1.1.0
vite: 4.1.1 vite: 4.1.1

View File

@ -29,7 +29,7 @@ describe.skipIf(isWindows)('minimal nuxt application', () => {
expect(stats.client.totalBytes).toBeLessThan(108000) expect(stats.client.totalBytes).toBeLessThan(108000)
expect(stats.client.files.map(f => f.replace(/\..*\.js/, '.js'))).toMatchInlineSnapshot(` expect(stats.client.files.map(f => f.replace(/\..*\.js/, '.js'))).toMatchInlineSnapshot(`
[ [
"_nuxt/app.js", "_nuxt/_plugin-vue_export-helper.js",
"_nuxt/entry.js", "_nuxt/entry.js",
"_nuxt/error-404.js", "_nuxt/error-404.js",
"_nuxt/error-500.js", "_nuxt/error-500.js",
@ -40,7 +40,7 @@ describe.skipIf(isWindows)('minimal nuxt application', () => {
it('default server bundle size', async () => { it('default server bundle size', async () => {
stats.server = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir) stats.server = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
expect(stats.server.totalBytes).toBeLessThan(90200) expect(stats.server.totalBytes).toBeLessThan(92000)
const modules = await analyzeSizes('node_modules/**/*', serverDir) const modules = await analyzeSizes('node_modules/**/*', serverDir)
expect(modules.totalBytes).toBeLessThan(2700000) expect(modules.totalBytes).toBeLessThan(2700000)

View File

@ -1,3 +1,8 @@
<script setup lang="ts">
import { componentNames } from '#components'
console.log(componentNames)
</script>
<template> <template>
<div>Hello World!</div> <div>Hello World!</div>
</template> </template>