diff --git a/.github/workflows/runtime_commit_artifacts.yml b/.github/workflows/runtime_commit_artifacts.yml index 1b98673cd4dd..11a22e6c2a4c 100644 --- a/.github/workflows/runtime_commit_artifacts.yml +++ b/.github/workflows/runtime_commit_artifacts.yml @@ -116,11 +116,13 @@ jobs: run: | sed -i -e 's/ @license React*//' \ build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \ + build/facebook-www/ESLintPluginReactHooks-dev.modern.js \ build/oss-experimental/react-refresh/cjs/react-refresh-babel.development.js - name: Insert @headers into eslint plugin and react-refresh run: | sed -i -e 's/ LICENSE file in the root directory of this source tree./ LICENSE file in the root directory of this source tree.\n *\n * @noformat\n * @nolint\n * @lightSyntaxTransform\n * @preventMunge\n * @oncall react_core/' \ build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \ + build/facebook-www/ESLintPluginReactHooks-dev.modern.js \ build/oss-experimental/react-refresh/cjs/react-refresh-babel.development.js - name: Move relevant files for React in www into compiled run: | @@ -132,9 +134,9 @@ jobs: mkdir ./compiled/facebook-www/__test_utils__ mv build/__test_utils__/ReactAllWarnings.js ./compiled/facebook-www/__test_utils__/ReactAllWarnings.js - # Copy eslint-plugin-react-hooks + # Copy eslint-plugin-react-hooks (www build with feature flags) mkdir ./compiled/eslint-plugin-react-hooks - cp build/oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js \ + cp ./compiled/facebook-www/ESLintPluginReactHooks-dev.modern.js \ ./compiled/eslint-plugin-react-hooks/index.js # Move unstable_server-external-runtime.js into facebook-www diff --git a/compiler/docs/DESIGN_GOALS.md b/compiler/docs/DESIGN_GOALS.md index 76d19e25c084..d07bd2b30189 100644 --- a/compiler/docs/DESIGN_GOALS.md +++ b/compiler/docs/DESIGN_GOALS.md @@ -8,7 +8,7 @@ The idea of React Compiler is to allow developers to use React's familiar declar * Bound the amount of re-rendering that happens on updates to ensure that apps have predictably fast performance by default. * Keep startup time neutral with pre-React Compiler performance. Notably, this means holding code size increases and memoization overhead low enough to not impact startup. -* Retain React's familiar declarative, component-oriented programming model. Ie, the solution should not fundamentally change how developers think about writing React, and should generally _remove_ concepts (the need to use React.memo(), useMemo(), and useCallback()) rather than introduce new concepts. +* Retain React's familiar declarative, component-oriented programming model. i.e, the solution should not fundamentally change how developers think about writing React, and should generally _remove_ concepts (the need to use React.memo(), useMemo(), and useCallback()) rather than introduce new concepts. * "Just work" on idiomatic React code that follows React's rules (pure render functions, the rules of hooks, etc). * Support typical debugging and profiling tools and workflows. * Be predictable and understandable enough by React developers — i.e. developers should be able to quickly develop a rough intuition of how React Compiler works. diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/MUTABILITY_ALIASING_MODEL.md b/compiler/packages/babel-plugin-react-compiler/src/Inference/MUTABILITY_ALIASING_MODEL.md index ab327c255b10..dfff673cabc4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/MUTABILITY_ALIASING_MODEL.md +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/MUTABILITY_ALIASING_MODEL.md @@ -24,7 +24,7 @@ The goal of mutability and aliasing inference is to understand the set of instru In code, the mutability and aliasing model is compromised of the following phases: -* `InferMutationAliasingEffects`. Infers a set of mutation and aliasing effects for each instruction. The approach is to generate a set of candidate effects based purely on the semantics of each instruction and the types of the operands, then use abstract interpretation to determine the actual effects (or errros) that would apply. For example, an instruction that by default has a Capture effect might downgrade to an ImmutableCapture effect if the value is known to be frozen. +* `InferMutationAliasingEffects`. Infers a set of mutation and aliasing effects for each instruction. The approach is to generate a set of candidate effects based purely on the semantics of each instruction and the types of the operands, then use abstract interpretation to determine the actual effects (or errors) that would apply. For example, an instruction that by default has a Capture effect might downgrade to an ImmutableCapture effect if the value is known to be frozen. * `InferMutationAliasingRanges`. Infers a mutable range (start:end instruction ids) for each value in the program, and annotates each Place with its effect type for usage in later passes. This builds a graph of data flow through the program over time in order to understand which mutations effect which values. * `InferReactiveScopeVariables`. Given the per-Place effects, determines disjoint sets of values that mutate together and assigns all identifiers in each set to a unique scope, and updates the range to include the ranges of all constituent values. @@ -69,7 +69,7 @@ Describes the creation of new function value, capturing the given set of mutable kind: 'Apply'; receiver: Place; function: Place; // same as receiver for function calls - mutatesFunction: boolean; // indicates if this is a type that we consdier to mutate the function itself by default + mutatesFunction: boolean; // indicates if this is a type that we consider to mutate the function itself by default args: Array; into: Place; // where result is stored signature: FunctionSignature | null; @@ -526,7 +526,7 @@ Capture c <- a Intuition: these effects are inverses of each other (capturing into an object, extracting from an object). The result is based on the order of operations: -Capture then CreatFrom is equivalent to Alias: we have to assume that the result _is_ the original value and that a local mutation of the result could mutate the original. +Capture then CreateFrom is equivalent to Alias: we have to assume that the result _is_ the original value and that a local mutation of the result could mutate the original. ```js const b = [a]; // capture diff --git a/design/fused-renderer-fizz-analysis.md b/design/fused-renderer-fizz-analysis.md new file mode 100644 index 000000000000..536dd6fa9d61 --- /dev/null +++ b/design/fused-renderer-fizz-analysis.md @@ -0,0 +1,306 @@ +# Fizz Internals Analysis for Fused Renderer + +Analysis of `packages/react-server/src/ReactFizzServer.js` and +`packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js` to identify +insertion points for the fused single-pass renderer. + +## 1. renderElement() Dispatch Table + +Location: `ReactFizzServer.js:2917` + +`renderElement(request, task, keyPath, type, props, ref)` dispatches on `type`: + +| Type check | Handler | Notes | +|---|---|---| +| `typeof type === 'function'` + `shouldConstruct(type)` | `renderClassComponent()` | Class components with `isReactComponent` | +| `typeof type === 'function'` (else) | `renderFunctionComponent()` | **All function components go here — server components would too** | +| `typeof type === 'string'` | `renderHostElement()` | DOM elements (`div`, `span`, etc.) | +| `type === REACT_FRAGMENT_TYPE` | `renderNodeDestructive(props.children)` | Fragments, also StrictMode, Profiler, LegacyHidden | +| `type === REACT_ACTIVITY_TYPE` | `renderActivity()` | Activity (offscreen) | +| `type === REACT_SUSPENSE_LIST_TYPE` | `renderSuspenseList()` | SuspenseList ordering | +| `type === REACT_VIEW_TRANSITION_TYPE` | `renderViewTransition()` | View transitions (feature-flagged) | +| `type === REACT_SCOPE_TYPE` | `renderNodeDestructive(props.children)` | Scope API (feature-flagged) | +| `type === REACT_SUSPENSE_TYPE` | `renderSuspenseBoundary()` | **Suspense — key for async server components** | +| `type.$$typeof === REACT_FORWARD_REF_TYPE` | `renderForwardRef()` | ForwardRef wrappers | +| `type.$$typeof === REACT_MEMO_TYPE` | `renderMemo()` | Memo wrappers | +| `type.$$typeof === REACT_CONTEXT_TYPE` | `renderContextProvider()` | Context.Provider | +| `type.$$typeof === REACT_CONSUMER_TYPE` | `renderContextConsumer()` | Context.Consumer | +| `type.$$typeof === REACT_LAZY_TYPE` | `renderLazyComponent()` | Lazy loading | +| Otherwise | **throw** | "Element type is invalid" | + +### Key observation for fused renderer + +Today, `typeof type === 'function'` catches **all** function components. Fizz has no concept of "server component" vs "client component" — Flight resolves everything before Fizz sees it. In fused mode, we need to distinguish: + +1. **Server component**: function without `'use client'` module reference → call it inline +2. **Client component**: function with module reference → render to HTML + emit hydration data +3. **Plain function component**: regular component (neither server nor client) → render as today + +The detection mechanism will depend on how the bundler marks client components (typically via `$$typeof` on the module reference or a special property). See TIM-472 flight analysis for details. + +## 2. Request Object Shape + +Location: `ReactFizzServer.js:366` (opaque type), `ReactFizzServer.js:517` (RequestInstance constructor) + +### Fields + +``` +Request { + // Output + destination: null | Destination // The writable stream we flush to + flushScheduled: boolean // Whether a flush is pending + + // Configuration (immutable after creation) + resumableState: ResumableState // Tracks what instructions have been sent (deduplication) + renderState: RenderState // Precomputed chunks, script prefixes, boundary prefixes + rootFormatContext: FormatContext // HTML/SVG/MathML context for the root + progressiveChunkSize: number // Target chunk size for streaming (~12.8KB default) + + // Lifecycle state + status: OPENING | OPEN | ABORTING | CLOSING | CLOSED | STALLED_DEV + fatalError: mixed // Set when a fatal error occurs + + // Task tracking + nextSegmentId: number // Auto-incrementing ID for new segments + allPendingTasks: number // Total pending tasks (when 0, connection can close) + pendingRootTasks: number // Pending tasks in the root/shell (when 0, shell is done) + abortableTasks: Set // All tasks that can be aborted + pingedTasks: Array // High-priority tasks to work on next (the work queue) + + // Output queues (flushed in priority order) + completedRootSegment: null | Segment // The root segment, once completed + completedPreambleSegments: null | Array> // Preamble segments (head content) + byteSize: number // Accumulated shell bytes + clientRenderedBoundaries: Array // Errored boundaries → client render + completedBoundaries: Array // Done boundaries → stream replacement + partialBoundaries: Array // Partially done → stream segments + + // Prerender tracking + trackedPostpones: null | PostponedHoles // Non-null during prerender to track holes + + // Callbacks + onError: (error, errorInfo) => ?string + onAllReady: () => void // All tasks done (good for static generation) + onShellReady: () => void // Shell/root done (good for streaming start) + onShellError: (error) => void // Shell failed + onFatalError: (error) => void // Unrecoverable + + // Form state + formState: null | ReactFormState // For MPA form submission hydration +} +``` + +### Lifecycle + +1. **OPENING** (10): Created, initial work scheduled via `scheduleMicrotask` +2. **OPEN** (11): First `scheduleWork` callback fired +3. **ABORTING** (12): `abort()` called, tasks are being torn down +4. **CLOSING** (13): All tasks done, flushing final output +5. **CLOSED** (14): Done, destination closed + +### Where to add fused renderer state + +A `fusedMode: boolean` flag on Request would gate the new behavior. Additionally: +- `clientBoundaryQueue: Array<{id, moduleRef, serializedProps}>` for hydration data +- `nextClientBoundaryId: number` for generating boundary IDs + +These would be added to RequestInstance constructor and the Request opaque type. + +## 3. Task/Segment Model + +### Task types + +Two task types share the `Task` union: + +**RenderTask** — produces HTML into a Segment: +``` +RenderTask { + replay: null // null = render mode (not replay) + node: ReactNodeList // The React node being rendered + childIndex: number // Position within parent's children + ping: () => void // Callback to re-queue this task + blockedBoundary: Root | SuspenseBoundary // The Suspense boundary this renders into + blockedSegment: Segment // The segment being written to + blockedPreamble: null | PreambleState // Preamble state for head content + hoistableState: null | HoistableState // Hoistable resources (stylesheets, etc.) + abortSet: Set // Which abort set this belongs to + keyPath: Root | KeyNode // React element key path + formatContext: FormatContext // HTML/SVG/MathML rendering context + context: ContextSnapshot // React context state + treeContext: TreeContext // ID generation tree context + row: null | SuspenseListRow // SuspenseList row tracking + componentStack: ComponentStackNode // For error messages + thenableState: null | ThenableState // Saved thenable state for resumption + legacyContext: LegacyContext // Legacy context (being removed) +} +``` + +**ReplayTask** — replays a prerendered tree (used for resume): +- Same shape but `replay: ReplaySet` (non-null) and `blockedSegment: null` + +### Segment + +``` +Segment { + status: PENDING | COMPLETED | FLUSHED | ABORTED | ERRORED | POSTPONED | RENDERING + parentFlushed: boolean // Can this be flushed (parent already sent)? + id: number // Lazily assigned segment ID + index: number // Position within parent's chunks + chunks: Array // THE HTML OUTPUT BUFFER + children: Array // Child segments (from Suspense boundaries) + preambleChildren: Array // Preamble child segments + parentFormatContext: FormatContext // Context when this segment was created + boundary: null | SuspenseBoundary // Associated boundary (for fallback segments) + lastPushedText: boolean // Text separator tracking + textEmbedded: boolean // Text separator tracking +} +``` + +### How they interact + +1. `createRequest()` creates a root Segment and a root RenderTask pointing at it +2. The task is pushed to `request.pingedTasks` +3. `performWork()` iterates `pingedTasks`, calling `retryTask()` on each +4. `retryTask()` → `retryRenderTask()` → `retryNode()` → `renderNodeDestructive()` → `renderElement()` +5. HTML chunks are pushed into `segment.chunks` via `pushStartInstance()` / `pushEndInstance()` +6. When a Suspense boundary is encountered, new child Segments and Tasks are created +7. When a Task completes, `finishedSegment()` and `finishedTask()` update counters +8. When all tasks for a boundary complete, it moves to `completedBoundaries` +9. `flushCompletedQueues()` writes segments to the destination stream + +## 4. Suspense Boundary Flow (suspend → resolve → stream) + +Location: `ReactFizzServer.js:1337` (`renderSuspenseBoundary`) + +### Normal render path (not prerender) + +1. **Create boundary**: `createSuspenseBoundary()` — tracks pending tasks, completed segments +2. **Create segments**: + - `boundarySegment` — holds the fallback content (child of parent segment) + - `contentRootSegment` — holds the actual content (independent) +3. **Try to render content synchronously**: + - Temporarily swap `task.blockedBoundary` and `task.blockedSegment` to the new boundary/segment + - Call `renderNode(request, task, content, -1)` + - If it succeeds without suspending → boundary is COMPLETED immediately + - If the boundary is small enough, skip creating the fallback entirely (early return) +4. **If content throws a thenable** (suspends): + - Caught in `renderNode()` catch block (~line 4268) + - `spawnNewSuspendedRenderTask()` creates a new task for the suspended subtree + - New task gets a new child segment, registered with the boundary + - The thenable's `.then(ping, ping)` ensures the task is re-queued when resolved +5. **Create fallback task**: Always created (unless early return), queued to `pingedTasks` +6. **When suspended content resolves**: + - `ping()` → `pingTask()` → pushes task to `pingedTasks` + - Next `performWork()` calls `retryRenderTask()` on it + - Rendering continues from where it left off (same node, same context) + - On completion: `finishedSegment()` + `finishedTask()` +7. **When boundary completes**: + - `finishedTask()` decrements `boundary.pendingTasks` + - When it reaches 0: boundary moves to `completedBoundaries` queue + - `flushCompletedQueues()` writes a streaming `