Skip to content

Commit e3f0a1a

Browse files
authored
[Segment Cache] Omit from bundle if flag disabled (#76622)
Alternative to #76597. This works by importing all Segment Cache related code through a single entry point (segment-cache.ts), and wrapping each export in a feature flag check. This fixes the size regression in v15.2 but long term I don't love this approach. Since experimental flags are an essential part of our workflow, we should establish a better pattern for dead code elimination. Ideally it would be done at the bundler level, like how React's build process works. In the React repo, you don't even need to add any extra configuration per experiment — if the code is not reachable, it gets stripped from the build automatically by Rollup. Or, shorter term, we could stub out experimental modules at build time by updating the build config, i.e. a more automated version of what I'm doing manually for this particular flag.
1 parent 61b81f4 commit e3f0a1a

18 files changed

+151
-55
lines changed

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@
6161
"packages/next/src/server/app-render/after-task-async-storage-instance.ts",
6262
"packages/next/src/server/app-render/clean-async-snapshot-instance.ts",
6363
"packages/next/src/server/app-render/work-async-storage-instance.ts",
64-
"packages/next/src/server/app-render/work-unit-async-storage-instance.ts"
64+
"packages/next/src/server/app-render/work-unit-async-storage-instance.ts",
65+
"packages/next/src/client/components/segment-cache-impl/*"
6566
],
6667
// Disable TypeScript surveys.
6768
"typescript.surveys.enabled": false,

packages/next/errors.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -651,5 +651,6 @@
651651
"650": "experimental.nodeMiddleware",
652652
"651": "Unexpected module type %s",
653653
"652": "Expected cached value for cache key %s not to be a %s kind, got \"FETCH\" instead.",
654-
"653": "Expected cached value for cache key %s to be a \"FETCH\" kind, got %s instead."
654+
"653": "Expected cached value for cache key %s to be a \"FETCH\" kind, got %s instead.",
655+
"654": "Segment Cache experiment is not enabled. This is a bug in Next.js."
655656
}

packages/next/src/client/app-dir/form.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,7 @@ import {
1616
hasUnsupportedSubmitterAttributes,
1717
type FormProps,
1818
} from '../form-shared'
19-
import {
20-
mountLinkInstance,
21-
unmountLinkInstance,
22-
} from '../components/segment-cache/links'
19+
import { mountLinkInstance, unmountLinkInstance } from '../components/links'
2320

2421
export type { FormProps }
2522

packages/next/src/client/app-dir/link.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
mountLinkInstance,
1717
onNavigationIntent,
1818
unmountLinkInstance,
19-
} from '../components/segment-cache/links'
19+
} from '../components/links'
2020

2121
type Url = string | UrlObject
2222
type RequiredKeys<T> = {

packages/next/src/client/components/app-router.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,11 @@ import type { FlightRouterState } from '../../server/app-render/types'
5858
import { useNavFailureHandler } from './nav-failure-handler'
5959
import { useServerActionDispatcher } from '../app-call-server'
6060
import type { AppRouterActionQueue } from '../../shared/lib/router/action-queue'
61-
import { prefetch as prefetchWithSegmentCache } from '../components/segment-cache/prefetch'
61+
import { prefetch as prefetchWithSegmentCache } from './segment-cache'
6262
import { getRedirectTypeFromError, getURLFromRedirectError } from './redirect'
6363
import { isRedirectError, RedirectType } from './redirect-error'
6464
import { prefetchReducer } from './router-reducer/reducers/prefetch-reducer'
65-
import { pingVisibleLinks } from './segment-cache/links'
65+
import { pingVisibleLinks } from './links'
6666

6767
const globalMutable: {
6868
pendingMpaPath?: string

packages/next/src/client/components/segment-cache/links.ts renamed to packages/next/src/client/components/links.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
import type { FlightRouterState } from '../../../server/app-render/types'
2-
import type { AppRouterInstance } from '../../../shared/lib/app-router-context.shared-runtime'
3-
import { getCurrentAppRouterState } from '../../../shared/lib/router/action-queue'
4-
import { createPrefetchURL } from '../app-router'
5-
import { PrefetchKind } from '../router-reducer/router-reducer-types'
6-
import { getCurrentCacheVersion } from './cache'
7-
import { createCacheKey } from './cache-key'
1+
import type { FlightRouterState } from '../../server/app-render/types'
2+
import type { AppRouterInstance } from '../../shared/lib/app-router-context.shared-runtime'
3+
import { getCurrentAppRouterState } from '../../shared/lib/router/action-queue'
4+
import { createPrefetchURL } from './app-router'
5+
import { PrefetchKind } from './router-reducer/router-reducer-types'
6+
import { getCurrentCacheVersion } from './segment-cache'
7+
import { createCacheKey } from './segment-cache'
88
import {
99
type PrefetchTask,
1010
PrefetchPriority,
1111
schedulePrefetchTask as scheduleSegmentPrefetchTask,
1212
cancelPrefetchTask,
1313
bumpPrefetchTask,
14-
} from './scheduler'
14+
} from './segment-cache'
1515

1616
type LinkElement = HTMLAnchorElement | SVGAElement | HTMLFormElement
1717

packages/next/src/client/components/router-reducer/reducers/navigate-reducer.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
navigate as navigateUsingSegmentCache,
3333
NavigationResultTag,
3434
type NavigationResult,
35-
} from '../../segment-cache/navigation'
35+
} from '../../segment-cache'
3636

