Skip to content

Commit

Permalink
Add experimental concurrentFeatures config (#27768)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
devknoll committed Aug 11, 2021
1 parent 3c837ed commit 459b391
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 9 deletions.
2 changes: 2 additions & 0 deletions packages/next/server/config-shared.ts
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
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
@@ -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

0 comments on commit 459b391

Please sign in to comment.