Skip to content

Commit 459b391

Browse files
authored
Add experimental concurrentFeatures config (#27768)
Allows opting in to support for new concurrent features, like server-side Suspense. **!!! DO NOT USE !!!** This is highly experimental. We **will** be gating additional breaking changes behind this same flag. **!!! DO NOT USE !!!** Also resolves suspense for static pages (i.e. `getStaticProps` or `next build`/`next export`) since we can't currently support streaming for those cases anyway.
1 parent 3c837ed commit 459b391

File tree

3 files changed

+55
-9
lines changed

3 files changed

+55
-9
lines changed

packages/next/server/config-shared.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export type NextConfig = { [key: string]: any } & {
113113
staticPageGenerationTimeout?: number
114114
pageDataCollectionTimeout?: number
115115
isrMemoryCacheSize?: number
116+
concurrentFeatures?: boolean
116117
}
117118
}
118119

@@ -185,6 +186,7 @@ export const defaultConfig: NextConfig = {
185186
pageDataCollectionTimeout: 60,
186187
// default to 50MB limit
187188
isrMemoryCacheSize: 50 * 1024 * 1024,
189+
concurrentFeatures: false,
188190
},
189191
future: {
190192
strictPostcssConfiguration: false,

packages/next/server/next-server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ export default class Server {
181181
defaultLocale?: string
182182
domainLocales?: DomainLocale[]
183183
distDir: string
184+
concurrentFeatures?: boolean
184185
}
185186
private compression?: Middleware
186187
private incrementalCache: IncrementalCache
@@ -241,6 +242,7 @@ export default class Server {
241242
.disableOptimizedLoading,
242243
domainLocales: this.nextConfig.i18n?.domains,
243244
distDir: this.distDir,
245+
concurrentFeatures: this.nextConfig.experimental.concurrentFeatures,
244246
}
245247

246248
// Only the `publicRuntimeConfig` key is exposed to the client side

packages/next/server/render.tsx

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { IncomingMessage, ServerResponse } from 'http'
22
import { ParsedUrlQuery } from 'querystring'
3+
import { PassThrough } from 'stream'
34
import React from 'react'
4-
import { renderToStaticMarkup, renderToString } from 'react-dom/server'
5+
import * as ReactDOMServer from 'react-dom/server'
56
import { warn } from '../build/output/log'
67
import { UnwrapPromise } from '../lib/coalesced-function'
78
import {
@@ -43,6 +44,7 @@ import {
4344
loadGetInitialProps,
4445
NextComponentType,
4546
RenderPage,
47+
RenderPageResult,
4648
} from '../shared/lib/utils'
4749
import {
4850
tryGetPreviewData,
@@ -190,6 +192,7 @@ export type RenderOptsPartial = {
190192
domainLocales?: DomainLocale[]
191193
disableOptimizedLoading?: boolean
192194
requireStaticHTML?: boolean
195+
concurrentFeatures?: boolean
193196
}
194197

195198
export type RenderOpts = LoadComponentsReturnType & RenderOptsPartial
@@ -263,7 +266,7 @@ function renderDocument(
263266
): string {
264267
return (
265268
'<!DOCTYPE html>' +
266-
renderToStaticMarkup(
269+
ReactDOMServer.renderToStaticMarkup(
267270
<AmpStateContext.Provider value={ampState}>
268271
{Document.renderDocument(Document, {
269272
__NEXT_DATA__: {
@@ -408,6 +411,7 @@ export async function renderToHTML(
408411
previewProps,
409412
basePath,
410413
devOnlyCacheBusterQueryString,
414+
concurrentFeatures,
411415
} = renderOpts
412416

413417
const getFontDefinition = (url: string): string => {
@@ -626,6 +630,8 @@ export async function renderToHTML(
626630
let head: JSX.Element[] = defaultHead(inAmpMode)
627631

628632
let scriptLoader: any = {}
633+
const nextExport =
634+
!isSSG && (renderOpts.nextExport || (dev && (isAutoExport || isFallback)))
629635

630636
const AppContainer = ({ children }: any) => (
631637
<RouterContext.Provider value={router}>
@@ -991,11 +997,45 @@ export async function renderToHTML(
991997
}
992998
}
993999

1000+
// TODO: Support SSR streaming of Suspense.
1001+
const renderToString = concurrentFeatures
1002+
? (element: React.ReactElement) =>
1003+
new Promise<string>((resolve, reject) => {
1004+
const stream = new PassThrough()
1005+
const buffers: Buffer[] = []
1006+
stream.on('data', (chunk) => {
1007+
buffers.push(chunk)
1008+
})
1009+
stream.once('end', () => {
1010+
resolve(Buffer.concat(buffers).toString('utf-8'))
1011+
})
1012+
1013+
const {
1014+
abort,
1015+
startWriting,
1016+
} = (ReactDOMServer as any).pipeToNodeWritable(element, stream, {
1017+
onError(error: Error) {
1018+
abort()
1019+
reject(error)
1020+
},
1021+
onCompleteAll() {
1022+
startWriting()
1023+
},
1024+
})
1025+
})
1026+
: ReactDOMServer.renderToString
1027+
9941028
const renderPage: RenderPage = (
9951029
options: ComponentsEnhancer = {}
996-
): { html: string; head: any } => {
1030+
): RenderPageResult | Promise<RenderPageResult> => {
9971031
if (ctx.err && ErrorDebug) {
998-
return { html: renderToString(<ErrorDebug error={ctx.err} />), head }
1032+
const htmlOrPromise = renderToString(<ErrorDebug error={ctx.err} />)
1033+
return typeof htmlOrPromise === 'string'
1034+
? { html: htmlOrPromise, head }
1035+
: htmlOrPromise.then((html) => ({
1036+
html,
1037+
head,
1038+
}))
9991039
}
10001040

10011041
if (dev && (props.router || props.Component)) {
@@ -1009,13 +1049,17 @@ export async function renderToHTML(
10091049
Component: EnhancedComponent,
10101050
} = enhanceComponents(options, App, Component)
10111051

1012-
const html = renderToString(
1052+
const htmlOrPromise = renderToString(
10131053
<AppContainer>
10141054
<EnhancedApp Component={EnhancedComponent} router={router} {...props} />
10151055
</AppContainer>
10161056
)
1017-
1018-
return { html, head }
1057+
return typeof htmlOrPromise === 'string'
1058+
? { html: htmlOrPromise, head }
1059+
: htmlOrPromise.then((html) => ({
1060+
html,
1061+
head,
1062+
}))
10191063
}
10201064
const documentCtx = { ...ctx, renderPage }
10211065
const docProps: DocumentInitialProps = await loadGetInitialProps(
@@ -1049,8 +1093,6 @@ export async function renderToHTML(
10491093
const hybridAmp = ampState.hybrid
10501094

10511095
const docComponentsRendered: DocumentProps['docComponentsRendered'] = {}
1052-
const nextExport =
1053-
!isSSG && (renderOpts.nextExport || (dev && (isAutoExport || isFallback)))
10541096

10551097
let html = renderDocument(Document, {
10561098
...renderOpts,

0 commit comments

Comments
 (0)