feat: load critical CSS on development (#765)

This commit is contained in:
Valerio Ageno
2025-05-09 17:02:18 +02:00
committed by GitHub
parent 58f4ecb260
commit dd157d6ef9
17 changed files with 366 additions and 17 deletions
@@ -0,0 +1,33 @@
import type { JSX } from 'react'
import type { Mode } from '../types'
const VITE_PROXY_PATH = '/vite-server'
const CRITICAL_CSS_PATH = VITE_PROXY_PATH + '/tuono_internal__critical_css'
interface CriticalCssProps {
routeId?: string
mode?: Mode
}
/**
* Returns the critical CSS for the given route
* This is required in order to avoid FOUC during development
* since vite does not support CSS injection without JS waterfall
*/
export function CriticalCss({
routeId,
mode,
}: CriticalCssProps): JSX.Element | null {
if (!routeId || mode !== 'Dev') {
return null
}
return (
<link
href={`${CRITICAL_CSS_PATH}?componentId=${routeId}`}
precedence="high"
rel="stylesheet"
/>
)
}
@@ -2,6 +2,8 @@ import type { JSX } from 'react'
import { useRoute } from '../hooks/useRoute'
import type { Mode } from '../types'
import { RouteMatch } from './RouteMatch'
import { NotFound } from './NotFound'
import { useRouterContext } from './RouterContext'
@@ -9,16 +11,26 @@ import { useRouterContext } from './RouterContext'
interface MatchesProps<TServerPayloadData = unknown> {
// user defined props
serverInitialData: TServerPayloadData
mode?: Mode
}
export function Matches({ serverInitialData }: MatchesProps): JSX.Element {
export function Matches({
serverInitialData,
mode,
}: MatchesProps): JSX.Element {
const { location } = useRouterContext()
const route = useRoute(location.pathname)
if (!route) {
return <NotFound />
return <NotFound mode={mode} />
}
return <RouteMatch route={route} serverInitialData={serverInitialData} />
return (
<RouteMatch
route={route}
mode={mode}
serverInitialData={serverInitialData}
/>
)
}
@@ -3,17 +3,25 @@ import type { JSX } from 'react'
import { useRouterContext } from '../components/RouterContext'
import { ROOT_ROUTE_ID } from '../route'
import type { Mode } from '../types'
import { RouteMatch } from './RouteMatch'
import { NotFoundDefaultContent } from './NotFoundDefaultContent'
import { CriticalCss } from './CriticalCss'
export function NotFound(): JSX.Element | null {
export function NotFound({ mode }: { mode?: Mode }): JSX.Element | null {
const { router } = useRouterContext()
const custom404Route = router.routesById['/404']
// Check if exists a custom 404 error page
if (custom404Route) {
return <RouteMatch route={custom404Route} serverInitialData={{}} />
return (
<>
<CriticalCss routeId={custom404Route.id} mode={mode} />
<RouteMatch route={custom404Route} mode={mode} serverInitialData={{}} />
</>
)
}
const RootLayout = router.routesById[ROOT_ROUTE_ID]?.component
@@ -22,6 +30,7 @@ export function NotFound(): JSX.Element | null {
return (
<RootLayout data={null} isLoading={false}>
<CriticalCss routeId="__root__" mode={mode} />
<NotFoundDefaultContent />
</RootLayout>
)
@@ -1,16 +1,19 @@
import type { JSX } from 'react'
import { memo, Suspense, useMemo } from 'react'
import type { Mode } from '../types'
import type { Route } from '../route'
import { useServerPayloadData } from '../hooks/useServerPayloadData'
import { useRouterContext } from './RouterContext'
import { CriticalCss } from './CriticalCss'
interface RouteMatchProps<TServerPayloadData = unknown> {
route: Route
// User defined server side props
serverInitialData: TServerPayloadData
mode?: Mode
}
/**
@@ -21,6 +24,7 @@ interface RouteMatchProps<TServerPayloadData = unknown> {
export const RouteMatch = ({
route,
serverInitialData,
mode,
}: RouteMatchProps): JSX.Element => {
const { data } = useServerPayloadData(route, serverInitialData)
const { isTransitioning } = useRouterContext()
@@ -35,8 +39,10 @@ export const RouteMatch = ({
routes={routes}
data={routeData}
isLoading={isTransitioning}
mode={mode}
>
<Suspense>
<CriticalCss routeId={route.id} mode={mode} />
<route.component data={routeData} isLoading={isTransitioning} />
</Suspense>
</TraverseRootComponents>
@@ -49,6 +55,7 @@ interface TraverseRootComponentsProps<TData = unknown> {
isLoading: boolean
children?: React.ReactNode
index?: number
mode?: Mode
}
/**
@@ -63,18 +70,22 @@ const TraverseRootComponents = memo(
data,
isLoading,
index = 0,
mode,
children,
}: TraverseRootComponentsProps): React.JSX.Element => {
if (routes.length > index) {
const Parent = (routes[index] as Route).component
const route = routes[index] as Route
const Parent = route.component
return (
<Parent data={data} isLoading={isLoading}>
<CriticalCss routeId={route.id} mode={mode} />
<TraverseRootComponents
routes={routes}
data={data}
isLoading={isLoading}
index={index + 1}
mode={mode}
>
{children}
</TraverseRootComponents>
@@ -1,6 +1,6 @@
import type { JSX } from 'react'
import type { ServerInitialLocation } from '../types'
import type { ServerInitialLocation, Mode } from '../types'
import type { Router } from '../router'
import { RouterContextProvider } from './RouterContext'
@@ -10,19 +10,21 @@ interface RouterProviderProps {
router: Router
serverInitialLocation: ServerInitialLocation
serverInitialData: unknown
mode?: Mode
}
export function RouterProvider({
router,
serverInitialLocation,
serverInitialData,
mode,
}: RouterProviderProps): JSX.Element {
return (
<RouterContextProvider
router={router}
serverInitialLocation={serverInitialLocation}
>
<Matches serverInitialData={serverInitialData} />
<Matches serverInitialData={serverInitialData} mode={mode} />
</RouterContextProvider>
)
}
+3 -1
View File
@@ -16,7 +16,9 @@ export function sanitizePathname(pathname: string): string {
return pathname
}
/*
/**
* Returns the route that matches the given pathname
*
* This hook is also implemented on server side to match the bundle
* file to load at the first rendering.
*
+2
View File
@@ -1,5 +1,7 @@
import type { ReactNode, ComponentType } from 'react'
export type Mode = 'Dev' | 'Prod'
export interface Segment {
type: 'pathname' | 'param' | 'wildcard'
value: string