Skip to content

Commit

Permalink
Improved server CSS handling (#39664)
Browse files Browse the repository at this point in the history
Upgrade experimental React, and render link tags directly in the tree during development. The client bundle won't import CSS anymore, and server CSS imports will be transpiled into no-op strings just for HMR to use.

## Follow Ups
- [ ] Flash of unstyled elements when reloading styles
- [ ] Collect client style imports
- [ ] Console warning for duplicated resources
- [ ] Tests

## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have helpful link attached, see `contributing.md`

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have helpful link attached, see `contributing.md`

## Documentation / Examples

- [ ] Make sure the linting passes by running `pnpm lint`
- [ ] The examples guidelines are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing.md#adding-examples)
  • Loading branch information
shuding committed Aug 17, 2022
1 parent d4a98a1 commit c79b67c
Show file tree
Hide file tree
Showing 10 changed files with 99 additions and 101 deletions.
4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -177,8 +177,8 @@
"react-17": "npm:react@17.0.2",
"react-dom": "18.2.0",
"react-dom-17": "npm:react-dom@17.0.2",
"react-dom-exp": "npm:react-dom@0.0.0-experimental-4cd788aef-20220630",
"react-exp": "npm:react@0.0.0-experimental-4cd788aef-20220630",
"react-dom-exp": "npm:react-dom@0.0.0-experimental-6ef466c68-20220816",
"react-exp": "npm:react@0.0.0-experimental-6ef466c68-20220816",
"react-ssr-prepass": "1.0.8",
"react-virtualized": "9.22.3",
"relay-compiler": "13.0.2",
Expand Down
35 changes: 25 additions & 10 deletions packages/next/build/webpack/config/blocks/css/index.ts
Expand Up @@ -275,16 +275,31 @@ export const css = curry(async function css(
}

if (ctx.isServer) {
fns.push(
loader({
oneOf: [
markRemovable({
test: [regexCssGlobal, regexSassGlobal],
use: require.resolve('next/dist/compiled/ignore-loader'),
}),
],
})
)
if (ctx.experimental.appDir && !ctx.isProduction) {
fns.push(
loader({
oneOf: [
markRemovable({
test: [regexCssGlobal, regexSassGlobal],
use: require.resolve(
'../../../loaders/next-flight-css-dev-loader'
),
}),
],
})
)
} else {
fns.push(
loader({
oneOf: [
markRemovable({
test: [regexCssGlobal, regexSassGlobal],
use: require.resolve('next/dist/compiled/ignore-loader'),
}),
],
})
)
}
} else {
fns.push(
loader({
Expand Down
Expand Up @@ -21,7 +21,11 @@ export default async function transformSource(this: any): Promise<string> {
requests
// Filter out css files on the server
.filter((request) => (isServer ? !request.endsWith('.css') : true))
.map((request) => `import(/* webpackMode: "eager" */ '${request}')`)
.map((request) =>
request.endsWith('.css')
? `(() => import(/* webpackMode: "lazy" */ '${request}'))`
: `import(/* webpackMode: "eager" */ '${request}')`
)
.join(';\n') +
`
export const __next_rsc__ = {
Expand Down
16 changes: 16 additions & 0 deletions packages/next/build/webpack/loaders/next-flight-css-dev-loader.ts
@@ -0,0 +1,16 @@
/**
* For server-side CSS imports, we need to ignore the actual module content but
* still trigger the hot-reloading diff mechanism. So here we put the content
* inside a comment.
*/

const NextServerCSSLoader = function (this: any, source: string | Buffer) {
this.cacheable && this.cacheable()

return `export default "${(typeof source === 'string'
? Buffer.from(source)
: source
).toString('hex')}"`
}

export default NextServerCSSLoader
101 changes: 33 additions & 68 deletions packages/next/server/app-render.tsx
Expand Up @@ -378,55 +378,26 @@ function getSegmentParam(segment: string): {
/**
* Get inline <link> tags based on server CSS manifest. Only used when rendering to HTML.
*/
// function getCssInlinedLinkTags(
// serverComponentManifest: FlightManifest,
// serverCSSManifest: FlightCSSManifest,
// filePath: string
// ): string[] {
// const layoutOrPageCss = serverCSSManifest[filePath]

// if (!layoutOrPageCss) {
// return []
// }

// const chunks = new Set<string>()
function getCssInlinedLinkTags(
serverComponentManifest: FlightManifest,
serverCSSManifest: FlightCSSManifest,
filePath: string
): string[] {
const layoutOrPageCss = serverCSSManifest[filePath]

// for (const css of layoutOrPageCss) {
// for (const chunk of serverComponentManifest[css].default.chunks) {
// chunks.add(chunk)
// }
// }
if (!layoutOrPageCss) {
return []
}

// return [...chunks]
// }
const chunks = new Set<string>()

/**
* Get inline <link> tags based on server CSS manifest. Only used when rendering to HTML.
*/
function getAllCssInlinedLinkTags(
serverComponentManifest: FlightManifest,
serverCSSManifest: FlightCSSManifest
): string[] {
const chunks: { [file: string]: string[] } = {}

// APP-TODO: Remove this once we have CSS injections at each level.
const allChunks = new Set<string>()

for (const layoutOrPage in serverCSSManifest) {
const uniqueChunks = new Set<string>()
for (const css of serverCSSManifest[layoutOrPage]) {
for (const chunk of serverComponentManifest[css].default.chunks) {
if (!uniqueChunks.has(chunk)) {
uniqueChunks.add(chunk)
chunks[layoutOrPage] = chunks[layoutOrPage] || []
chunks[layoutOrPage].push(chunk)
}
allChunks.add(chunk)
}
for (const css of layoutOrPageCss) {
for (const chunk of serverComponentManifest[css].default.chunks) {
chunks.add(chunk)
}
}

return [...allChunks]
return [...chunks]
}

export async function renderToHTMLOrFlight(
Expand Down Expand Up @@ -618,29 +589,23 @@ export async function renderToHTMLOrFlight(
*/
const createComponentTree = async ({
createSegmentPath,
loaderTree: [
segment,
parallelRoutes,
{ /* filePath, */ layout, loading, page },
],
loaderTree: [segment, parallelRoutes, { filePath, layout, loading, page }],
parentParams,
firstItem,
rootLayoutIncluded,
}: // parentSegmentPath,
{
}: {
createSegmentPath: CreateSegmentPath
loaderTree: LoaderTree
parentParams: { [key: string]: any }
rootLayoutIncluded?: boolean
firstItem?: boolean
// parentSegmentPath: string
}): Promise<{ Component: React.ComponentType }> => {
// TODO-APP: enable stylesheet per layout/page
// const stylesheets = getCssInlinedLinkTags(
// serverComponentManifest,
// serverCSSManifest!,
// filePath
// )
const stylesheets = getCssInlinedLinkTags(
serverComponentManifest,
serverCSSManifest!,
filePath
)
const Loading = loading ? await interopDefault(loading()) : undefined
const isLayout = typeof layout !== 'undefined'
const isPage = typeof page !== 'undefined'
Expand Down Expand Up @@ -719,7 +684,6 @@ export async function renderToHTMLOrFlight(
loaderTree: parallelRoutes[parallelRouteKey],
parentParams: currentParams,
rootLayoutIncluded: rootLayoutIncludedAtThisLevelOrAbove,
// parentSegmentPath: cssSegmentPath,
})

const childSegment = parallelRoutes[parallelRouteKey][0]
Expand Down Expand Up @@ -860,11 +824,20 @@ export async function renderToHTMLOrFlight(

return (
<>
{/* {stylesheets
{stylesheets
? stylesheets.map((href) => (
<link rel="stylesheet" href={`/_next/${href}`} key={href} />
<link
rel="stylesheet"
href={`/_next/${href}?ts=${Date.now()}`}
// `Precedence` is an opt-in signal for React to handle
// resource loading and deduplication, etc:
// https://github.com/facebook/react/pull/25060
// @ts-ignore
precedence="high"
key={href}
/>
))
: null} */}
: null}
<Component
{...props}
{...parallelRouteComponents}
Expand Down Expand Up @@ -988,19 +961,12 @@ export async function renderToHTMLOrFlight(

// Below this line is handling for rendering to HTML.

// Get all the server imported styles.
const initialStylesheets = getAllCssInlinedLinkTags(
serverComponentManifest,
serverCSSManifest || {}
)

// Create full component tree from root to leaf.
const { Component: ComponentTree } = await createComponentTree({
createSegmentPath: (child) => child,
loaderTree: loaderTree,
parentParams: {},
firstItem: true,
// parentSegmentPath: '',
})

// AppRouter is provided by next-app-loader
Expand Down Expand Up @@ -1108,7 +1074,6 @@ export async function renderToHTMLOrFlight(
generateStaticHTML: generateStaticHTML,
flushEffectHandler,
flushEffectsToHead: true,
initialStylesheets,
})
}

Expand Down
7 changes: 1 addition & 6 deletions packages/next/server/node-web-streams-helper.ts
Expand Up @@ -265,14 +265,12 @@ export async function continueFromInitialStream(
generateStaticHTML,
flushEffectHandler,
flushEffectsToHead,
initialStylesheets,
}: {
suffix?: string
dataStream?: ReadableStream<Uint8Array>
generateStaticHTML: boolean
flushEffectHandler?: () => string
flushEffectsToHead: boolean
initialStylesheets?: string[]
}
): Promise<ReadableStream<Uint8Array>> {
const closeTag = '</body></html>'
Expand All @@ -291,14 +289,11 @@ export async function continueFromInitialStream(
dataStream ? createInlineDataStream(dataStream) : null,
suffixUnclosed != null ? createSuffixStream(closeTag) : null,
createHeadInjectionTransformStream(() => {
const inlineStyleLinks = (initialStylesheets || [])
.map((href) => `<link rel="stylesheet" href="/_next/${href}">`)
.join('')
// TODO-APP: Inject flush effects to end of head in app layout rendering, to avoid
// hydration errors. Remove this once it's ready to be handled by react itself.
const flushEffectsContent =
flushEffectHandler && flushEffectsToHead ? flushEffectHandler() : ''
return inlineStyleLinks + flushEffectsContent
return flushEffectsContent
}),
].filter(nonNullable)

Expand Down
24 changes: 12 additions & 12 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion test/e2e/app-dir/app/app/css/css-page/style.css
@@ -1,3 +1,3 @@
h1 {
color: blueviolet;
color: red;
}
2 changes: 1 addition & 1 deletion test/e2e/app-dir/app/app/css/style.css
@@ -1,3 +1,3 @@
.server-css {
color: green;
color: blue;
}
3 changes: 3 additions & 0 deletions test/e2e/app-dir/app/app/style.css
@@ -0,0 +1,3 @@
body {
font-size: xx-large;
}

0 comments on commit c79b67c

Please sign in to comment.