Skip to content

Commit

Permalink
Drop legacy React DOM Server in Edge runtime (#40018)
Browse files Browse the repository at this point in the history
When possible (`ReactRoot` enabled), we always use
`renderToReadableStream` to render the element to string and drop all
`renderToString` and `renderToStaticMarkup` usages. Since this is always
true for the Edge Runtime (which requires React 18+), so we can safely
eliminate the `./cjs/react-dom-server-legacy.browser.production.min.js`
module there
([ref](https://unpkg.com/browse/[email protected]/server.browser.js)).

This reduces the gzipped bundle by 11kb (~9%). Let me know if there's
any concern or it's too hacky.

<img width="904" alt="image"
src="https://user-images.githubusercontent.com/11064311/192544933-298e3638-13ba-436d-9bcb-42dfb1224025.png">


## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] 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 `pnpm lint`
- [ ] The examples guidelines are followed from [our contributing
doc](https://github.com/vercel/next.js/blob/canary/contributing.md#adding-examples)

Co-authored-by: Jimmy Lai <[email protected]>
  • Loading branch information
shuding and feedthejim authored Sep 29, 2022
1 parent c1c95bf commit e550222
Show file tree
Hide file tree
Showing 6 changed files with 58 additions and 26 deletions.
4 changes: 4 additions & 0 deletions packages/next/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,10 @@ export default async function getBaseWebpackConfig(
'@builder.io/partytown': '{}',
'next/dist/compiled/etag': '{}',
'next/dist/compiled/chalk': '{}',
'./cjs/react-dom-server-legacy.browser.production.min.js':
'{}',
'./cjs/react-dom-server-legacy.browser.development.js':
'{}',
'react-dom': '{}',
},
handleWebpackExtenalForEdgeRuntime,
Expand Down
12 changes: 10 additions & 2 deletions packages/next/server/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
renderToInitialStream,
createBufferedTransformStream,
continueFromInitialStream,
streamToString,
} from './node-web-streams-helper'
import { ESCAPE_REGEX, htmlEscapeJsonString } from './htmlescape'
import { shouldUseReactRoot } from './utils'
Expand Down Expand Up @@ -595,6 +596,13 @@ function headersWithoutFlight(headers: IncomingHttpHeaders) {
return newHeaders
}

async function renderToString(element: React.ReactElement) {
if (!shouldUseReactRoot) return ReactDOMServer.renderToString(element)
const renderStream = await ReactDOMServer.renderToReadableStream(element)
await renderStream.allReady
return streamToString(renderStream)
}

export async function renderToHTMLOrFlight(
req: IncomingMessage,
res: ServerResponse,
Expand Down Expand Up @@ -1272,8 +1280,8 @@ export async function renderToHTMLOrFlight(
</FlushEffects>
)

const flushEffectHandler = (): string => {
const flushed = ReactDOMServer.renderToString(
const flushEffectHandler = (): Promise<string> => {
const flushed = renderToString(
<>{Array.from(flushEffectsCallbacks).map((callback) => callback())}</>
)
return flushed
Expand Down
15 changes: 11 additions & 4 deletions packages/next/server/base-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import {
getCookieParser,
checkIsManualRevalidate,
} from './api-utils'
import * as envConfig from '../shared/lib/runtime-config'
import { setConfig } from '../shared/lib/runtime-config'
import Router from './router'

import { setRevalidateHeaders } from './send-payload/revalidate-headers'
Expand All @@ -67,7 +67,6 @@ import { removePathPrefix } from '../shared/lib/router/utils/remove-path-prefix'
import { normalizeAppPath } from '../shared/lib/router/utils/app-paths'
import { getRouteMatcher } from '../shared/lib/router/utils/route-matcher'
import { getRouteRegex } from '../shared/lib/router/utils/route-regex'
import { getLocaleRedirect } from '../shared/lib/i18n/get-locale-redirect'
import { getHostname } from '../shared/lib/get-hostname'
import { parseUrl as parseUrlUtil } from '../shared/lib/router/utils/parse-url'
import { getNextPathnameInfo } from '../shared/lib/router/utils/get-next-pathname-info'
Expand Down Expand Up @@ -415,7 +414,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
}

// Initialize next/config with the environment configuration
envConfig.setConfig({
setConfig({
serverRuntimeConfig,
publicRuntimeConfig,
})
Expand Down Expand Up @@ -668,7 +667,14 @@ export default abstract class Server<ServerOptions extends Options = Options> {
}
}

if (!this.minimalMode && defaultLocale) {
if (
// Edge runtime always has minimal mode enabled.
process.env.NEXT_RUNTIME !== 'edge' &&
!this.minimalMode &&
defaultLocale
) {
const { getLocaleRedirect } =
require('../shared/lib/i18n/get-locale-redirect') as typeof import('../shared/lib/i18n/get-locale-redirect')
const redirect = getLocaleRedirect({
defaultLocale,
domainLocale,
Expand Down Expand Up @@ -1367,6 +1373,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
// getStaticPaths, then finish the data request on the client-side.
//
if (
process.env.NEXT_RUNTIME !== 'edge' &&
this.minimalMode !== true &&
fallbackMode !== 'blocking' &&
ssgCacheKey &&
Expand Down
20 changes: 11 additions & 9 deletions packages/next/server/node-web-streams-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,11 @@ export function createBufferedTransformStream(
}

export function createFlushEffectStream(
handleFlushEffect: () => string
handleFlushEffect: () => Promise<string>
): TransformStream<Uint8Array, Uint8Array> {
return new TransformStream({
transform(chunk, controller) {
const flushedChunk = encodeText(handleFlushEffect())
async transform(chunk, controller) {
const flushedChunk = encodeText(await handleFlushEffect())

controller.enqueue(flushedChunk)
controller.enqueue(chunk)
Expand All @@ -149,17 +149,17 @@ export function renderToInitialStream({
}

export function createHeadInjectionTransformStream(
inject: () => string
inject: () => Promise<string>
): TransformStream<Uint8Array, Uint8Array> {
let injected = false
return new TransformStream({
transform(chunk, controller) {
async transform(chunk, controller) {
const content = decodeText(chunk)
let index
if (!injected && (index = content.indexOf('</head')) !== -1) {
injected = true
const injectedContent =
content.slice(0, index) + inject() + content.slice(index)
content.slice(0, index) + (await inject()) + content.slice(index)
controller.enqueue(encodeText(injectedContent))
} else {
controller.enqueue(chunk)
Expand Down Expand Up @@ -269,7 +269,7 @@ export async function continueFromInitialStream(
suffix?: string
dataStream?: ReadableStream<Uint8Array>
generateStaticHTML: boolean
flushEffectHandler?: () => string
flushEffectHandler?: () => Promise<string>
flushEffectsToHead: boolean
}
): Promise<ReadableStream<Uint8Array>> {
Expand All @@ -288,11 +288,13 @@ export async function continueFromInitialStream(
suffixUnclosed != null ? createDeferredSuffixStream(suffixUnclosed) : null,
dataStream ? createInlineDataStream(dataStream) : null,
suffixUnclosed != null ? createSuffixStream(closeTag) : null,
createHeadInjectionTransformStream(() => {
createHeadInjectionTransformStream(async () => {
// TODO-APP: Inject flush effects to end of head in app layout rendering, to avoid
// hydration errors. Remove this once it's ready to be handled by react itself.
const flushEffectsContent =
flushEffectHandler && flushEffectsToHead ? flushEffectHandler() : ''
flushEffectHandler && flushEffectsToHead
? await flushEffectHandler()
: ''
return flushEffectsContent
}),
].filter(nonNullable)
Expand Down
29 changes: 20 additions & 9 deletions packages/next/server/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,18 @@ function noRouter() {
throw new Error(message)
}

async function renderToString(element: React.ReactElement) {
if (!shouldUseReactRoot) return ReactDOMServer.renderToString(element)
const renderStream = await ReactDOMServer.renderToReadableStream(element)
await renderStream.allReady
return streamToString(renderStream)
}

async function renderToStaticMarkup(element: React.ReactElement) {
if (!shouldUseReactRoot) return ReactDOMServer.renderToStaticMarkup(element)
return renderToString(element)
}

class ServerRouter implements NextRouter {
route: string
pathname: string
Expand Down Expand Up @@ -1148,16 +1160,16 @@ export async function renderToHTML(
_Component: NextComponentType
) => Promise<ReactReadableStream>
) {
const renderPage: RenderPage = (
const renderPage: RenderPage = async (
options: ComponentsEnhancer = {}
): RenderPageResult | Promise<RenderPageResult> => {
): Promise<RenderPageResult> => {
if (ctx.err && ErrorDebug) {
// Always start rendering the shell even if there's an error.
if (renderShell) {
renderShell(App, Component)
}

const html = ReactDOMServer.renderToString(
const html = await renderToString(
<Body>
<ErrorDebug error={ctx.err} />
</Body>
Expand All @@ -1184,7 +1196,7 @@ export async function renderToHTML(
)
}

const html = ReactDOMServer.renderToString(
const html = await renderToString(
<Body>
<AppContainerWithIsomorphicFiberStructure>
{renderPageTree(EnhancedApp, EnhancedComponent, {
Expand Down Expand Up @@ -1256,7 +1268,7 @@ export async function renderToHTML(
// for non-concurrent rendering we need to ensure App is rendered
// before _document so that updateHead is called/collected before
// rendering _document's head
const result = ReactDOMServer.renderToString(content)
const result = await renderToString(content)
const bodyResult = (suffix: string) => streamFromArray([result, suffix])

const styles = jsxStyleRegistry.styles()
Expand Down Expand Up @@ -1289,9 +1301,8 @@ export async function renderToHTML(
) => {
// this must be called inside bodyResult so appWrappers is
// up to date when `wrapApp` is called
const flushEffectHandler = (): string => {
const flushed = ReactDOMServer.renderToString(styledJsxFlushEffect())
return flushed
const flushEffectHandler = async (): Promise<string> => {
return renderToString(styledJsxFlushEffect())
}

return continueFromInitialStream(initialStream, {
Expand Down Expand Up @@ -1459,7 +1470,7 @@ export async function renderToHTML(
</AmpStateContext.Provider>
)

const documentHTML = ReactDOMServer.renderToStaticMarkup(document)
const documentHTML = await renderToStaticMarkup(document)

if (process.env.NODE_ENV !== 'production') {
const nonRenderedComponents = []
Expand Down
4 changes: 2 additions & 2 deletions packages/next/server/web-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import type { LoadComponentsReturnType } from './load-components'
import { NoFallbackError, Options } from './base-server'
import type { DynamicRoutes, PageChecker, Route } from './router'
import type { NextConfig } from './config-shared'
import type { BaseNextRequest, BaseNextResponse } from './base-http'
import type { UrlWithParsedQuery } from 'url'

import BaseServer from './base-server'
import { byteLength } from './api-utils/web'
Expand All @@ -19,8 +21,6 @@ import getRouteFromAssetPath from '../shared/lib/router/utils/get-route-from-ass
import { detectDomainLocale } from '../shared/lib/i18n/detect-domain-locale'
import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path'
import { removeTrailingSlash } from '../shared/lib/router/utils/remove-trailing-slash'
import type { BaseNextRequest, BaseNextResponse } from './base-http'
import type { UrlWithParsedQuery } from 'url'

interface WebServerOptions extends Options {
webServerConfig: {
Expand Down

0 comments on commit e550222

Please sign in to comment.