Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial implementation of statically optimized flight data of server component pages #35619

Merged
merged 23 commits into from Apr 1, 2022
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 15 additions & 2 deletions packages/next/build/index.ts
Expand Up @@ -98,6 +98,7 @@ import {
copyTracedFiles,
isReservedPage,
isCustomErrorPage,
isFlightPage,
} from './utils'
import getBaseWebpackConfig from './webpack-config'
import { PagesManifest } from './webpack/plugins/pages-manifest-plugin'
Expand Down Expand Up @@ -161,7 +162,6 @@ export default async function build(
// using React 18 or experimental.
const hasReactRoot = shouldUseReactRoot()
const hasConcurrentFeatures = hasReactRoot

const hasServerComponents =
hasReactRoot && !!config.experimental.serverComponents

Expand Down Expand Up @@ -287,6 +287,7 @@ export default async function build(
.traceAsyncFn(() => collectPages(pagesDir, config.pageExtensions))
// needed for static exporting since we want to replace with HTML
// files

const allStaticPages = new Set<string>()
let allPageInfos = new Map<string, PageInfo>()

Expand Down Expand Up @@ -962,6 +963,7 @@ export default async function build(

let isSsg = false
let isStatic = false
let isServerComponent = false
let isHybridAmp = false
let ssgPageRoutes: string[] | null = null
let isMiddlewareRoute = !!page.match(MIDDLEWARE_ROUTE)
Expand All @@ -975,6 +977,12 @@ export default async function build(
? await getPageRuntime(join(pagesDir, pagePath), config)
: undefined

if (hasServerComponents && pagePath) {
if (isFlightPage(config, pagePath)) {
isServerComponent = true
}
}

if (
!isMiddlewareRoute &&
!isReservedPage(page) &&
Expand Down Expand Up @@ -1044,11 +1052,16 @@ export default async function build(
serverPropsPages.add(page)
} else if (
workerResult.isStatic &&
!workerResult.hasFlightData &&
!isServerComponent &&
(await customAppGetInitialPropsPromise) === false
) {
staticPages.add(page)
isStatic = true
} else if (isServerComponent) {
// This is a static server component page that doesn't have
// gSP or gSSP. We still treat it as a SSG page.
ssgPages.add(page)
isSsg = true
}

if (hasPages404 && page === '/404') {
Expand Down
9 changes: 1 addition & 8 deletions packages/next/build/utils.ts
Expand Up @@ -859,7 +859,6 @@ export async function isPageStatic(
isStatic?: boolean
isAmpOnly?: boolean
isHybridAmp?: boolean
hasFlightData?: boolean
hasServerProps?: boolean
hasStaticProps?: boolean
prerenderRoutes?: string[]
Expand All @@ -882,7 +881,6 @@ export async function isPageStatic(
throw new Error('INVALID_DEFAULT_EXPORT')
}

const hasFlightData = !!(mod as any).__next_rsc__
const hasGetInitialProps = !!(Comp as any).getInitialProps
const hasStaticProps = !!mod.getStaticProps
const hasStaticPaths = !!mod.getStaticPaths
Expand Down Expand Up @@ -970,19 +968,14 @@ export async function isPageStatic(
const isNextImageImported = (global as any).__NEXT_IMAGE_IMPORTED
const config: PageConfig = mod.pageConfig
return {
isStatic:
!hasStaticProps &&
!hasGetInitialProps &&
!hasServerProps &&
!hasFlightData,
isStatic: !hasStaticProps && !hasGetInitialProps && !hasServerProps,
isHybridAmp: config.amp === 'hybrid',
isAmpOnly: config.amp === true,
prerenderRoutes,
prerenderFallback,
encodedPrerenderRoutes,
hasStaticProps,
hasServerProps,
hasFlightData,
isNextImageImported,
traceIncludes: config.unstable_includeFiles || [],
traceExcludes: config.unstable_excludeFiles || [],
Expand Down
Expand Up @@ -41,6 +41,8 @@ async function parseModuleInfo({
source: string
imports: string
isEsm: boolean
__N_SSP: boolean
pageRuntime: 'edge' | 'nodejs' | null
}> {
const ast = await parse(source, {
filename: resourcePath,
Expand All @@ -50,12 +52,15 @@ async function parseModuleInfo({
let transformedSource = ''
let lastIndex = 0
let imports = ''
let __N_SSP = false
let pageRuntime = null

const isEsm = type === 'Module'

for (let i = 0; i < body.length; i++) {
const node = body[i]
switch (node.type) {
case 'ImportDeclaration': {
case 'ImportDeclaration':
const importSource = node.source.value
if (!isClientCompilation) {
// Server compilation for .server.js.
Expand Down Expand Up @@ -112,7 +117,32 @@ async function parseModuleInfo({

lastIndex = node.source.span.end
break
}
case 'ExportDeclaration':
if (isClientCompilation) {
// Keep `__N_SSG` and `__N_SSP` exports.
if (node.declaration?.type === 'VariableDeclaration') {
for (const declaration of node.declaration.declarations) {
if (declaration.type === 'VariableDeclarator') {
if (declaration.id?.type === 'Identifier') {
const value = declaration.id.value
if (value === '__N_SSP') {
huozhi marked this conversation as resolved.
Show resolved Hide resolved
__N_SSP = true
} else if (value === 'config') {
const props = declaration.init.properties
const runtimeKeyValue = props.find(
(prop: any) => prop.key.value === 'runtime'
)
const runtime = runtimeKeyValue?.value?.value
if (runtime === 'nodejs' || runtime === 'edge') {
pageRuntime = runtime
}
}
}
}
}
}
}
break
default:
break
}
Expand All @@ -122,7 +152,7 @@ async function parseModuleInfo({
transformedSource += source.substring(lastIndex)
}

return { source: transformedSource, imports, isEsm }
return { source: transformedSource, imports, isEsm, __N_SSP, pageRuntime }
}

export default async function transformSource(
Expand Down Expand Up @@ -161,6 +191,8 @@ export default async function transformSource(
source: transformedSource,
imports,
isEsm,
__N_SSP,
pageRuntime,
} = await parseModuleInfo({
resourcePath,
source,
Expand Down Expand Up @@ -190,7 +222,20 @@ export default async function transformSource(
}

if (isClientCompilation) {
rscExports['default'] = 'function RSC() {}'
rscExports.default = 'function RSC() {}'

if (pageRuntime === 'edge') {
// Currently for the Edge runtime, we treat all RSC pages as SSR pages.
rscExports.__N_SSP = 'true'
} else {
if (__N_SSP) {
rscExports.__N_SSP = 'true'
} else {
// Server component pages are always considered as SSG by default because
// the flight data is needed for client navigation.
rscExports.__N_SSG = 'true'
}
}
}

const output = transformedSource + '\n' + buildExports(rscExports, isEsm)
Expand Down
45 changes: 23 additions & 22 deletions packages/next/client/index.tsx
Expand Up @@ -547,14 +547,17 @@ function renderReactElement(

const reactEl = fn(shouldHydrate ? markHydrateComplete : markRenderComplete)
if (process.env.__NEXT_REACT_ROOT) {
const ReactDOMClient = require('react-dom/client')
if (!reactRoot) {
// Unlike with createRoot, you don't need a separate root.render() call here
reactRoot = (ReactDOMClient as any).hydrateRoot(domEl, reactEl)
const ReactDOMClient = require('react-dom/client')
reactRoot = ReactDOMClient.hydrateRoot(domEl, reactEl)
// TODO: Remove shouldHydrate variable when React 18 is stable as it can depend on `reactRoot` existing
shouldHydrate = false
} else {
reactRoot.render(reactEl)
const startTransition = (React as any).startTransition
startTransition(() => {
reactRoot.render(reactEl)
})
Comment on lines +558 to +560
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
} else {
// The check for `.hydrate` is there to support React alternatives like preact
Expand Down Expand Up @@ -675,6 +678,7 @@ if (process.env.__NEXT_RSC) {

const {
createFromFetch,
createFromReadableStream,
} = require('next/dist/compiled/react-server-dom-webpack')

const encoder = new TextEncoder()
Expand Down Expand Up @@ -764,22 +768,19 @@ if (process.env.__NEXT_RSC) {
if (response) return response

if (initialServerDataBuffer) {
const t = new TransformStream()
const writer = t.writable.getWriter()
response = createFromFetch(Promise.resolve({ body: t.readable }))
nextServerDataRegisterWriter(writer)
const { readable, writable } = new TransformStream()
response = createFromReadableStream(readable)
shuding marked this conversation as resolved.
Show resolved Hide resolved
nextServerDataRegisterWriter(writable.getWriter())
} else {
const fetchPromise = serialized
? (() => {
const t = new TransformStream()
const writer = t.writable.getWriter()
writer.ready.then(() => {
writer.write(new TextEncoder().encode(serialized))
})
return Promise.resolve({ body: t.readable })
})()
: fetchFlight(getCacheKey())
response = createFromFetch(fetchPromise)
if (serialized) {
const { readable, writable } = new TransformStream()
const writer = writable.getWriter()
writer.write(new TextEncoder().encode(serialized))
writer.close()
response = createFromReadableStream(readable)
} else {
response = createFromFetch(fetchFlight(getCacheKey()))
}
}

rscCache.set(cacheKey, response)
Expand All @@ -797,16 +798,16 @@ if (process.env.__NEXT_RSC) {
rscCache.delete(cacheKey)
})
const response = useServerResponse(cacheKey, serialized)
const root = response.readRoot()
return root
return response.readRoot()
}

RSCComponent = (props: any) => {
const cacheKey = getCacheKey()
const { __flight_serialized__ } = props
const { __flight__ } = props
const [, dispatch] = useState({})
const startTransition = (React as any).startTransition
const rerender = () => dispatch({})

// If there is no cache, or there is serialized data already
function refreshCache(nextProps?: any) {
startTransition(() => {
Expand All @@ -822,7 +823,7 @@ if (process.env.__NEXT_RSC) {

return (
<RefreshContext.Provider value={refreshCache}>
<ServerRoot cacheKey={cacheKey} serialized={__flight_serialized__} />
<ServerRoot cacheKey={cacheKey} serialized={__flight__} />
</RefreshContext.Provider>
)
}
Expand Down
6 changes: 3 additions & 3 deletions packages/next/client/page-loader.ts
Expand Up @@ -133,21 +133,21 @@ export default class PageLoader {
href,
asPath,
ssg,
rsc,
flight,
locale,
}: {
href: string
asPath: string
ssg?: boolean
rsc?: boolean
flight?: boolean
locale?: string | false
}): string {
const { pathname: hrefPathname, query, search } = parseRelativeUrl(href)
const { pathname: asPathname } = parseRelativeUrl(asPath)
const route = normalizeRoute(hrefPathname)

const getHrefForSlug = (path: string) => {
if (rsc) {
if (flight) {
return path + search + (search ? `&` : '?') + '__flight__=1'
}

Expand Down
19 changes: 16 additions & 3 deletions packages/next/server/base-server.ts
Expand Up @@ -1124,13 +1124,23 @@ export default abstract class Server {
const isLikeServerless =
typeof components.ComponentMod === 'object' &&
typeof (components.ComponentMod as any).renderReqToHTML === 'function'
const isSSG = !!components.getStaticProps
const hasServerProps = !!components.getServerSideProps
const hasStaticPaths = !!components.getStaticPaths
const hasGetInitialProps = !!components.Component?.getInitialProps
const isServerComponent = !!components.ComponentMod?.__next_rsc__
const isSSG =
!!components.getStaticProps ||
// For static server component pages, we currently always consider them
// as SSG since we also need to handle the next data.
(isServerComponent &&
!hasServerProps &&
!hasGetInitialProps &&
!process.browser)

// Toggle whether or not this is a Data request
const isDataReq = !!query._nextDataReq && (isSSG || hasServerProps)
const isDataReq =
!!query._nextDataReq && (isSSG || hasServerProps || isServerComponent)

delete query._nextDataReq
// Don't delete query.__flight__ yet, it still needs to be used in renderToHTML later
const isFlightRequest = Boolean(
Expand Down Expand Up @@ -1602,7 +1612,10 @@ export default abstract class Server {
if (isDataReq) {
return {
type: 'json',
body: RenderResult.fromStatic(JSON.stringify(cachedData.props)),
body: RenderResult.fromStatic(
// @TODO: Handle flight data.
JSON.stringify(cachedData.props)
),
revalidateOptions,
}
} else {
Expand Down