3737
export function handleExternalUrl(
3838
state: ReadonlyReducerState,
@@ -203,9 +203,6 @@ export function navigateReducer(
203203
// Temporary glue code between the router reducer and the new navigation
204204
// implementation. Eventually we'll rewrite the router reducer to a
205205
// state machine.
206-
// TODO: Currently this always returns an async result, but in the future
207-
// it will return a sync result if the navigation was prefetched. Hence
208-
// a result type that's more complicated than you might expect.
209206
const result = navigateUsingSegmentCache(
210207
url,
211208
state.cache,

packages/next/src/client/components/router-reducer/reducers/prefetch-reducer.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
getOrCreatePrefetchCacheEntry,
99
prunePrefetchCache,
1010
} from '../prefetch-cache-utils'
11-
1211
export const prefetchQueue = new PromiseQueue(5)
1312

1413
export const prefetchReducer = process.env.__NEXT_CLIENT_SEGMENT_CACHE

packages/next/src/client/components/router-reducer/reducers/refresh-reducer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { createEmptyCacheNode } from '../../app-router'
1616
import { handleSegmentMismatch } from '../handle-segment-mismatch'
1717
import { hasInterceptionRouteInCurrentTree } from './has-interception-route-in-current-tree'
1818
import { refreshInactiveParallelSegments } from '../refetch-inactive-parallel-segments'
19-
import { revalidateEntireCache } from '../../segment-cache/cache'
19+
import { revalidateEntireCache } from '../../segment-cache'
2020

2121
export function refreshReducer(
2222
state: ReadonlyReducerState,

packages/next/src/client/components/router-reducer/reducers/server-action-reducer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ import {
5656
extractInfoFromServerReferenceId,
5757
omitUnusedArgs,
5858
} from '../../../../shared/lib/server-reference-info'
59-
import { revalidateEntireCache } from '../../segment-cache/cache'
59+
import { revalidateEntireCache } from '../../segment-cache'
6060

6161
type FetchServerActionResult = {
6262
redirectLocation: URL | undefined

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ import type {
5252
} from '../../../server/app-render/types'
5353
import { normalizeFlightData } from '../../flight-data-helpers'
5454
import { STATIC_STALETIME_MS } from '../router-reducer/prefetch-cache-utils'
55-
import { pingVisibleLinks } from './links'
55+
import { pingVisibleLinks } from '../links'
5656

5757
// A note on async/await when working in the prefetch cache:
5858
//

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

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,7 @@ import {
2424
type RouteTree,
2525
} from './cache'
2626
import { createCacheKey } from './cache-key'
27-
28-
export const enum NavigationResultTag {
29-
MPA,
30-
Success,
31-
NoOp,
32-
Async,
33-
}
27+
import { NavigationResultTag } from '../segment-cache'
3428

3529
type MPANavigationResult = {
3630
tag: NavigationResultTag.MPA

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import type { FlightRouterState } from '../../../server/app-render/types'
2-
import { createPrefetchURL } from '../../components/app-router'
2+
import { createPrefetchURL } from '../app-router'
33
import { createCacheKey } from './cache-key'
4-
import { schedulePrefetchTask, PrefetchPriority } from './scheduler'
4+
import { schedulePrefetchTask } from './scheduler'
5+
import { PrefetchPriority } from '../segment-cache'
56

67
/**
78
* Entrypoint for prefetching a URL into the Segment Cache.

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

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
resetRevalidatingSegmentEntry,
2626
} from './cache'
2727
import type { RouteCacheKey } from './cache-key'
28+
import { PrefetchPriority } from '../segment-cache'
2829

2930
const scheduleMicrotask =
3031
typeof queueMicrotask === 'function'
@@ -136,27 +137,6 @@ const enum PrefetchTaskExitStatus {
136137
Done,
137138
}
138139

139-
/**
140-
* The priority of the prefetch task. Higher numbers are higher priority.
141-
*/
142-
export const enum PrefetchPriority {
143-
/**
144-
* Assigned to any visible link that was hovered/touched at some point. This
145-
* is not removed on mouse exit, because a link that was momentarily
146-
* hovered is more likely to to be interacted with than one that was not.
147-
*/
148-
Intent = 2,
149-
/**
150-
* The default priority for prefetch tasks.
151-
*/
152-
Default = 1,
153-
/**
154-
* Assigned to tasks when they spawn non-blocking background work, like
155-
* revalidating a partially cached entry to see if more data is available.
156-
*/
157-
Background = 0,
158-
}
159-
160140
/**
161141
* Prefetch tasks are processed in two phases: first the route tree is fetched,
162142
* then the segments. We use this to priortize tasks that have not yet fetched
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/**
2+
* Entry point to the Segment Cache implementation.
3+
*
4+
* All code related to the Segment Cache lives `segment-cache-impl` directory.
5+
* Callers access it through this indirection.
6+
*
7+
* This is to ensure the code is dead code eliminated from the bundle if the
8+
* flag is disabled.
9+
*
10+
* TODO: This is super tedious. Since experimental flags are an essential part
11+
* of our workflow, we should establish a better pattern for dead code
12+
* elimination. Ideally it would be done at the bundler level, like how React's
13+
* build process works. In the React repo, you don't even need to add any extra
14+
* configuration per experiment — if the code is not reachable, it gets stripped
15+
* from the build automatically by Rollup. Or, shorter term, we could stub out
16+
* experimental modules at build time by updating the build config, i.e. a more
17+
* automated version of what I'm doing manually in this file.
18+
*/
19+
20+
export type { NavigationResult } from './segment-cache-impl/navigation'
21+
export type { PrefetchTask } from './segment-cache-impl/scheduler'
22+
23+
const notEnabled: any = () => {
24+
throw new Error(
25+
'Segment Cache experiment is not enabled. This is a bug in Next.js.'
26+
)
27+
}
28+
29+
export const prefetch: typeof import('./segment-cache-impl/prefetch').prefetch =
30+
process.env.__NEXT_CLIENT_SEGMENT_CACHE
31+
? function (...args) {
32+
return require('./segment-cache-impl/prefetch').prefetch(...args)
33+
}
34+
: notEnabled
35+
36+
export const navigate: typeof import('./segment-cache-impl/navigation').navigate =
37+
process.env.__NEXT_CLIENT_SEGMENT_CACHE
38+
? function (...args) {
39+
return require('./segment-cache-impl/navigation').navigate(...args)
40+
}
41+
: notEnabled
42+
43+
export const revalidateEntireCache: typeof import('./segment-cache-impl/cache').revalidateEntireCache =
44+
process.env.__NEXT_CLIENT_SEGMENT_CACHE
45+
? function (...args) {
46+
return require('./segment-cache-impl/cache').revalidateEntireCache(
47+
...args
48+
)
49+
}
50+
: notEnabled
51+
52+
export const getCurrentCacheVersion: typeof import('./segment-cache-impl/cache').getCurrentCacheVersion =
53+
process.env.__NEXT_CLIENT_SEGMENT_CACHE
54+
? function (...args) {
55+
return require('./segment-cache-impl/cache').getCurrentCacheVersion(
56+
...args
57+
)
58+
}
59+
: notEnabled
60+
61+
export const schedulePrefetchTask: typeof import('./segment-cache-impl/scheduler').schedulePrefetchTask =
62+
process.env.__NEXT_CLIENT_SEGMENT_CACHE
63+
? function (...args) {
64+
return require('./segment-cache-impl/scheduler').schedulePrefetchTask(
65+
...args
66+
)
67+
}
68+
: notEnabled
69+
70+
export const cancelPrefetchTask: typeof import('./segment-cache-impl/scheduler').cancelPrefetchTask =
71+
process.env.__NEXT_CLIENT_SEGMENT_CACHE
72+
? function (...args) {
73+
return require('./segment-cache-impl/scheduler').cancelPrefetchTask(
74+
...args
75+
)
76+
}
77+
: notEnabled
78+
79+
export const bumpPrefetchTask: typeof import('./segment-cache-impl/scheduler').bumpPrefetchTask =
80+
process.env.__NEXT_CLIENT_SEGMENT_CACHE
81+
? function (...args) {
82+
return require('./segment-cache-impl/scheduler').bumpPrefetchTask(
83+
...args
84+
)
85+
}
86+
: notEnabled
87+
88+
export const createCacheKey: typeof import('./segment-cache-impl/cache-key').createCacheKey =
89+
process.env.__NEXT_CLIENT_SEGMENT_CACHE
90+
? function (...args) {
91+
return require('./segment-cache-impl/cache-key').createCacheKey(...args)
92+
}
93+
: notEnabled
94+
95+
/**
96+
* Below are public constants. They're small enough that we don't need to
97+
* DCE them.
98+
*/
99+
100+
export const enum NavigationResultTag {
101+
MPA,
102+
Success,
103+
NoOp,
104+
Async,
105+
}
106+
107+
/**
108+
* The priority of the prefetch task. Higher numbers are higher priority.
109+
*/
110+
export const enum PrefetchPriority {
111+
/**
112+
* Assigned to any visible link that was hovered/touched at some point. This
113+
* is not removed on mouse exit, because a link that was momentarily
114+
* hovered is more likely to to be interacted with than one that was not.
115+
*/
116+
Intent = 2,
117+
/**
118+
* The default priority for prefetch tasks.
119+
*/
120+
Default = 1,
121+
/**
122+
* Assigned to tasks when they spawn non-blocking background work, like
123+
* revalidating a partially cached entry to see if more data is available.
124+
*/
125+
Background = 0,
126+
}

0 commit comments

Comments
 (0)