Skip to content

Commit 9118a0b

Browse files
authored
Keep server code out of browser chunks (#76660)
Reduces the main chunk's size for a hello world app ~from 60.2 kB to 51.6 kB~ from 53.5 kB to 45.6 kB (after #76622 got merged). In the past we were a bit sloppy with using modules from `src/server` also in universally used modules (server & browser). This led to code ending up in browser chunks that's only meant for the server, e.g. async local storage modules. We did have already quite a few conditional requires with `typeof window` checks for those modules, but not everywhere. This is fixed in this PR. And shared modules are also moved out of `src/server` into either `src/shared/lib` or `src/client`. A simple smoke test is added to ensure we don't regress here, at least for a simple hello world app. A better verification might be some kind of linter rule though. In addition, we don't remove the `typeof window` SWC optimization for Next.js modules anymore (which is still done for other `node_modules`, see #62051), to ensure that guarded code becomes dead code and is eliminated. Ideally, SWC would take care of the DCE solely by using `typeof window` checks, without needing to manually split modules and dynamically requiring them, which is very tedious and we'd like to avoid. But this is currently not the case. Another option worth exploring is using the ESM modules from `next/dist/esm` in hopes of yielding better DCE results.
1 parent f125638 commit 9118a0b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+465
-340
lines changed

packages/next/src/build/normalize-catchall-routes.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isInterceptionRouteAppPath } from '../server/lib/interception-routes'
1+
import { isInterceptionRouteAppPath } from '../shared/lib/router/utils/interception-routes'
22
import { AppPathnameNormalizer } from '../server/normalizers/built/app/app-pathname-normalizer'
33

44
/**

packages/next/src/build/swc/options.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,10 @@ export function getLoaderSWCOptions({
513513
options.cjsRequireOptimizer = undefined
514514
// Disable optimizer for node_modules in app browser layer, to avoid unnecessary replacement.
515515
// e.g. typeof window could result differently in js worker or browser.
516-
if (options.jsc.transform.optimizer.globals?.typeofs) {
516+
if (
517+
options.jsc.transform.optimizer.globals?.typeofs &&
518+
!filename.includes(nextDirname)
519+
) {
517520
delete options.jsc.transform.optimizer.globals.typeofs.window
518521
}
519522
}

packages/next/src/build/utils.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ import { normalizeAppPath } from '../shared/lib/router/utils/app-paths'
6969
import { denormalizeAppPagePath } from '../shared/lib/page-path/denormalize-app-path'
7070
import { RouteKind } from '../server/route-kind'
7171
import type { PageExtensions } from './page-extensions-type'
72-
import { isInterceptionRouteAppPath } from '../server/lib/interception-routes'
72+
import { isInterceptionRouteAppPath } from '../shared/lib/router/utils/interception-routes'
7373
import { checkIsRoutePPREnabled } from '../server/lib/experimental/ppr'
7474
import type { FallbackMode } from '../lib/fallback'
7575
import type { OutgoingHttpHeaders } from 'http'

packages/next/src/client/components/client-page.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@ export function ClientPageRoot({
5151
return <Component params={clientParams} searchParams={clientSearchParams} />
5252
} else {
5353
const { createRenderSearchParamsFromClient } =
54-
require('../../server/request/search-params.browser') as typeof import('../../server/request/search-params.browser')
54+
require('../request/search-params.browser') as typeof import('../request/search-params.browser')
5555
const clientSearchParams = createRenderSearchParamsFromClient(searchParams)
5656
const { createRenderParamsFromClient } =
57-
require('../../server/request/params.browser') as typeof import('../../server/request/params.browser')
57+
require('../request/params.browser') as typeof import('../request/params.browser')
5858
const clientParams = createRenderParamsFromClient(params)
5959

6060
return <Component params={clientParams} searchParams={clientSearchParams} />

packages/next/src/client/components/client-segment.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export function ClientSegmentRoot({
4545
return <Component {...slots} params={clientParams} />
4646
} else {
4747
const { createRenderParamsFromClient } =
48-
require('../../server/request/params.browser') as typeof import('../../server/request/params.browser')
48+
require('../request/params.browser') as typeof import('../request/params.browser')
4949
const clientParams = createRenderParamsFromClient(params)
5050
return <Component {...slots} params={clientParams} />
5151
}

packages/next/src/client/components/error-boundary.tsx

+13-5
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ import React, { type JSX } from 'react'
44
import { useUntrackedPathname } from './navigation-untracked'
55
import { isNextRouterError } from './is-next-router-error'
66
import { handleHardNavError } from './nav-failure-handler'
7-
import { workAsyncStorage } from '../../server/app-render/work-async-storage.external'
7+
8+
const workAsyncStorage =
9+
typeof window === 'undefined'
10+
? (
11+
require('../../server/app-render/work-async-storage.external') as typeof import('../../server/app-render/work-async-storage.external')
12+
).workAsyncStorage
13+
: undefined
814

915
const styles = {
1016
error: {
@@ -54,10 +60,12 @@ interface ErrorBoundaryHandlerState {
5460
// function crashes so we can maintain our previous cache
5561
// instead of caching the error page
5662
function HandleISRError({ error }: { error: any }) {
57-
const store = workAsyncStorage.getStore()
58-
if (store?.isRevalidate || store?.isStaticGeneration) {
59-
console.error(error)
60-
throw error
63+
if (workAsyncStorage) {
64+
const store = workAsyncStorage.getStore()
65+
if (store?.isRevalidate || store?.isStaticGeneration) {
66+
console.error(error)
67+
throw error
68+
}
6169
}
6270

6371
return null
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { getSegmentParam } from '../../server/app-render/get-segment-param'
21
import type { Segment } from '../../server/app-render/types'
32

43
export const matchSegment = (
@@ -19,17 +18,3 @@ export const matchSegment = (
1918
}
2019
return existingSegment[0] === segment[0] && existingSegment[1] === segment[1]
2120
}
22-
23-
/*
24-
* This function is used to determine if an existing segment can be overridden by the incoming segment.
25-
*/
26-
export const canSegmentBeOverridden = (
27-
existingSegment: Segment,
28-
segment: Segment
29-
): boolean => {
30-
if (Array.isArray(existingSegment) || !Array.isArray(segment)) {
31-
return false
32-
}
33-
34-
return getSegmentParam(existingSegment)?.param === segment[0]
35-
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
'use client'
2+
3+
import { Suspense, use } from 'react'
4+
import type { StreamingMetadataResolvedState } from './types'
5+
6+
export const AsyncMetadata =
7+
typeof window === 'undefined'
8+
? (
9+
require('./server-inserted-metadata') as typeof import('./server-inserted-metadata')
10+
).ServerInsertMetadata
11+
: (
12+
require('./browser-resolved-metadata') as typeof import('./browser-resolved-metadata')
13+
).BrowserResolvedMetadata
14+
15+
function MetadataOutlet({
16+
promise,
17+
}: {
18+
promise: Promise<StreamingMetadataResolvedState>
19+
}) {
20+
const { error, digest } = use(promise)
21+
if (error) {
22+
if (digest) {
23+
// The error will lose its original digest after passing from server layer to client layer;
24+
// We recover the digest property here to override the React created one if original digest exists.
25+
;(error as any).digest = digest
26+
}
27+
throw error
28+
}
29+
return null
30+
}
31+
32+
export function AsyncMetadataOutlet({
33+
promise,
34+
}: {
35+
promise: Promise<StreamingMetadataResolvedState>
36+
}) {
37+
return (
38+
<Suspense fallback={null}>
39+
<MetadataOutlet promise={promise} />
40+
</Suspense>
41+
)
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { use } from 'react'
2+
import type { StreamingMetadataResolvedState } from './types'
3+
4+
export function BrowserResolvedMetadata({
5+
promise,
6+
}: {
7+
promise: Promise<StreamingMetadataResolvedState>
8+
}) {
9+
const { metadata, error } = use(promise)
10+
// If there's metadata error on client, discard the browser metadata
11+
// and let metadata outlet deal with the error. This will avoid the duplication metadata.
12+
if (error) return null
13+
return metadata
14+
}

packages/next/src/lib/metadata/metadata-boundary.tsx renamed to packages/next/src/client/components/metadata/metadata-boundary.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
METADATA_BOUNDARY_NAME,
55
VIEWPORT_BOUNDARY_NAME,
66
OUTLET_BOUNDARY_NAME,
7-
} from './metadata-constants'
7+
} from '../../../lib/metadata/metadata-constants'
88

99
// We use a namespace object to allow us to recover the name of the function
1010
// at runtime even when production bundling/minification is used.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { use, useContext } from 'react'
2+
import {
3+
type MetadataResolver,
4+
ServerInsertedMetadataContext,
5+
} from '../../../shared/lib/server-inserted-metadata.shared-runtime'
6+
import type { StreamingMetadataResolvedState } from './types'
7+
8+
// Receives a metadata resolver setter from the context, and will pass the metadata resolving promise to
9+
// the context where we gonna use it to resolve the metadata, and render as string to append in <body>.
10+
const useServerInsertedMetadata = (metadataResolver: MetadataResolver) => {
11+
const setMetadataResolver = useContext(ServerInsertedMetadataContext)
12+
13+
if (setMetadataResolver) {
14+
setMetadataResolver(metadataResolver)
15+
}
16+
}
17+
18+
export function ServerInsertMetadata({
19+
promise,
20+
}: {
21+
promise: Promise<StreamingMetadataResolvedState>
22+
}) {
23+
// Apply use() to the metadata promise to suspend the rendering in SSR.
24+
const { metadata } = use(promise)
25+
// Insert metadata into the HTML stream through the `useServerInsertedMetadata`
26+
useServerInsertedMetadata(() => metadata)
27+
28+
return null
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export type StreamingMetadataResolvedState = {
2+
metadata: React.ReactNode
3+
error: unknown | null
4+
digest: string | undefined
5+
}

packages/next/src/client/components/navigation.ts

+11-5
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@ import {
1515
import { getSegmentValue } from './router-reducer/reducers/get-segment-value'
1616
import { PAGE_SEGMENT_KEY, DEFAULT_SEGMENT_KEY } from '../../shared/lib/segment'
1717
import { ReadonlyURLSearchParams } from './navigation.react-server'
18-
import { useDynamicRouteParams } from '../../server/app-render/dynamic-rendering'
18+
19+
const useDynamicRouteParams =
20+
typeof window === 'undefined'
21+
? (
22+
require('../../server/app-render/dynamic-rendering') as typeof import('../../server/app-render/dynamic-rendering')
23+
).useDynamicRouteParams
24+
: undefined
1925

2026
/**
2127
* A [Client Component](https://nextjs.org/docs/app/building-your-application/rendering/client-components) hook
@@ -84,7 +90,7 @@ export function useSearchParams(): ReadonlyURLSearchParams {
8490
*/
8591
// Client components API
8692
export function usePathname(): string {
87-
useDynamicRouteParams('usePathname()')
93+
useDynamicRouteParams?.('usePathname()')
8894

8995
// In the case where this is `null`, the compat types added in `next-env.d.ts`
9096
// will add a new overload that changes the return type to include `null`.
@@ -144,7 +150,7 @@ export function useRouter(): AppRouterInstance {
144150
*/
145151
// Client components API
146152
export function useParams<T extends Params = Params>(): T {
147-
useDynamicRouteParams('useParams()')
153+
useDynamicRouteParams?.('useParams()')
148154

149155
return useContext(PathParamsContext) as T
150156
}
@@ -215,7 +221,7 @@ function getSelectedLayoutSegmentPath(
215221
export function useSelectedLayoutSegments(
216222
parallelRouteKey: string = 'children'
217223
): string[] {
218-
useDynamicRouteParams('useSelectedLayoutSegments()')
224+
useDynamicRouteParams?.('useSelectedLayoutSegments()')
219225

220226
const context = useContext(LayoutRouterContext)
221227
// @ts-expect-error This only happens in `pages`. Type is overwritten in navigation.d.ts
@@ -246,7 +252,7 @@ export function useSelectedLayoutSegments(
246252
export function useSelectedLayoutSegment(
247253
parallelRouteKey: string = 'children'
248254
): string | null {
249-
useDynamicRouteParams('useSelectedLayoutSegment()')
255+
useDynamicRouteParams?.('useSelectedLayoutSegment()')
250256

251257
const selectedLayoutSegments = useSelectedLayoutSegments(parallelRouteKey)
252258

packages/next/src/client/components/redirect.ts

+12-9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { actionAsyncStorage } from '../../server/app-render/action-async-storage.external'
21
import { RedirectStatusCode } from './redirect-status-code'
32
import {
43
RedirectType,
@@ -7,6 +6,13 @@ import {
76
REDIRECT_ERROR_CODE,
87
} from './redirect-error'
98

9+
const actionAsyncStorage =
10+
typeof window === 'undefined'
11+
? (
12+
require('../../server/app-render/action-async-storage.external') as typeof import('../../server/app-render/action-async-storage.external')
13+
).actionAsyncStorage
14+
: undefined
15+
1016
export function getRedirectError(
1117
url: string,
1218
type: RedirectType,
@@ -34,14 +40,11 @@ export function redirect(
3440
url: string,
3541
type?: RedirectType
3642
): never {
37-
const actionStore = actionAsyncStorage.getStore()
38-
const redirectType =
39-
type || (actionStore?.isAction ? RedirectType.push : RedirectType.replace)
40-
throw getRedirectError(
41-
url,
42-
redirectType,
43-
RedirectStatusCode.TemporaryRedirect
44-
)
43+
type ??= actionAsyncStorage?.getStore()?.isAction
44+
? RedirectType.push
45+
: RedirectType.replace
46+
47+
throw getRedirectError(url, type, RedirectStatusCode.TemporaryRedirect)
4548
}
4649

4750
/**

packages/next/src/client/components/router-reducer/compute-changed-path.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type {
22
FlightRouterState,
33
Segment,
44
} from '../../../server/app-render/types'
5-
import { INTERCEPTION_ROUTE_MARKERS } from '../../../server/lib/interception-routes'
5+
import { INTERCEPTION_ROUTE_MARKERS } from '../../../shared/lib/router/utils/interception-routes'
66
import type { Params } from '../../../server/request/params'
77
import {
88
isGroupSegment,

packages/next/src/client/components/router-reducer/reducers/has-interception-route-in-current-tree.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { FlightRouterState } from '../../../../server/app-render/types'
2-
import { isInterceptionRouteAppPath } from '../../../../server/lib/interception-routes'
2+
import { isInterceptionRouteAppPath } from '../../../../shared/lib/router/utils/interception-routes'
33

44
export function hasInterceptionRouteInCurrentTree([
55
segment,

packages/next/src/client/components/segment-cache-impl/cache.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import {
4646
encodeChildSegmentKey,
4747
encodeSegment,
4848
ROOT_SEGMENT_KEY,
49-
} from '../../../server/app-render/segment-value-encoding'
49+
} from '../../../shared/lib/segment-cache/segment-value-encoding'
5050
import type {
5151
FlightRouterState,
5252
NavigationFlightResponse,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { isBailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr'
2+
import { isNextRouterError } from './is-next-router-error'
3+
4+
export function unstable_rethrow(error: unknown): void {
5+
if (isNextRouterError(error) || isBailoutToCSRError(error)) {
6+
throw error
7+
}
8+
9+
if (error instanceof Error && 'cause' in error) {
10+
unstable_rethrow(error.cause)
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { isHangingPromiseRejectionError } from '../../server/dynamic-rendering-utils'
2+
import { isPostpone } from '../../server/lib/router-utils/is-postpone'
3+
import { isBailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr'
4+
import { isNextRouterError } from './is-next-router-error'
5+
import { isDynamicPostpone } from '../../server/app-render/dynamic-rendering'
6+
import { isDynamicServerError } from './hooks-server-context'
7+
8+
export function unstable_rethrow(error: unknown): void {
9+
if (
10+
isNextRouterError(error) ||
11+
isBailoutToCSRError(error) ||
12+
isDynamicServerError(error) ||
13+
isDynamicPostpone(error) ||
14+
isPostpone(error) ||
15+
isHangingPromiseRejectionError(error)
16+
) {
17+
throw error
18+
}
19+
20+
if (error instanceof Error && 'cause' in error) {
21+
unstable_rethrow(error.cause)
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,15 @@
1-
import { isDynamicUsageError } from '../../export/helpers/is-dynamic-usage-error'
2-
import { isHangingPromiseRejectionError } from '../../server/dynamic-rendering-utils'
3-
import { isPostpone } from '../../server/lib/router-utils/is-postpone'
4-
import { isBailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr'
5-
import { isNextRouterError } from './is-next-router-error'
6-
71
/**
82
* This function should be used to rethrow internal Next.js errors so that they can be handled by the framework.
93
* When wrapping an API that uses errors to interrupt control flow, you should use this function before you do any error handling.
104
* This function will rethrow the error if it is a Next.js error so it can be handled, otherwise it will do nothing.
115
*
126
* Read more: [Next.js Docs: `unstable_rethrow`](https://nextjs.org/docs/app/api-reference/functions/unstable_rethrow)
137
*/
14-
export function unstable_rethrow(error: unknown): void {
15-
if (
16-
isNextRouterError(error) ||
17-
isBailoutToCSRError(error) ||
18-
isDynamicUsageError(error) ||
19-
isPostpone(error) ||
20-
isHangingPromiseRejectionError(error)
21-
) {
22-
throw error
23-
}
24-
25-
if (error instanceof Error && 'cause' in error) {
26-
unstable_rethrow(error.cause)
27-
}
28-
}
8+
export const unstable_rethrow =
9+
typeof window === 'undefined'
10+
? (
11+
require('./unstable-rethrow.server') as typeof import('./unstable-rethrow.server')
12+
).unstable_rethrow
13+
: (
14+
require('./unstable-rethrow.browser') as typeof import('./unstable-rethrow.browser')
15+
).unstable_rethrow

0 commit comments

Comments
 (0)