mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-29 09:02:03 +00:00
perf(vite): remove duplicate css links from rendered page when inlined (#7264)
This commit is contained in:
parent
2bb898fa98
commit
577a7b681e
@ -14,7 +14,6 @@ import type { OutputOptions } from 'rollup'
|
|||||||
import { cacheDirPlugin } from './plugins/cache-dir'
|
import { cacheDirPlugin } from './plugins/cache-dir'
|
||||||
import { wpfs } from './utils/wpfs'
|
import { wpfs } from './utils/wpfs'
|
||||||
import type { ViteBuildContext, ViteOptions } from './vite'
|
import type { ViteBuildContext, ViteOptions } from './vite'
|
||||||
import { writeManifest } from './manifest'
|
|
||||||
import { devStyleSSRPlugin } from './plugins/dev-ssr-css'
|
import { devStyleSSRPlugin } from './plugins/dev-ssr-css'
|
||||||
import { viteNodePlugin } from './vite-node'
|
import { viteNodePlugin } from './vite-node'
|
||||||
|
|
||||||
@ -140,6 +139,4 @@ export async function buildClient (ctx: ViteBuildContext) {
|
|||||||
await ctx.nuxt.callHook('build:resources', wpfs)
|
await ctx.nuxt.callHook('build:resources', wpfs)
|
||||||
logger.info(`Client built in ${Date.now() - start}ms`)
|
logger.info(`Client built in ${Date.now() - start}ms`)
|
||||||
}
|
}
|
||||||
|
|
||||||
await writeManifest(ctx)
|
|
||||||
}
|
}
|
||||||
|
@ -9,11 +9,12 @@ import { isCSS } from '../utils'
|
|||||||
|
|
||||||
interface SSRStylePluginOptions {
|
interface SSRStylePluginOptions {
|
||||||
srcDir: string
|
srcDir: string
|
||||||
|
chunksWithInlinedCSS: Set<string>
|
||||||
shouldInline?: (id?: string) => boolean
|
shouldInline?: (id?: string) => boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ssrStylesPlugin (options: SSRStylePluginOptions): Plugin {
|
export function ssrStylesPlugin (options: SSRStylePluginOptions): Plugin {
|
||||||
const cssMap: Record<string, string[]> = {}
|
const cssMap: Record<string, { files: string[], inBundle: boolean }> = {}
|
||||||
const idRefMap: Record<string, string> = {}
|
const idRefMap: Record<string, string> = {}
|
||||||
const globalStyles = new Set<string>()
|
const globalStyles = new Set<string>()
|
||||||
|
|
||||||
@ -24,7 +25,9 @@ export function ssrStylesPlugin (options: SSRStylePluginOptions): Plugin {
|
|||||||
generateBundle (outputOptions) {
|
generateBundle (outputOptions) {
|
||||||
const emitted: Record<string, string> = {}
|
const emitted: Record<string, string> = {}
|
||||||
for (const file in cssMap) {
|
for (const file in cssMap) {
|
||||||
if (!cssMap[file].length) { continue }
|
const { files, inBundle } = cssMap[file]
|
||||||
|
// File has been tree-shaken out of build (or there are no styles to inline)
|
||||||
|
if (!files.length || !inBundle) { continue }
|
||||||
|
|
||||||
const base = typeof outputOptions.assetFileNames === 'string'
|
const base = typeof outputOptions.assetFileNames === 'string'
|
||||||
? outputOptions.assetFileNames
|
? outputOptions.assetFileNames
|
||||||
@ -38,14 +41,19 @@ export function ssrStylesPlugin (options: SSRStylePluginOptions): Plugin {
|
|||||||
type: 'asset',
|
type: 'asset',
|
||||||
name: `${filename(file)}-styles.mjs`,
|
name: `${filename(file)}-styles.mjs`,
|
||||||
source: [
|
source: [
|
||||||
...cssMap[file].map((css, i) => `import style_${i} from './${relative(dirname(base), this.getFileName(css))}';`),
|
...files.map((css, i) => `import style_${i} from './${relative(dirname(base), this.getFileName(css))}';`),
|
||||||
`export default [${cssMap[file].map((_, i) => `style_${i}`).join(', ')}]`
|
`export default [${files.map((_, i) => `style_${i}`).join(', ')}]`
|
||||||
].join('\n')
|
].join('\n')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const globalStylesArray = Array.from(globalStyles).map(css => idRefMap[css] && this.getFileName(idRefMap[css])).filter(Boolean)
|
const globalStylesArray = Array.from(globalStyles).map(css => idRefMap[css] && this.getFileName(idRefMap[css])).filter(Boolean)
|
||||||
|
|
||||||
|
for (const key in emitted) {
|
||||||
|
// Track the chunks we are inlining CSS for so we can omit including links to the .css files
|
||||||
|
options.chunksWithInlinedCSS.add(key)
|
||||||
|
}
|
||||||
|
|
||||||
this.emitFile({
|
this.emitFile({
|
||||||
type: 'asset',
|
type: 'asset',
|
||||||
fileName: 'styles.mjs',
|
fileName: 'styles.mjs',
|
||||||
@ -61,13 +69,23 @@ export function ssrStylesPlugin (options: SSRStylePluginOptions): Plugin {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
renderChunk (_code, chunk) {
|
renderChunk (_code, chunk) {
|
||||||
if (!chunk.isEntry) { return null }
|
if (!chunk.facadeModuleId) { return null }
|
||||||
|
const id = relativeToSrcDir(chunk.facadeModuleId)
|
||||||
|
for (const file in chunk.modules) {
|
||||||
|
const relativePath = relativeToSrcDir(file)
|
||||||
|
if (relativePath in cssMap) {
|
||||||
|
cssMap[relativePath].inBundle = cssMap[relativePath].inBundle ?? !!id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunk.isEntry) {
|
||||||
// Entry
|
// Entry
|
||||||
for (const mod in chunk.modules) {
|
for (const mod in chunk.modules) {
|
||||||
if (isCSS(mod) && !mod.includes('&used')) {
|
if (isCSS(mod) && !mod.includes('&used')) {
|
||||||
globalStyles.add(relativeToSrcDir(mod))
|
globalStyles.add(relativeToSrcDir(mod))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
async transform (code, id) {
|
async transform (code, id) {
|
||||||
@ -77,7 +95,7 @@ export function ssrStylesPlugin (options: SSRStylePluginOptions): Plugin {
|
|||||||
if (options.shouldInline && !options.shouldInline(id)) { return }
|
if (options.shouldInline && !options.shouldInline(id)) { return }
|
||||||
|
|
||||||
const relativeId = relativeToSrcDir(id)
|
const relativeId = relativeToSrcDir(id)
|
||||||
cssMap[relativeId] = cssMap[relativeId] || []
|
cssMap[relativeId] = cssMap[relativeId] || { files: [] }
|
||||||
|
|
||||||
let styleCtr = 0
|
let styleCtr = 0
|
||||||
for (const i of findStaticImports(code)) {
|
for (const i of findStaticImports(code)) {
|
||||||
@ -94,7 +112,7 @@ export function ssrStylesPlugin (options: SSRStylePluginOptions): Plugin {
|
|||||||
})
|
})
|
||||||
|
|
||||||
idRefMap[relativeToSrcDir(resolved.id)] = ref
|
idRefMap[relativeToSrcDir(resolved.id)] = ref
|
||||||
cssMap[relativeId].push(ref)
|
cssMap[relativeId].files.push(ref)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import { wpfs } from './utils/wpfs'
|
|||||||
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 { writeManifest } from './manifest'
|
||||||
|
|
||||||
export async function buildServer (ctx: ViteBuildContext) {
|
export async function buildServer (ctx: ViteBuildContext) {
|
||||||
const useAsyncEntry = ctx.nuxt.options.experimental.asyncEntry ||
|
const useAsyncEntry = ctx.nuxt.options.experimental.asyncEntry ||
|
||||||
@ -113,12 +114,35 @@ export async function buildServer (ctx: ViteBuildContext) {
|
|||||||
} as ViteOptions)
|
} as ViteOptions)
|
||||||
|
|
||||||
if (ctx.nuxt.options.experimental.inlineSSRStyles) {
|
if (ctx.nuxt.options.experimental.inlineSSRStyles) {
|
||||||
|
const chunksWithInlinedCSS = new Set<string>()
|
||||||
serverConfig.plugins!.push(ssrStylesPlugin({
|
serverConfig.plugins!.push(ssrStylesPlugin({
|
||||||
srcDir: ctx.nuxt.options.srcDir,
|
srcDir: ctx.nuxt.options.srcDir,
|
||||||
|
chunksWithInlinedCSS,
|
||||||
shouldInline: typeof ctx.nuxt.options.experimental.inlineSSRStyles === 'function'
|
shouldInline: typeof ctx.nuxt.options.experimental.inlineSSRStyles === 'function'
|
||||||
? ctx.nuxt.options.experimental.inlineSSRStyles
|
? ctx.nuxt.options.experimental.inlineSSRStyles
|
||||||
: undefined
|
: undefined
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
// Remove CSS entries for files that will have inlined styles
|
||||||
|
ctx.nuxt.hook('build:manifest', (manifest) => {
|
||||||
|
for (const key in manifest) {
|
||||||
|
const entry = manifest[key]
|
||||||
|
const shouldRemoveCSS = chunksWithInlinedCSS.has(key)
|
||||||
|
if (shouldRemoveCSS) {
|
||||||
|
entry.css = []
|
||||||
|
}
|
||||||
|
// Add entry CSS as prefetch (non-blocking)
|
||||||
|
if (entry.isEntry) {
|
||||||
|
manifest[key + '-css'] = {
|
||||||
|
file: '',
|
||||||
|
css: entry.css
|
||||||
|
}
|
||||||
|
entry.css = []
|
||||||
|
entry.dynamicImports = entry.dynamicImports || []
|
||||||
|
entry.dynamicImports.push(key + '-css')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add type-checking
|
// Add type-checking
|
||||||
@ -140,6 +164,8 @@ export async function buildServer (ctx: ViteBuildContext) {
|
|||||||
const start = Date.now()
|
const start = Date.now()
|
||||||
logger.info('Building server...')
|
logger.info('Building server...')
|
||||||
await vite.build(serverConfig)
|
await vite.build(serverConfig)
|
||||||
|
// Write production client manifest
|
||||||
|
await writeManifest(ctx)
|
||||||
await onBuild()
|
await onBuild()
|
||||||
logger.success(`Server built in ${Date.now() - start}ms`)
|
logger.success(`Server built in ${Date.now() - start}ms`)
|
||||||
return
|
return
|
||||||
@ -150,6 +176,9 @@ export async function buildServer (ctx: ViteBuildContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Write dev client manifest
|
||||||
|
await writeManifest(ctx)
|
||||||
|
|
||||||
// Start development server
|
// Start development server
|
||||||
const viteServer = await vite.createServer(serverConfig)
|
const viteServer = await vite.createServer(serverConfig)
|
||||||
ctx.ssrServer = viteServer
|
ctx.ssrServer = viteServer
|
||||||
|
@ -2,7 +2,7 @@ import { fileURLToPath } from 'node:url'
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import { joinURL } from 'ufo'
|
import { joinURL } from 'ufo'
|
||||||
// import { isWindows } from 'std-env'
|
// import { isWindows } from 'std-env'
|
||||||
import { setup, fetch, $fetch, startServer } from '@nuxt/test-utils'
|
import { setup, fetch, $fetch, startServer, createPage } from '@nuxt/test-utils'
|
||||||
// eslint-disable-next-line import/order
|
// eslint-disable-next-line import/order
|
||||||
import { expectNoClientErrors, renderPage } from './utils'
|
import { expectNoClientErrors, renderPage } from './utils'
|
||||||
|
|
||||||
@ -384,8 +384,23 @@ if (!process.env.NUXT_TEST_DEV && !process.env.TEST_WITH_WEBPACK) {
|
|||||||
expect(html).toContain(style)
|
expect(html).toContain(style)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
it.todo('does not render style hints for inlined styles')
|
|
||||||
it.todo('renders client-only styles?', async () => {
|
it('only renders prefetch for entry styles', async () => {
|
||||||
|
const html: string = await $fetch('/styles')
|
||||||
|
expect(html.match(/<link [^>]*href="[^"]*\.css">/)?.map(m => m.replace(/\.[^.]*\.css/, '.css'))).toMatchInlineSnapshot(`
|
||||||
|
[
|
||||||
|
"<link rel=\\"prefetch stylesheet\\" href=\\"/_nuxt/entry.css\\">",
|
||||||
|
]
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('still downloads client-only styles', async () => {
|
||||||
|
const page = await createPage('/styles')
|
||||||
|
await page.waitForLoadState('networkidle')
|
||||||
|
expect(await page.$eval('.client-only-css', e => getComputedStyle(e).color)).toBe('rgb(50, 50, 50)')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.todo('renders client-only styles only', async () => {
|
||||||
const html = await $fetch('/styles')
|
const html = await $fetch('/styles')
|
||||||
expect(html).toContain('{--client-only:"client-only"}')
|
expect(html).toContain('{--client-only:"client-only"}')
|
||||||
})
|
})
|
||||||
|
@ -11,7 +11,9 @@ export default defineNuxtComponent({
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div>client only script component {{ foo }}</div>
|
<div class="client-only-css">
|
||||||
|
client only script component {{ foo }}
|
||||||
|
</div>
|
||||||
<slot name="test" />
|
<slot name="test" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -21,3 +23,9 @@ export default defineNuxtComponent({
|
|||||||
--client-only: 'client-only';
|
--client-only: 'client-only';
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.client-only-css {
|
||||||
|
color: rgb(50, 50, 50);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
1
test/fixtures/basic/nuxt.config.ts
vendored
1
test/fixtures/basic/nuxt.config.ts
vendored
@ -36,6 +36,7 @@ export default defineNuxtConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
|
inlineSSRStyles: id => !id.includes('assets.vue'),
|
||||||
reactivityTransform: true,
|
reactivityTransform: true,
|
||||||
treeshakeClientOnly: true
|
treeshakeClientOnly: true
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user