Skip to content

Commit 0eb9f7e

Browse files
authored
Initial implementation of statically optimized flight data of server component pages (#35619)
Part of #31506 and #34179. This PR ensures that in the `nodejs` runtime, the flight data is statically stored as a JSON file if possible. Most of the touched code is related to conditions of static/SSG/SSR when runtime and/or RSC is involved. ## Bug - [ ] Related issues linked using `fixes #number` - [x] 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 `yarn lint`
1 parent bcd2aa5 commit 0eb9f7e

File tree

15 files changed

+450
-88
lines changed

15 files changed

+450
-88
lines changed

packages/next/build/index.ts

+15-2
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ import {
9999
copyTracedFiles,
100100
isReservedPage,
101101
isCustomErrorPage,
102+
isFlightPage,
102103
} from './utils'
103104
import getBaseWebpackConfig from './webpack-config'
104105
import { PagesManifest } from './webpack/plugins/pages-manifest-plugin'
@@ -162,7 +163,6 @@ export default async function build(
162163
// using React 18 or experimental.
163164
const hasReactRoot = shouldUseReactRoot()
164165
const hasConcurrentFeatures = hasReactRoot
165-
166166
const hasServerComponents =
167167
hasReactRoot && !!config.experimental.serverComponents
168168

@@ -288,6 +288,7 @@ export default async function build(
288288
.traceAsyncFn(() => collectPages(pagesDir, config.pageExtensions))
289289
// needed for static exporting since we want to replace with HTML
290290
// files
291+
291292
const allStaticPages = new Set<string>()
292293
let allPageInfos = new Map<string, PageInfo>()
293294

@@ -963,6 +964,7 @@ export default async function build(
963964

964965
let isSsg = false
965966
let isStatic = false
967+
let isServerComponent = false
966968
let isHybridAmp = false
967969
let ssgPageRoutes: string[] | null = null
968970
let isMiddlewareRoute = !!page.match(MIDDLEWARE_ROUTE)
@@ -976,6 +978,12 @@ export default async function build(
976978
? await getPageRuntime(join(pagesDir, pagePath), config)
977979
: undefined
978980

981+
if (hasServerComponents && pagePath) {
982+
if (isFlightPage(config, pagePath)) {
983+
isServerComponent = true
984+
}
985+
}
986+
979987
if (
980988
!isMiddlewareRoute &&
981989
!isReservedPage(page) &&
@@ -1045,11 +1053,16 @@ export default async function build(
10451053
serverPropsPages.add(page)
10461054
} else if (
10471055
workerResult.isStatic &&
1048-
!workerResult.hasFlightData &&
1056+
!isServerComponent &&
10491057
(await customAppGetInitialPropsPromise) === false
10501058
) {
10511059
staticPages.add(page)
10521060
isStatic = true
1061+
} else if (isServerComponent) {
1062+
// This is a static server component page that doesn't have
1063+
// gSP or gSSP. We still treat it as a SSG page.
1064+
ssgPages.add(page)
1065+
isSsg = true
10531066
}
10541067

10551068
if (hasPages404 && page === '/404') {

packages/next/build/utils.ts

+1-8
Original file line numberDiff line numberDiff line change
@@ -859,7 +859,6 @@ export async function isPageStatic(
859859
isStatic?: boolean
860860
isAmpOnly?: boolean
861861
isHybridAmp?: boolean
862-
hasFlightData?: boolean
863862
hasServerProps?: boolean
864863
hasStaticProps?: boolean
865864
prerenderRoutes?: string[]
@@ -882,7 +881,6 @@ export async function isPageStatic(
882881
throw new Error('INVALID_DEFAULT_EXPORT')
883882
}
884883

885-
const hasFlightData = !!(mod as any).__next_rsc__
886884
const hasGetInitialProps = !!(Comp as any).getInitialProps
887885
const hasStaticProps = !!mod.getStaticProps
888886
const hasStaticPaths = !!mod.getStaticPaths
@@ -970,19 +968,14 @@ export async function isPageStatic(
970968
const isNextImageImported = (global as any).__NEXT_IMAGE_IMPORTED
971969
const config: PageConfig = mod.pageConfig
972970
return {
973-
isStatic:
974-
!hasStaticProps &&
975-
!hasGetInitialProps &&
976-
!hasServerProps &&
977-
!hasFlightData,
971+
isStatic: !hasStaticProps && !hasGetInitialProps && !hasServerProps,
978972
isHybridAmp: config.amp === 'hybrid',
979973
isAmpOnly: config.amp === true,
980974
prerenderRoutes,
981975
prerenderFallback,
982976
encodedPrerenderRoutes,
983977
hasStaticProps,
984978
hasServerProps,
985-
hasFlightData,
986979
isNextImageImported,
987980
traceIncludes: config.unstable_includeFiles || [],
988981
traceExcludes: config.unstable_excludeFiles || [],

packages/next/build/webpack/loaders/next-flight-server-loader.ts

+49-4
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ async function parseModuleInfo({
4141
source: string
4242
imports: string
4343
isEsm: boolean
44+
__N_SSP: boolean
45+
pageRuntime: 'edge' | 'nodejs' | null
4446
}> {
4547
const ast = await parse(source, {
4648
filename: resourcePath,
@@ -50,12 +52,15 @@ async function parseModuleInfo({
5052
let transformedSource = ''
5153
let lastIndex = 0
5254
let imports = ''
55+
let __N_SSP = false
56+
let pageRuntime = null
57+
5358
const isEsm = type === 'Module'
5459

5560
for (let i = 0; i < body.length; i++) {
5661
const node = body[i]
5762
switch (node.type) {
58-
case 'ImportDeclaration': {
63+
case 'ImportDeclaration':
5964
const importSource = node.source.value
6065
if (!isClientCompilation) {
6166
// Server compilation for .server.js.
@@ -112,7 +117,32 @@ async function parseModuleInfo({
112117

113118
lastIndex = node.source.span.end
114119
break
115-
}
120+
case 'ExportDeclaration':
121+
if (isClientCompilation) {
122+
// Keep `__N_SSG` and `__N_SSP` exports.
123+
if (node.declaration?.type === 'VariableDeclaration') {
124+
for (const declaration of node.declaration.declarations) {
125+
if (declaration.type === 'VariableDeclarator') {
126+
if (declaration.id?.type === 'Identifier') {
127+
const value = declaration.id.value
128+
if (value === '__N_SSP') {
129+
__N_SSP = true
130+
} else if (value === 'config') {
131+
const props = declaration.init.properties
132+
const runtimeKeyValue = props.find(
133+
(prop: any) => prop.key.value === 'runtime'
134+
)
135+
const runtime = runtimeKeyValue?.value?.value
136+
if (runtime === 'nodejs' || runtime === 'edge') {
137+
pageRuntime = runtime
138+
}
139+
}
140+
}
141+
}
142+
}
143+
}
144+
}
145+
break
116146
default:
117147
break
118148
}
@@ -122,7 +152,7 @@ async function parseModuleInfo({
122152
transformedSource += source.substring(lastIndex)
123153
}
124154

125-
return { source: transformedSource, imports, isEsm }
155+
return { source: transformedSource, imports, isEsm, __N_SSP, pageRuntime }
126156
}
127157

128158
export default async function transformSource(
@@ -161,6 +191,8 @@ export default async function transformSource(
161191
source: transformedSource,
162192
imports,
163193
isEsm,
194+
__N_SSP,
195+
pageRuntime,
164196
} = await parseModuleInfo({
165197
resourcePath,
166198
source,
@@ -190,7 +222,20 @@ export default async function transformSource(
190222
}
191223

192224
if (isClientCompilation) {
193-
rscExports['default'] = 'function RSC() {}'
225+
rscExports.default = 'function RSC() {}'
226+
227+
if (pageRuntime === 'edge') {
228+
// Currently for the Edge runtime, we treat all RSC pages as SSR pages.
229+
rscExports.__N_SSP = 'true'
230+
} else {
231+
if (__N_SSP) {
232+
rscExports.__N_SSP = 'true'
233+
} else {
234+
// Server component pages are always considered as SSG by default because
235+
// the flight data is needed for client navigation.
236+
rscExports.__N_SSG = 'true'
237+
}
238+
}
194239
}
195240

196241
const output = transformedSource + '\n' + buildExports(rscExports, isEsm)

packages/next/client/index.tsx

+23-20
Original file line numberDiff line numberDiff line change
@@ -547,14 +547,17 @@ function renderReactElement(
547547

548548
const reactEl = fn(shouldHydrate ? markHydrateComplete : markRenderComplete)
549549
if (process.env.__NEXT_REACT_ROOT) {
550-
const ReactDOMClient = require('react-dom/client')
551550
if (!reactRoot) {
552551
// Unlike with createRoot, you don't need a separate root.render() call here
553-
reactRoot = (ReactDOMClient as any).hydrateRoot(domEl, reactEl)
552+
const ReactDOMClient = require('react-dom/client')
553+
reactRoot = ReactDOMClient.hydrateRoot(domEl, reactEl)
554554
// TODO: Remove shouldHydrate variable when React 18 is stable as it can depend on `reactRoot` existing
555555
shouldHydrate = false
556556
} else {
557-
reactRoot.render(reactEl)
557+
const startTransition = (React as any).startTransition
558+
startTransition(() => {
559+
reactRoot.render(reactEl)
560+
})
558561
}
559562
} else {
560563
// The check for `.hydrate` is there to support React alternatives like preact
@@ -675,6 +678,7 @@ if (process.env.__NEXT_RSC) {
675678

676679
const {
677680
createFromFetch,
681+
createFromReadableStream,
678682
} = require('next/dist/compiled/react-server-dom-webpack')
679683

680684
const encoder = new TextEncoder()
@@ -769,20 +773,19 @@ if (process.env.__NEXT_RSC) {
769773
nextServerDataRegisterWriter(controller)
770774
},
771775
})
772-
response = createFromFetch(Promise.resolve({ body: readable }))
776+
response = createFromReadableStream(readable)
773777
} else {
774-
const fetchPromise = serialized
775-
? (() => {
776-
const readable = new ReadableStream({
777-
start(controller) {
778-
controller.enqueue(new TextEncoder().encode(serialized))
779-
controller.close()
780-
},
781-
})
782-
return Promise.resolve({ body: readable })
783-
})()
784-
: fetchFlight(getCacheKey())
785-
response = createFromFetch(fetchPromise)
778+
if (serialized) {
779+
const readable = new ReadableStream({
780+
start(controller) {
781+
controller.enqueue(new TextEncoder().encode(serialized))
782+
controller.close()
783+
},
784+
})
785+
response = createFromReadableStream(readable)
786+
} else {
787+
response = createFromFetch(fetchFlight(getCacheKey()))
788+
}
786789
}
787790

788791
rscCache.set(cacheKey, response)
@@ -800,16 +803,16 @@ if (process.env.__NEXT_RSC) {
800803
rscCache.delete(cacheKey)
801804
})
802805
const response = useServerResponse(cacheKey, serialized)
803-
const root = response.readRoot()
804-
return root
806+
return response.readRoot()
805807
}
806808

807809
RSCComponent = (props: any) => {
808810
const cacheKey = getCacheKey()
809-
const { __flight_serialized__ } = props
811+
const { __flight__ } = props
810812
const [, dispatch] = useState({})
811813
const startTransition = (React as any).startTransition
812814
const rerender = () => dispatch({})
815+
813816
// If there is no cache, or there is serialized data already
814817
function refreshCache(nextProps?: any) {
815818
startTransition(() => {
@@ -825,7 +828,7 @@ if (process.env.__NEXT_RSC) {
825828

826829
return (
827830
<RefreshContext.Provider value={refreshCache}>
828-
<ServerRoot cacheKey={cacheKey} serialized={__flight_serialized__} />
831+
<ServerRoot cacheKey={cacheKey} serialized={__flight__} />
829832
</RefreshContext.Provider>
830833
)
831834
}

packages/next/client/page-loader.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -133,21 +133,21 @@ export default class PageLoader {
133133
href,
134134
asPath,
135135
ssg,
136-
rsc,
136+
flight,
137137
locale,
138138
}: {
139139
href: string
140140
asPath: string
141141
ssg?: boolean
142-
rsc?: boolean
142+
flight?: boolean
143143
locale?: string | false
144144
}): string {
145145
const { pathname: hrefPathname, query, search } = parseRelativeUrl(href)
146146
const { pathname: asPathname } = parseRelativeUrl(asPath)
147147
const route = normalizeRoute(hrefPathname)
148148

149149
const getHrefForSlug = (path: string) => {
150-
if (rsc) {
150+
if (flight) {
151151
return path + search + (search ? `&` : '?') + '__flight__=1'
152152
}
153153

packages/next/server/base-server.ts

+19-5
Original file line numberDiff line numberDiff line change
@@ -1124,14 +1124,25 @@ export default abstract class Server {
11241124
const isLikeServerless =
11251125
typeof components.ComponentMod === 'object' &&
11261126
typeof (components.ComponentMod as any).renderReqToHTML === 'function'
1127-
const isSSG = !!components.getStaticProps
11281127
const hasServerProps = !!components.getServerSideProps
11291128
const hasStaticPaths = !!components.getStaticPaths
11301129
const hasGetInitialProps = !!components.Component?.getInitialProps
1130+
const isServerComponent = !!components.ComponentMod?.__next_rsc__
1131+
const isSSG =
1132+
!!components.getStaticProps ||
1133+
// For static server component pages, we currently always consider them
1134+
// as SSG since we also need to handle the next data (flight JSON).
1135+
(isServerComponent &&
1136+
!hasServerProps &&
1137+
!hasGetInitialProps &&
1138+
!process.browser)
11311139

11321140
// Toggle whether or not this is a Data request
1133-
const isDataReq = !!query._nextDataReq && (isSSG || hasServerProps)
1141+
const isDataReq =
1142+
!!query._nextDataReq && (isSSG || hasServerProps || isServerComponent)
1143+
11341144
delete query._nextDataReq
1145+
11351146
// Don't delete query.__flight__ yet, it still needs to be used in renderToHTML later
11361147
const isFlightRequest = Boolean(
11371148
this.serverComponentManifest && query.__flight__
@@ -1290,8 +1301,8 @@ export default abstract class Server {
12901301
}
12911302

12921303
let ssgCacheKey =
1293-
isPreviewMode || !isSSG || opts.supportsDynamicHTML
1294-
? null // Preview mode and manual revalidate bypasses the cache
1304+
isPreviewMode || !isSSG || opts.supportsDynamicHTML || isFlightRequest
1305+
? null // Preview mode, manual revalidate, flight request can bypass the cache
12951306
: `${locale ? `/${locale}` : ''}${
12961307
(pathname === '/' || resolvedUrlPathname === '/') && locale
12971308
? ''
@@ -1602,7 +1613,10 @@ export default abstract class Server {
16021613
if (isDataReq) {
16031614
return {
16041615
type: 'json',
1605-
body: RenderResult.fromStatic(JSON.stringify(cachedData.props)),
1616+
body: RenderResult.fromStatic(
1617+
// @TODO: Handle flight data.
1618+
JSON.stringify(cachedData.props)
1619+
),
16061620
revalidateOptions,
16071621
}
16081622
} else {

packages/next/server/next-server.ts

+1
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,7 @@ export default class NextNodeServer extends BaseServer {
680680
_nextDataReq: query._nextDataReq,
681681
__nextLocale: query.__nextLocale,
682682
__nextDefaultLocale: query.__nextDefaultLocale,
683+
__flight__: query.__flight__,
683684
} as NextParsedUrlQuery)
684685
: query),
685686
...(params || {}),

0 commit comments

Comments
 (0)