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

Add experimental concurrentFeatures config #27768

Merged
merged 13 commits into from
Aug 11, 2021
2 changes: 2 additions & 0 deletions packages/next/server/config-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export type NextConfig = { [key: string]: any } & {
staticPageGenerationTimeout?: number
pageDataCollectionTimeout?: number
isrMemoryCacheSize?: number
concurrentFeatures?: boolean
}
}

Expand Down Expand Up @@ -185,6 +186,7 @@ export const defaultConfig: NextConfig = {
pageDataCollectionTimeout: 60,
// default to 50MB limit
isrMemoryCacheSize: 50 * 1024 * 1024,
concurrentFeatures: false,
},
future: {
strictPostcssConfiguration: false,
Expand Down
2 changes: 2 additions & 0 deletions packages/next/server/next-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ export default class Server {
defaultLocale?: string
domainLocales?: DomainLocale[]
distDir: string
concurrentFeatures?: boolean
}
private compression?: Middleware
private incrementalCache: IncrementalCache
Expand Down Expand Up @@ -241,6 +242,7 @@ export default class Server {
.disableOptimizedLoading,
domainLocales: this.nextConfig.i18n?.domains,
distDir: this.distDir,
concurrentFeatures: this.nextConfig.experimental.concurrentFeatures,
}

// Only the `publicRuntimeConfig` key is exposed to the client side
Expand Down
60 changes: 51 additions & 9 deletions packages/next/server/render.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { IncomingMessage, ServerResponse } from 'http'
import { ParsedUrlQuery } from 'querystring'
import { PassThrough } from 'stream'
import React from 'react'
import { renderToStaticMarkup, renderToString } from 'react-dom/server'
import * as ReactDOMServer from 'react-dom/server'
import { warn } from '../build/output/log'
import { UnwrapPromise } from '../lib/coalesced-function'
import {
Expand Down Expand Up @@ -43,6 +44,7 @@ import {
loadGetInitialProps,
NextComponentType,
RenderPage,
RenderPageResult,
} from '../shared/lib/utils'
import {
tryGetPreviewData,
Expand Down Expand Up @@ -190,6 +192,7 @@ export type RenderOptsPartial = {
domainLocales?: DomainLocale[]
disableOptimizedLoading?: boolean
requireStaticHTML?: boolean
concurrentFeatures?: boolean
}

export type RenderOpts = LoadComponentsReturnType & RenderOptsPartial
Expand Down Expand Up @@ -263,7 +266,7 @@ function renderDocument(
): string {
return (
'<!DOCTYPE html>' +
renderToStaticMarkup(
ReactDOMServer.renderToStaticMarkup(
<AmpStateContext.Provider value={ampState}>
{Document.renderDocument(Document, {
__NEXT_DATA__: {
Expand Down Expand Up @@ -408,6 +411,7 @@ export async function renderToHTML(
previewProps,
basePath,
devOnlyCacheBusterQueryString,
concurrentFeatures,
} = renderOpts

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

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

const AppContainer = ({ children }: any) => (
<RouterContext.Provider value={router}>
Expand Down Expand Up @@ -991,11 +997,45 @@ export async function renderToHTML(
}
}

// TODO: Support SSR streaming of Suspense.
const renderToString = concurrentFeatures
? (element: React.ReactElement) =>
new Promise<string>((resolve, reject) => {
const stream = new PassThrough()
const buffers: Buffer[] = []
stream.on('data', (chunk) => {
buffers.push(chunk)
})
stream.once('end', () => {
resolve(Buffer.concat(buffers).toString('utf-8'))
})

const {
abort,
startWriting,
} = (ReactDOMServer as any).pipeToNodeWritable(element, stream, {
onError(error: Error) {
abort()
reject(error)
},
onCompleteAll() {
startWriting()
},
})
})
: ReactDOMServer.renderToString

const renderPage: RenderPage = (
options: ComponentsEnhancer = {}
): { html: string; head: any } => {
): RenderPageResult | Promise<RenderPageResult> => {
if (ctx.err && ErrorDebug) {
return { html: renderToString(<ErrorDebug error={ctx.err} />), head }
const htmlOrPromise = renderToString(<ErrorDebug error={ctx.err} />)
return typeof htmlOrPromise === 'string'
? { html: htmlOrPromise, head }
: htmlOrPromise.then((html) => ({
html,
head,
}))
}

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

const html = renderToString(
const htmlOrPromise = renderToString(
<AppContainer>
<EnhancedApp Component={EnhancedComponent} router={router} {...props} />
</AppContainer>
)

return { html, head }
return typeof htmlOrPromise === 'string'
? { html: htmlOrPromise, head }
: htmlOrPromise.then((html) => ({
html,
head,
}))
}
const documentCtx = { ...ctx, renderPage }
const docProps: DocumentInitialProps = await loadGetInitialProps(
Expand Down Expand Up @@ -1049,8 +1093,6 @@ export async function renderToHTML(
const hybridAmp = ampState.hybrid

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

let html = renderDocument(Document, {
...renderOpts,
Expand Down