fix: improve critical css generation (#770)

This commit is contained in:
Valerio Ageno
2025-05-11 19:11:30 +02:00
committed by GitHub
parent eef2cd26e0
commit 3027b828a8
11 changed files with 137 additions and 82 deletions
@@ -207,6 +207,7 @@ export async function routeGenerator(
rustHandlersNodes.includes(node.path || '')
? 'hasHandler: true'
: '',
`filePath: '${node.path || '/'}'`,
]
.filter(Boolean)
.join(',')}
+89 -71
View File
@@ -46,6 +46,94 @@ const isCssUrlWithoutSideEffects = (url: string): boolean => {
return false
}
const normalizePath = (modulePath: string): string => {
return modulePath.startsWith('node_modules')
? path.join(process.cwd(), modulePath)
: modulePath
}
export const getStylesForModule = async (
viteDevServer: ViteDevServer,
moduleUrl: string,
/**
* All the CSS modules are preloaded and saved in this manifest
*/
cssModulesManifest: Record<string, string>,
): Promise<string | undefined> => {
const styles: Record<string, string> = {}
const deps: Set<ModuleNode> = new Set()
const moduleFilePath = normalizePath(moduleUrl)
try {
let node: ModuleNode | undefined =
await viteDevServer.moduleGraph.getModuleByUrl(moduleFilePath)
// If the module is only present in the client module graph, the module
// won't have been found on the first request to the server. If so, we
// request the module so it's in the module graph, then try again.
if (!node) {
try {
await viteDevServer.transformRequest(moduleFilePath)
} catch (err) {
console.error(err)
}
node = await viteDevServer.moduleGraph.getModuleByUrl(moduleFilePath)
}
if (!node) {
console.error(`Could not resolve module for file: ${moduleFilePath}`)
return
}
await findNodeDependencies(viteDevServer, node, deps)
} catch (error) {
console.error(error)
}
for (const dep of deps) {
if (
dep.file &&
isCssFile(dep.file) &&
!isCssUrlWithoutSideEffects(dep.url) // Ignore styles that resolved as URLs, inline or raw. These shouldn't get injected.
) {
try {
const css = isCssModulesFile(dep.file)
? cssModulesManifest[dep.file]
: ((
await viteDevServer.ssrLoadModule(
// We need the ?inline query in Vite v6 when loading CSS in SSR
// since it does not expose the default export for CSS in a
// server environment.
injectQuery(normalizePath(dep.file), 'inline'),
)
).default as string)
if (css === undefined) {
throw new Error()
}
styles[dep.url] = css
} catch {
// this can happen with dynamically imported modules
console.warn(`Could not load ${dep.file}`)
}
}
}
return (
Object.entries(styles)
.map(([fileName, css]) => [
`\n/* ${fileName
// Escape comment syntax in file paths
.replace(/\/\*/g, '/\\*')
.replace(/\*\//g, '*\\/')} */`,
css,
])
.flat()
.join('\n') || undefined
)
}
/**
* This function transform the componentId into a file path.
* File extension is not required for the vite.moduleGraph URL search.
@@ -80,77 +168,7 @@ export const getStylesForComponentId = async (
const fileUrl = path.join(process.cwd(), relativeFilePath)
const styles: Record<string, string> = {}
const deps: Set<ModuleNode> = new Set()
try {
let node: ModuleNode | undefined =
await viteDevServer.moduleGraph.getModuleByUrl(fileUrl)
// If the module is only present in the client module graph, the module
// won't have been found on the first request to the server. If so, we
// request the module so it's in the module graph, then try again.
if (!node) {
try {
await viteDevServer.transformRequest(fileUrl)
} catch (err) {
console.error(err)
}
node = await viteDevServer.moduleGraph.getModuleByUrl(fileUrl)
}
if (!node) {
console.error(`Could not resolve module for file: ${fileUrl}`)
return
}
await findNodeDependencies(viteDevServer, node, deps)
} catch (error) {
console.error(error)
}
for (const dep of deps) {
if (
dep.file &&
isCssFile(dep.file) &&
!isCssUrlWithoutSideEffects(dep.url) // Ignore styles that resolved as URLs, inline or raw. These shouldn't get injected.
) {
try {
const css = isCssModulesFile(dep.file)
? cssModulesManifest[dep.file]
: ((
await viteDevServer.ssrLoadModule(
// We need the ?inline query in Vite v6 when loading CSS in SSR
// since it does not expose the default export for CSS in a
// server environment.
injectQuery(dep.url, 'inline'),
)
).default as string)
if (css === undefined) {
throw new Error()
}
styles[dep.url] = css
} catch {
// this can happen with dynamically imported modules
console.warn(`Could not load ${dep.file}`)
}
}
}
return (
Object.entries(styles)
.map(([fileName, css]) => [
`\n/* ${fileName
// Escape comment syntax in file paths
.replace(/\/\*/g, '/\\*')
.replace(/\*\//g, '*\\/')} */`,
css,
])
.flat()
.join('\n') || undefined
)
return await getStylesForModule(viteDevServer, fileUrl, cssModulesManifest)
}
/**
@@ -21,12 +21,14 @@ const Postscatchall = createRoute({ component: PostscatchallImport })
const IndexRoute = Index.update({
path: '/',
getParentRoute: () => rootRoute,
filePath: '/',
})
const PostscatchallRoute = Postscatchall.update({
path: '/posts/[...catch_all]',
getParentRoute: () => rootRoute,
hasHandler: true,
filePath: '/posts/[...catch_all]',
})
// Create and export the route tree
@@ -21,11 +21,13 @@ const Index = createRoute({ component: IndexImport })
const AboutRoute = About.update({
path: '/about',
getParentRoute: () => rootRoute,
filePath: '/about',
})
const IndexRoute = Index.update({
path: '/',
getParentRoute: () => rootRoute,
filePath: '/',
})
// Create and export the route tree
@@ -36,31 +36,37 @@ const PostsMyPost = createRoute({ component: PostsMyPostImport })
const PostslayoutRoute = Postslayout.update({
getParentRoute: () => rootRoute,
filePath: '/posts/__layout',
})
const AboutRoute = About.update({
path: '/about',
getParentRoute: () => rootRoute,
filePath: '/about',
})
const IndexRoute = Index.update({
path: '/',
getParentRoute: () => rootRoute,
filePath: '/',
})
const PostspostRoute = Postspost.update({
path: '/posts/[post]',
getParentRoute: () => PostslayoutRoute,
filePath: '/posts/[post]',
})
const PostsIndexRoute = PostsIndex.update({
path: '/posts',
getParentRoute: () => PostslayoutRoute,
filePath: '/posts/',
})
const PostsMyPostRoute = PostsMyPost.update({
path: '/posts/my-post',
getParentRoute: () => PostslayoutRoute,
filePath: '/posts/my-post',
})
// Create and export the route tree
@@ -25,16 +25,19 @@ const PostsMyPost = createRoute({ component: PostsMyPostImport })
const AboutRoute = About.update({
path: '/about',
getParentRoute: () => rootRoute,
filePath: '/about',
})
const IndexRoute = Index.update({
path: '/',
getParentRoute: () => rootRoute,
filePath: '/',
})
const PostsMyPostRoute = PostsMyPost.update({
path: '/posts/my-post',
getParentRoute: () => rootRoute,
filePath: '/posts/my-post',
})
// Create and export the route tree
@@ -21,11 +21,13 @@ const Index = createRoute({ component: IndexImport })
const AboutRoute = About.update({
path: '/about',
getParentRoute: () => rootRoute,
filePath: '/about',
})
const IndexRoute = Index.update({
path: '/',
getParentRoute: () => rootRoute,
filePath: '/',
})
// Create and export the route tree
@@ -6,7 +6,7 @@ const VITE_PROXY_PATH = '/vite-server'
const CRITICAL_CSS_PATH = VITE_PROXY_PATH + '/tuono_internal__critical_css'
interface CriticalCssProps {
routeId?: string
routeFilePath?: string
mode?: Mode
}
@@ -16,16 +16,16 @@ interface CriticalCssProps {
* since vite does not support CSS injection without JS waterfall
*/
export function CriticalCss({
routeId,
routeFilePath,
mode,
}: CriticalCssProps): JSX.Element | null {
if (!routeId || mode !== 'Dev') {
if (!routeFilePath || mode !== 'Dev') {
return null
}
return (
<link
href={`${CRITICAL_CSS_PATH}?componentId=${routeId}`}
href={`${CRITICAL_CSS_PATH}?componentId=${routeFilePath}`}
precedence="high"
rel="stylesheet"
/>
@@ -18,7 +18,7 @@ export function NotFound({ mode }: { mode?: Mode }): JSX.Element | null {
if (custom404Route) {
return (
<>
<CriticalCss routeId={custom404Route.id} mode={mode} />
<CriticalCss routeFilePath={custom404Route.filePath} mode={mode} />
<RouteMatch route={custom404Route} mode={mode} serverInitialData={{}} />
</>
)
@@ -30,7 +30,7 @@ export function NotFound({ mode }: { mode?: Mode }): JSX.Element | null {
return (
<RootLayout data={null} isLoading={false}>
<CriticalCss routeId="__root__" mode={mode} />
<CriticalCss routeFilePath="__root__" mode={mode} />
<NotFoundDefaultContent />
</RootLayout>
)
@@ -42,7 +42,7 @@ export const RouteMatch = ({
mode={mode}
>
<Suspense>
<CriticalCss routeId={route.id} mode={mode} />
<CriticalCss routeFilePath={route.filePath} mode={mode} />
<route.component data={routeData} isLoading={isTransitioning} />
</Suspense>
</TraverseRootComponents>
@@ -77,9 +77,13 @@ const TraverseRootComponents = memo(
const route = routes[index] as Route
const Parent = route.component
// Fallback to the route id if the filePath is not defined
// as is the case for the root route
const routeFilePath = route.filePath || route.id
return (
<Parent data={data} isLoading={isLoading}>
<CriticalCss routeId={route.id} mode={mode} />
<CriticalCss routeFilePath={routeFilePath} mode={mode} />
<TraverseRootComponents
routes={routes}
data={data}
+20 -3
View File
@@ -6,6 +6,7 @@ interface RouteOptions {
isRoot?: boolean
getParentRoute?: () => Route
path?: string
filePath?: string
component: RouteComponent
hasHandler?: boolean
}
@@ -19,11 +20,28 @@ export const ROOT_ROUTE_ID = '__root__'
export class Route {
options: RouteOptions
/**
* The route id is used to identify the route in the router
* and is used to match the route with the URL.
*
* For now is the `path`
*/
id?: string
isRoot: boolean
/**
* Used for identify the route by matching the URL
*/
path?: string
fullPath!: string
/**
* Utility to identify the route in the file system
* Used i.e. for finding the criticalCss to load
*
* The path does not include the file extension
*/
filePath?: string
children?: Array<Route>
parentRoute?: Route
originalIndex?: number
@@ -70,11 +88,10 @@ export class Route {
id = joinPaths(['/', id])
}
const fullPath = id === ROOT_ROUTE_ID ? '/' : path
this.filePath = this.options.filePath
this.path = path
this.id = id
this.fullPath = fullPath || ''
this.fullPath = path || ''
}
addChildren(routes: Array<Route>): this {