From 80a8675d651628521a0f7f0c696fc77f51944b36 Mon Sep 17 00:00:00 2001 From: Cong-Cong Pan Date: Fri, 10 Jul 2026 11:03:18 +0800 Subject: [PATCH 1/2] fix(runtime): exclude the RSC client runtime from web bundles when `server.rsc` is disabled --- .changeset/clean-rsc-web-runtime.md | 5 ++++ .../plugin-runtime/src/cli/ssr/index.ts | 3 ++ .../src/router/runtime/plugin.tsx | 28 +++++++++++-------- 3 files changed, 25 insertions(+), 11 deletions(-) create mode 100644 .changeset/clean-rsc-web-runtime.md diff --git a/.changeset/clean-rsc-web-runtime.md b/.changeset/clean-rsc-web-runtime.md new file mode 100644 index 000000000000..3d233bfbd863 --- /dev/null +++ b/.changeset/clean-rsc-web-runtime.md @@ -0,0 +1,5 @@ +--- +'@modern-js/runtime': patch +--- + +fix(runtime): exclude the RSC client runtime from web bundles when `server.rsc` is disabled diff --git a/packages/runtime/plugin-runtime/src/cli/ssr/index.ts b/packages/runtime/plugin-runtime/src/cli/ssr/index.ts index 98aa2863eb67..02bafb4373c8 100644 --- a/packages/runtime/plugin-runtime/src/cli/ssr/index.ts +++ b/packages/runtime/plugin-runtime/src/cli/ssr/index.ts @@ -111,6 +111,9 @@ const ssrBuilderPlugin = ( ? JSON.stringify('node') : JSON.stringify('browser'), 'process.env.MODERN_SSR_ENV': JSON.stringify(ssrEnv), + 'process.env.MODERN_RSC': JSON.stringify( + Boolean(userConfig.server?.rsc), + ), }, }, output: { diff --git a/packages/runtime/plugin-runtime/src/router/runtime/plugin.tsx b/packages/runtime/plugin-runtime/src/router/runtime/plugin.tsx index 6f9b7834852b..9bc1aa37e192 100644 --- a/packages/runtime/plugin-runtime/src/router/runtime/plugin.tsx +++ b/packages/runtime/plugin-runtime/src/router/runtime/plugin.tsx @@ -152,10 +152,12 @@ export const routerPlugin = ( const RouterWrapper = (props: any) => { const routerResult = useRouterCreation( - { - ...props, - rscPayload: props?.rscPayload, - }, + process.env.MODERN_RSC + ? { + ...props, + rscPayload: props?.rscPayload, + } + : props, { api: api as any, createRoutes, @@ -166,7 +168,6 @@ export const routerPlugin = ( ); // Only cache router instance, routes are always from routerResult - // rscPayload is stable after first render, so we only create router once const router = useMemo(() => { if (cachedRouter) { return cachedRouter; @@ -244,9 +245,16 @@ function useRouterCreation(props: any, options: UseRouterCreationOptions) { : baseUrl; const { unstable_getBlockNavState: getBlockNavState } = runtimeContext; - const rscPayload = props?.rscPayload ? safeUse(props.rscPayload) : null; + // Keep the define expression inline at every RSC branch. Rspack can then + // remove the RSC imports in both production and development compilations. + const rscPayload = + process.env.MODERN_RSC && props?.rscPayload + ? safeUse(props.rscPayload) + : null; - let hydrationData = window._ROUTER_DATA || rscPayload; + let hydrationData = process.env.MODERN_RSC + ? window._ROUTER_DATA || rscPayload + : window._ROUTER_DATA; return useMemo(() => { if (hydrationData?.errors) { @@ -256,10 +264,8 @@ function useRouterCreation(props: any, options: UseRouterCreationOptions) { }; } - const isRscClient = getGlobalIsRscClient(); - let routes: RouteObject[] | null = null; - if (isRscClient) { + if (process.env.MODERN_RSC && getGlobalIsRscClient()) { routes = createRoutes ? createRoutes() : createRouteObjectsFromConfig({ @@ -282,7 +288,7 @@ function useRouterCreation(props: any, options: UseRouterCreationOptions) { const hooks = api.getHooks(); - if (rscPayload) { + if (process.env.MODERN_RSC && rscPayload) { try { const router = createClientRouterFromPayload( rscPayload, From 013aebd94ddcb0b80fd987d1f90d1f13d5487a78 Mon Sep 17 00:00:00 2001 From: Cong-Cong Pan Date: Fri, 10 Jul 2026 14:50:37 +0800 Subject: [PATCH 2/2] fix(runtime): exclude hydration runtime from CSR bundles --- .changeset/clean-csr-hydration-runtime.md | 5 ++++ .../plugin-runtime/src/cli/ssr/index.ts | 5 +++- .../src/core/browser/hydrate.tsx | 13 +++++++-- .../plugin-runtime/src/core/browser/index.tsx | 28 ++++++------------- .../src/router/runtime/plugin.tsx | 10 +++---- 5 files changed, 34 insertions(+), 27 deletions(-) create mode 100644 .changeset/clean-csr-hydration-runtime.md diff --git a/.changeset/clean-csr-hydration-runtime.md b/.changeset/clean-csr-hydration-runtime.md new file mode 100644 index 000000000000..97caaf422804 --- /dev/null +++ b/.changeset/clean-csr-hydration-runtime.md @@ -0,0 +1,5 @@ +--- +'@modern-js/runtime': patch +--- + +fix(runtime): exclude the SSR hydration runtime from CSR bundles diff --git a/packages/runtime/plugin-runtime/src/cli/ssr/index.ts b/packages/runtime/plugin-runtime/src/cli/ssr/index.ts index 02bafb4373c8..153f32c5e61e 100644 --- a/packages/runtime/plugin-runtime/src/cli/ssr/index.ts +++ b/packages/runtime/plugin-runtime/src/cli/ssr/index.ts @@ -111,7 +111,10 @@ const ssrBuilderPlugin = ( ? JSON.stringify('node') : JSON.stringify('browser'), 'process.env.MODERN_SSR_ENV': JSON.stringify(ssrEnv), - 'process.env.MODERN_RSC': JSON.stringify( + 'process.env.MODERN_ENABLE_HYDRATION': JSON.stringify( + isUseSSRBundle(userConfig), + ), + 'process.env.MODERN_ENABLE_RSC': JSON.stringify( Boolean(userConfig.server?.rsc), ), }, diff --git a/packages/runtime/plugin-runtime/src/core/browser/hydrate.tsx b/packages/runtime/plugin-runtime/src/core/browser/hydrate.tsx index dd41c94b7b4c..74a8f5b359ef 100644 --- a/packages/runtime/plugin-runtime/src/core/browser/hydrate.tsx +++ b/packages/runtime/plugin-runtime/src/core/browser/hydrate.tsx @@ -1,12 +1,21 @@ import { loadableReady } from '@loadable/component'; -import { normalizePathname } from '@modern-js/runtime-utils/url'; +import { SSR_HYDRATION_ID_PREFIX } from '@modern-js/utils/universal/constants'; import type React from 'react'; -import type { Root } from 'react-dom/client'; +import { type Root, hydrateRoot as hydrateReactRoot } from 'react-dom/client'; import { RenderLevel } from '../constants'; import type { TRuntimeContext } from '../context/runtime'; import { wrapRuntimeContextProvider } from '../react/wrapper'; import { WithCallback } from './withCallback'; +export async function hydrateWithReact( + App: React.ReactElement, + rootElement: HTMLElement, +) { + return hydrateReactRoot(rootElement, App, { + identifierPrefix: SSR_HYDRATION_ID_PREFIX, + }); +} + export function hydrateRoot( App: React.ReactElement, context: TRuntimeContext, diff --git a/packages/runtime/plugin-runtime/src/core/browser/index.tsx b/packages/runtime/plugin-runtime/src/core/browser/index.tsx index 46c7e508e35e..8574177f6c20 100644 --- a/packages/runtime/plugin-runtime/src/core/browser/index.tsx +++ b/packages/runtime/plugin-runtime/src/core/browser/index.tsx @@ -1,13 +1,13 @@ -import { SSR_HYDRATION_ID_PREFIX } from '@modern-js/utils/universal/constants'; import cookieTool from 'cookie'; import type React from 'react'; -// aliased because this file already has Modern's own `hydrateRoot` from './hydrate' -import { createRoot, hydrateRoot as hydrateReactRoot } from 'react-dom/client'; +import { createRoot } from 'react-dom/client'; import { getGlobalInternalRuntimeContext } from '../context'; import { type TRuntimeContext, getInitialContext } from '../context/runtime'; import { wrapRuntimeContextProvider } from '../react/wrapper'; import type { SSRContainer } from '../types'; -import { hydrateRoot } from './hydrate'; +import { hydrateRoot, hydrateWithReact } from './hydrate'; + +export { hydrateWithReact }; const getQuery = () => window.location.search @@ -100,12 +100,12 @@ export async function render( return renderWithReact(App, rootElement); } - async function ModernHydrate(App: React.ReactElement) { - return hydrateWithReact(App, rootElement); - } + // we should hydrateRoot only when SSR or SSG is enabled + if (process.env.MODERN_ENABLE_HYDRATION && window._SSR_DATA) { + async function ModernHydrate(App: React.ReactElement) { + return hydrateWithReact(App, rootElement); + } - // we should hydateRoot only when ssr - if (window._SSR_DATA) { return hydrateRoot(App, context, ModernRender, ModernHydrate); } return ModernRender(wrapRuntimeContextProvider(App, context)); @@ -123,13 +123,3 @@ export async function renderWithReact( root.render(App); return root; } - -export async function hydrateWithReact( - App: React.ReactElement, - rootElement: HTMLElement, -) { - const root = hydrateReactRoot(rootElement, App, { - identifierPrefix: SSR_HYDRATION_ID_PREFIX, - }); - return root; -} diff --git a/packages/runtime/plugin-runtime/src/router/runtime/plugin.tsx b/packages/runtime/plugin-runtime/src/router/runtime/plugin.tsx index 9bc1aa37e192..b1de799eee54 100644 --- a/packages/runtime/plugin-runtime/src/router/runtime/plugin.tsx +++ b/packages/runtime/plugin-runtime/src/router/runtime/plugin.tsx @@ -152,7 +152,7 @@ export const routerPlugin = ( const RouterWrapper = (props: any) => { const routerResult = useRouterCreation( - process.env.MODERN_RSC + process.env.MODERN_ENABLE_RSC ? { ...props, rscPayload: props?.rscPayload, @@ -248,11 +248,11 @@ function useRouterCreation(props: any, options: UseRouterCreationOptions) { // Keep the define expression inline at every RSC branch. Rspack can then // remove the RSC imports in both production and development compilations. const rscPayload = - process.env.MODERN_RSC && props?.rscPayload + process.env.MODERN_ENABLE_RSC && props?.rscPayload ? safeUse(props.rscPayload) : null; - let hydrationData = process.env.MODERN_RSC + let hydrationData = process.env.MODERN_ENABLE_RSC ? window._ROUTER_DATA || rscPayload : window._ROUTER_DATA; @@ -265,7 +265,7 @@ function useRouterCreation(props: any, options: UseRouterCreationOptions) { } let routes: RouteObject[] | null = null; - if (process.env.MODERN_RSC && getGlobalIsRscClient()) { + if (process.env.MODERN_ENABLE_RSC && getGlobalIsRscClient()) { routes = createRoutes ? createRoutes() : createRouteObjectsFromConfig({ @@ -288,7 +288,7 @@ function useRouterCreation(props: any, options: UseRouterCreationOptions) { const hooks = api.getHooks(); - if (process.env.MODERN_RSC && rscPayload) { + if (process.env.MODERN_ENABLE_RSC && rscPayload) { try { const router = createClientRouterFromPayload( rscPayload,