@samitouri / QOS-React / commits / b0c1dc01ec

[Flight] Add approximate parent context for FormatContext (#34601)

Flight doesn't have any semantically sound notion of a parent context. That's why we removed Server Context. Each root can really start anywhere in the tree when you refetch subtrees. Additionally when you dedupe elements they can end up in multiple different parent contexts. However, we do have a DEV only version of this with debugTask being tracked for the nearest parent element to track the context of properties inside of it. To apply certain DOM specific hints and optimizations when you render host components we need some information of the context. This is usually very local so doesn't suffer from the likelihood that you refetch in the middle. We'll also only use this information for optimistic hints and not hard semantics so getting it wrong isn't terrible. ``` <picture> <img /> </picture> <noscript> <p> <img /> </p> </noscript> ``` For example, in these cases we should exclude preloading the image but we have to know if that's the scope we're in. We can easily get this wrong if they're split or even if they're wrapped in client components that we don't know about like: ``` <NoScript> <p> <img /> </p> </NoScript> ``` However, getting it wrong in either direction is not the end of the world. It's about covering the common cases well.

Sebastian Markbåge committed Sep 25, 2025 at 12:05 UTC b0c1dc01ecbfaf81aa69f760b29dd76c02600792
5 files changed +101
packages/react-dom-bindings/src/server/ReactFlightServerConfigDOM.js
+14
@@ -61,3 +61,17 @@ export type Hints = Set<string>;
61 export function createHints(): Hints {
62 return new Set();
63 }
64 +
65 +export opaque type FormatContext = null;
66 +
67 +export function createRootFormatContext(): FormatContext {
68 + return null;
69 +}
70 +
71 +export function getChildFormatContext(
72 + parentContext: FormatContext,
73 + type: string,
74 + props: Object,
75 +): FormatContext {
76 + return parentContext;
77 +}
packages/react-server/src/ReactFlightServer.js
+45
@@ -49,6 +49,7 @@ import type {
49 Hints,
50 HintCode,
51 HintModel,
52 + FormatContext,
53 } from './ReactFlightServerConfig';
54 import type {ThenableState} from './ReactFlightThenable';
55 import type {
@@ -88,6 +89,8 @@ import {
89 supportsRequestStorage,
90 requestStorage,
91 createHints,
92 + createRootFormatContext,
93 + getChildFormatContext,
94 initAsyncDebugInfo,
95 markAsyncSequenceRootTask,
96 getCurrentAsyncSequence,
@@ -525,6 +528,7 @@ type Task = {
528 toJSON: (key: string, value: ReactClientValue) => ReactJSONValue,
529 keyPath: null | string, // parent server component keys
530 implicitSlot: boolean, // true if the root server component of this sequence had a null key
531 + formatContext: FormatContext, // an approximate parent context from host components
532 thenableState: ThenableState | null,
533 timed: boolean, // Profiling-only. Whether we need to track the completion time of this task.
534 time: number, // Profiling-only. The last time stamp emitted for this task.
@@ -758,6 +762,7 @@ function RequestInstance(
762 model,
763 null,
764 false,
765 + createRootFormatContext(),
766 abortSet,
767 timeOrigin,
768 null,
@@ -980,6 +985,7 @@ function serializeThenable(
985 (thenable: any), // will be replaced by the value before we retry. used for debug info.
986 task.keyPath, // the server component sequence continues through Promise-as-a-child.
987 task.implicitSlot,
988 + task.formatContext,
989 request.abortableTasks,
990 enableProfilerTimer &&
991 (enableComponentPerformanceTrack || enableAsyncDebugInfo)
@@ -1102,6 +1108,7 @@ function serializeReadableStream(
1108 task.model,
1109 task.keyPath,
1110 task.implicitSlot,
1111 + task.formatContext,
1112 request.abortableTasks,
1113 enableProfilerTimer &&
1114 (enableComponentPerformanceTrack || enableAsyncDebugInfo)
@@ -1197,6 +1204,7 @@ function serializeAsyncIterable(
1204 task.model,
1205 task.keyPath,
1206 task.implicitSlot,
1207 + task.formatContext,
1208 request.abortableTasks,
1209 enableProfilerTimer &&
1210 (enableComponentPerformanceTrack || enableAsyncDebugInfo)
@@ -2028,6 +2036,7 @@ function deferTask(request: Request, task: Task): ReactJSONValue {
2036 task.model, // the currently rendering element
2037 task.keyPath, // unlike outlineModel this one carries along context
2038 task.implicitSlot,
2039 + task.formatContext,
2040 request.abortableTasks,
2041 enableProfilerTimer &&
2042 (enableComponentPerformanceTrack || enableAsyncDebugInfo)
@@ -2048,6 +2057,7 @@ function outlineTask(request: Request, task: Task): ReactJSONValue {
2057 task.model, // the currently rendering element
2058 task.keyPath, // unlike outlineModel this one carries along context
2059 task.implicitSlot,
2060 + task.formatContext,
2061 request.abortableTasks,
2062 enableProfilerTimer &&
2063 (enableComponentPerformanceTrack || enableAsyncDebugInfo)
@@ -2214,6 +2224,22 @@ function renderElement(
2224 }
2225 }
2226 }
2227 + } else if (typeof type === 'string') {
2228 + const parentFormatContext = task.formatContext;
2229 + const newFormatContext = getChildFormatContext(
2230 + parentFormatContext,
2231 + type,
2232 + props,
2233 + );
2234 + if (parentFormatContext !== newFormatContext && props.children != null) {
2235 + // We've entered a new context. We need to create another Task which has
2236 + // the new context set up since it's not safe to push/pop in the middle of
2237 + // a tree. Additionally this means that any deduping within this tree now
2238 + // assumes the new context even if it's reused outside in a different context.
2239 + // We'll rely on this to dedupe the value later as we discover it again
2240 + // inside the returned element's tree.
2241 + outlineModelWithFormatContext(request, props.children, newFormatContext);
2242 + }
2243 }
2244 // For anything else, try it on the client instead.
2245 // We don't know if the client will support it or not. This might error on the
@@ -2530,6 +2556,7 @@ function createTask(
2556 model: ReactClientValue,
2557 keyPath: null | string,
2558 implicitSlot: boolean,
2559 + formatContext: FormatContext,
2560 abortSet: Set<Task>,
2561 lastTimestamp: number, // Profiling-only
2562 debugOwner: null | ReactComponentInfo, // DEV-only
@@ -2554,6 +2581,7 @@ function createTask(
2581 model,
2582 keyPath,
2583 implicitSlot,
2584 + formatContext: formatContext,
2585 ping: () => pingTask(request, task),
2586 toJSON: function (
2587 this:
@@ -2819,11 +2847,26 @@ function serializeDebugClientReference(
2847 }
2848
2849 function outlineModel(request: Request, value: ReactClientValue): number {
2850 + return outlineModelWithFormatContext(
2851 + request,
2852 + value,
2853 + // For deduped values we don't know which context it will be reused in
2854 + // so we have to assume that it's the root context.
2855 + createRootFormatContext(),
2856 + );
2857 +}
2858 +
2859 +function outlineModelWithFormatContext(
2860 + request: Request,
2861 + value: ReactClientValue,
2862 + formatContext: FormatContext,
2863 +): number {
2864 const newTask = createTask(
2865 request,
2866 value,
2867 null, // The way we use outlining is for reusing an object.
2868 false, // It makes no sense for that use case to be contextual.
2869 + formatContext, // Except for FormatContext we optimistically use it.
2870 request.abortableTasks,
2871 enableProfilerTimer &&
2872 (enableComponentPerformanceTrack || enableAsyncDebugInfo)
@@ -3071,6 +3114,7 @@ function serializeBlob(request: Request, blob: Blob): string {
3114 model,
3115 null,
3116 false,
3117 + createRootFormatContext(),
3118 request.abortableTasks,
3119 enableProfilerTimer &&
3120 (enableComponentPerformanceTrack || enableAsyncDebugInfo)
@@ -3208,6 +3252,7 @@ function renderModel(
3252 task.model,
3253 task.keyPath,
3254 task.implicitSlot,
3255 + task.formatContext,
3256 request.abortableTasks,
3257 enableProfilerTimer &&
3258 (enableComponentPerformanceTrack || enableAsyncDebugInfo)
packages/react-server/src/forks/ReactFlightServerConfig.custom.js
+14
@@ -32,3 +32,17 @@ export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
32 export function createHints(): any {
33 return null;
34 }
35 +
36 +export type FormatContext = null;
37 +
38 +export function createRootFormatContext(): FormatContext {
39 + return null;
40 +}
41 +
42 +export function getChildFormatContext(
43 + parentContext: FormatContext,
44 + type: string,
45 + props: Object,
46 +): FormatContext {
47 + return parentContext;
48 +}
packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js
+14
@@ -32,3 +32,17 @@ export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
32 export function createHints(): any {
33 return null;
34 }
35 +
36 +export type FormatContext = null;
37 +
38 +export function createRootFormatContext(): FormatContext {
39 + return null;
40 +}
41 +
42 +export function getChildFormatContext(
43 + parentContext: FormatContext,
44 + type: string,
45 + props: Object,
46 +): FormatContext {
47 + return parentContext;
48 +}
packages/react-server/src/forks/ReactFlightServerConfig.markup.js
+14
@@ -19,6 +19,20 @@ export function createHints(): Hints {
19 return null;
20 }
21
22 +export type FormatContext = null;
23 +
24 +export function createRootFormatContext(): FormatContext {
25 + return null;
26 +}
27 +
28 +export function getChildFormatContext(
29 + parentContext: FormatContext,
30 + type: string,
31 + props: Object,
32 +): FormatContext {
33 + return parentContext;
34 +}
35 +
36 export const supportsRequestStorage = false;
37 export const requestStorage: AsyncLocalStorage<Request | void> = (null: any);
38