From e550222db74983ee49f438e48e26b55e596d80d9 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Thu, 29 Sep 2022 10:56:28 +0200 Subject: [PATCH] Drop legacy React DOM Server in Edge runtime (#40018) 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/react-dom@18.2.0/server.browser.js)). This reduces the gzipped bundle by 11kb (~9%). Let me know if there's any concern or it's too hacky. image ## 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 --- packages/next/build/webpack-config.ts | 4 +++ packages/next/server/app-render.tsx | 12 ++++++-- packages/next/server/base-server.ts | 15 +++++++--- .../next/server/node-web-streams-helper.ts | 20 +++++++------ packages/next/server/render.tsx | 29 +++++++++++++------ packages/next/server/web-server.ts | 4 +-- 6 files changed, 58 insertions(+), 26 deletions(-) diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 102de1c6c249e..31beb8be6054e 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -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, diff --git a/packages/next/server/app-render.tsx b/packages/next/server/app-render.tsx index 2b019dea089e8..4975b86ba9a00 100644 --- a/packages/next/server/app-render.tsx +++ b/packages/next/server/app-render.tsx @@ -17,6 +17,7 @@ import { renderToInitialStream, createBufferedTransformStream, continueFromInitialStream, + streamToString, } from './node-web-streams-helper' import { ESCAPE_REGEX, htmlEscapeJsonString } from './htmlescape' import { shouldUseReactRoot } from './utils' @@ -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, @@ -1272,8 +1280,8 @@ export async function renderToHTMLOrFlight( ) - const flushEffectHandler = (): string => { - const flushed = ReactDOMServer.renderToString( + const flushEffectHandler = (): Promise => { + const flushed = renderToString( <>{Array.from(flushEffectsCallbacks).map((callback) => callback())} ) return flushed diff --git a/packages/next/server/base-server.ts b/packages/next/server/base-server.ts index 4aafc39b8a821..f5ff7587e3beb 100644 --- a/packages/next/server/base-server.ts +++ b/packages/next/server/base-server.ts @@ -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' @@ -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' @@ -415,7 +414,7 @@ export default abstract class Server { } // Initialize next/config with the environment configuration - envConfig.setConfig({ + setConfig({ serverRuntimeConfig, publicRuntimeConfig, }) @@ -668,7 +667,14 @@ export default abstract class Server { } } - 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, @@ -1367,6 +1373,7 @@ export default abstract class Server { // getStaticPaths, then finish the data request on the client-side. // if ( + process.env.NEXT_RUNTIME !== 'edge' && this.minimalMode !== true && fallbackMode !== 'blocking' && ssgCacheKey && diff --git a/packages/next/server/node-web-streams-helper.ts b/packages/next/server/node-web-streams-helper.ts index 2ffaaf5bf131a..9e6bdfb203d7e 100644 --- a/packages/next/server/node-web-streams-helper.ts +++ b/packages/next/server/node-web-streams-helper.ts @@ -124,11 +124,11 @@ export function createBufferedTransformStream( } export function createFlushEffectStream( - handleFlushEffect: () => string + handleFlushEffect: () => Promise ): TransformStream { 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) @@ -149,17 +149,17 @@ export function renderToInitialStream({ } export function createHeadInjectionTransformStream( - inject: () => string + inject: () => Promise ): TransformStream { let injected = false return new TransformStream({ - transform(chunk, controller) { + async transform(chunk, controller) { const content = decodeText(chunk) let index if (!injected && (index = content.indexOf(' generateStaticHTML: boolean - flushEffectHandler?: () => string + flushEffectHandler?: () => Promise flushEffectsToHead: boolean } ): Promise> { @@ -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) diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index 5e5a0a8aed862..2540c3274ab29 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -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 @@ -1148,16 +1160,16 @@ export async function renderToHTML( _Component: NextComponentType ) => Promise ) { - const renderPage: RenderPage = ( + const renderPage: RenderPage = async ( options: ComponentsEnhancer = {} - ): RenderPageResult | Promise => { + ): Promise => { 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( @@ -1184,7 +1196,7 @@ export async function renderToHTML( ) } - const html = ReactDOMServer.renderToString( + const html = await renderToString( {renderPageTree(EnhancedApp, EnhancedComponent, { @@ -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() @@ -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 => { + return renderToString(styledJsxFlushEffect()) } return continueFromInitialStream(initialStream, { @@ -1459,7 +1470,7 @@ export async function renderToHTML( ) - const documentHTML = ReactDOMServer.renderToStaticMarkup(document) + const documentHTML = await renderToStaticMarkup(document) if (process.env.NODE_ENV !== 'production') { const nonRenderedComponents = [] diff --git a/packages/next/server/web-server.ts b/packages/next/server/web-server.ts index 77fe205d393d0..97d001f3dbdae 100644 --- a/packages/next/server/web-server.ts +++ b/packages/next/server/web-server.ts @@ -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' @@ -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: {