github.com-facebook-react
all · 22 devs · built 2026-06-13
Repository snapshot
Monthly reports
Highlights
- Multiple critical bug fixes were implemented across *React Fizz* to enhance stability and error reporting, addressing issues like abort re-entrancy [f1af67e1 · Josh Story], incorrect aborts during resumed rendering [de8e0054 · Josh Story], and crashes related to stalled Flight chunks [37fa36ce · Janka Uryga].
- *React Flight Server* received crucial fixes to prevent main-thread stalls from large debug strings [f0dfee38 · Hendrik Liebau] and resolve data corruption when streaming over Node.js under backpressure [c0148134 · Hendrik Liebau].
- Significant improvements were made to the CI/CD pipeline, including extending artifact attestations to backport branches [6b5ea125 · Sebastian "Sebbie" Silbermann] and unifying the release process for enhanced security and efficiency [75b0945b · Sebastian "Sebbie" Silbermann].
- Error handling for incorrectly rejected Promises in *React Reconciler* and *React Server* was improved to provide more descriptive messages and call stacks [c0cd4d5d · Sebastian "Sebbie" Silbermann].
- A regression in *ReactFlightDOMServerNode* was fixed, ensuring `FormData` entries are not dropped during reply decoding [b91823e2 · Hendrik Liebau].
Observations
- The *Grow score* plummeted by -81% compared to the 2-month average (current: 0, avg: 2), indicating a complete shift away from new feature development during this period.
- *Maintenance* activity saw a substantial increase of +174% compared to the 2-month average (current: 2, avg: 1), reflecting a strong focus on infrastructure, CI/CD, and internal system improvements.
- The *Waste score* also rose significantly by +149% compared to the 2-month average (current: 2, avg: 1), suggesting a higher proportion of rework or bug fixes.
- Overall commit volume decreased by -38% (22 commits this period vs 36-commit 2-month average), contributing to a -33% reduction in total output (Grow + Maint) compared to the 2-month average.
- A high concentration of bug fixes and stability improvements were observed in the *React Fizz* and *React Flight* components, with 7 commits directly addressing issues in *Fizz* and 3 in *Flight*, indicating these areas required significant stabilization efforts.
- Several commits focused on enhancing developer tooling and CI/CD robustness, including improvements to *React DevTools* [commit/9635257c, commit/4f273bd3] and CI pipeline optimizations [commit/6b5ea125, commit/75ae73e6, commit/75b0945b, commit/926fa855].
Performance over time
ETV stacked by Growth, Maintenance and Fixes — 90-day moving average, normalized to ETV / month.
Average performance per developer
ETV per active developer per month — 30-day moving average.
Active developers over time
Unique developers committing each day — 90-day moving average.
Knowledge concentration
How dependent is this repo on a small number of contributors? Higher top-1 share = higher key-person risk.
Sebastian Markbåge owns 30.6 % of commits.
Top contributors
Most impactful commits
Top 20 by ETV in the all-time window.
- 4.5ETVImplement Partial Hydration for Activity (#32863) Stacked on #32862 and #32842. This means that Activity boundaries now act as boundaries which can have their effects mounted independently. Just like Suspense boundaries, we hydrate the outer content first and then start hydrating the content in an Offscreen lane. Flowing props or interacting with the content increases the priority just like Suspense boundaries. This skips emitting even the comments for `<Activity mode="hidden">` so we don't hydrate those. Instead those are deferred to a later client render. The implementation are just forked copies of the SuspenseComponent branches and then carefully going through each line and tweaking it. The main interesting bit is that, unlike Suspense, Activity boundaries don't have fallbacks so all those branches where you might commit a suspended tree disappears. Instead, if something suspends while hydration, we can just leave the dehydrated content in place. However, if something does suspend during client rendering then it should bubble up to the parent. Therefore, we have to be careful to only pushSuspenseHandler when hydrating. That's really the main difference. This just uses the existing basic Activity tests but I've started work on port all of the applicable Suspense tests in SelectiveHydration-test and PartialHydration-test to Activity versions.Sebastian Markbåge · 3ef31d19 · 2025-04-23
- 2.8ETV[eslint-plugin-react-hooks] updates for component syntax (#33089) Adds support for Flow's component and hook syntax. [docs](https://flow.org/en/docs/react/component-syntax/)Jan Kassens · 4c4a57c4 · 2025-05-02
- 1.9ETVAdd <ViewTransition> Component (#31975) This will provide the opt-in for using [View Transitions](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API) in React. View Transitions only trigger for async updates like `startTransition`, `useDeferredValue`, Actions or `<Suspense>` revealing from fallback to content. Synchronous updates provide an opt-out but also guarantee that they commit immediately which View Transitions can't. There's no need to opt-in to View Transitions at the "cause" side like event handlers or actions. They don't know what UI will change and whether that has an animated transition described. Conceptually the `<ViewTransition>` component is like a DOM fragment that transitions its children in its own isolate/snapshot. The API works by wrapping a DOM node or inner component: ```js import {ViewTransition} from 'react'; <ViewTransition><Component /></ViewTransition> ``` The default is `name="auto"` which will automatically assign a `view-transition-name` to the inner DOM node. That way you can add a View Transition to a Component without controlling its DOM nodes styling otherwise. A difference between this and the browser's built-in `view-transition-name: auto` is that switching the DOM nodes within the `<ViewTransition>` component preserves the same name so this example cross-fades between the DOM nodes instead of causing an exit and enter: ```js <ViewTransition>{condition ? <ComponentA /> : <ComponentB />}</ViewTransition> ``` This becomes especially useful with `<Suspense>` as this example cross-fades between Skeleton and Content: ```js <ViewTransition> <Suspense fallback={<Skeleton />}> <Content /> </Suspense> </ViewTransition> ``` Where as this example triggers an exit of the Skeleton and an enter of the Content: ```js <Suspense fallback={<ViewTransition><Skeleton /></ViewTransition>}> <ViewTransition><Content /></ViewTransition> </Suspense> ``` Managing instances and keys becomes extra important. You can also specify an explicit `name` property for example for animating the same conceptual item from one page onto another. However, best practices is to property namespace these since they can easily collide. It's also useful to add an `id` to it if available. ```js <ViewTransition name="my-shared-view"> ``` The model in general is the same as plain `view-transition-name` except React manages a set of heuristics for when to apply it. A problem with the naive View Transitions model is that it overly opts in every boundary that *might* transition into transitioning. This is leads to unfortunate effects like things floating around when unrelated updates happen. This leads the whole document to animate which means that nothing is clickable in the meantime. It makes it not useful for smaller and more local transitions. Best practice is to add `view-transition-name` only right before you're about to need to animate the thing. This is tricky to manage globally on complex apps and is not compositional. Instead we let React manage when a `<ViewTransition>` "activates" and add/remove the `view-transition-name`. This is also when React calls `startViewTransition` behind the scenes while it mutates the DOM. I've come up with a number of heuristics that I think will make a lot easier to coordinate this. The principle is that only if something that updates that particular boundary do we activate it. I hope that one day maybe browsers will have something like these built-in and we can remove our implementation. A `<ViewTransition>` only activates if: - If a mounted Component renders a `<ViewTransition>` within it outside the first DOM node, and it is within the viewport, then that ViewTransition activates as an "enter" animation. This avoids inner "enter" animations trigger when the parent mounts. - If an unmounted Component had a `<ViewTransition>` within it outside the first DOM node, and it was within the viewport, then that ViewTransition activates as an "exit" animation. This avoids inner "exit" animations triggering when the parent unmounts. - If an explicitly named `<ViewTransition name="...">` is deep within an unmounted tree and one with the same name appears in a mounted tree at the same time, then both are activated as a pair, but only if they're both in the viewport. This avoids these triggering "enter" or "exit" animations when going between parents that don't have a pair. - If an already mounted `<ViewTransition>` is visible and a DOM mutation, that might affect how it's painted, happens within its children but outside any nested `<ViewTransition>`. This allows it to "cross-fade" between its updates. - If an already mounted `<ViewTransition>` resizes or moves as the result of direct DOM nodes siblings changing or moving around. This allows insertion, deletion and reorders into a list to animate all children. It is only within one DOM node though, to avoid unrelated changes in the parent to trigger this. If an item is outside the viewport before and after, then it's skipped to avoid things flying across the screen. - If a `<ViewTransition>` boundary changes size, due to a DOM mutation within it, then the parent activates (or the root document if there are no more parents). This ensures that the container can cross-fade to avoid abrupt relayout. This can be avoided by using absolutely positioned children. When this can avoid bubbling to the root document, whatever is not animating is still responsive to clicks during the transition. Conceptually each DOM node has its own default that activates the parent `<ViewTransition>` or no transition if the parent is the root. That means that if you add a DOM node like `<div><ViewTransition><Component /></ViewTransition></div>` this won't trigger an "enter" animation since it was the div that was added, not the ViewTransition. Instead, it might cause a cross-fade of the parent ViewTransition or no transition if it had no parent. This ensures that only explicit boundaries perform coarse animations instead of every single node which is really the benefit of the View Transitions model. This ends up working out well for simple cases like switching between two pages immediately while transitioning one floating item that appears on both pages. Because only the floating item transitions by default. Note that it's possible to add manual `view-transition-name` with CSS or `style={{ viewTransitionName: 'auto' }}` that always transitions as long as something else has a `<ViewTransition>` that activates. For example a `<ViewTransition>` can wrap a whole page for a cross-fade but inside of it an explicit name can be added to something to ensure it animates as a move when something relates else changes its layout. Instead of just cross-fading it along with the Page which would be the default. There's more PRs coming with some optimizations, fixes and expanded APIs. This first PR explores the above core heuristic. --------- Co-authored-by: Sebastian "Sebbie" Silbermann <silbermann.sebastian@gmail.com>Sebastian Markbåge · a4d122f2 · 2025-01-08
- 1.7ETV[compiler] Aggregate error reporting, separate eslint rules (#34176) NOTE: this is a merged version of @mofeiZ's original PR along with my edits per offline discussion. The description is updated to reflect the latest approach. The key problem we're trying to solve with this PR is to allow developers more control over the compiler's various validations. The idea is to have a number of rules targeting a specific category of issues, such as enforcing immutability of props/state/etc or disallowing access to refs during render. We don't want to have to run the compiler again for every single rule, though, so @mofeiZ added an LRU cache that caches the full compilation output of N most recent files. The first rule to run on a given file will cause it to get cached, and then subsequent rules can pull from the cache, with each rule filtering down to its specific category of errors. For the categories, I went through and assigned a category roughly 1:1 to existing validations, and then used my judgement on some places that felt distinct enough to warrant a separate error. Every error in the compiler now has to supply both a severity (for legacy reasons) and a category (for ESLint). Each category corresponds 1:1 to a ESLint rule definition, so that the set of rules is automatically populated based on the defined categories. Categories include a flag for whether they should be in the recommended set or not. Note that as with the original version of this PR, only eslint-plugin-react-compiler is changed. We still have to update the main lint rule. ## Test Plan * Created a sample project using ESLint v9 and verified that the plugin can be configured correctly and detects errors * Edited `fixtures/eslint-v9` and introduced errors, verified that the w latest config changes in that fixture it correctly detects the errors * In the sample project, confirmed that the LRU caching is correctly caching compiler output, ie compiling files just once. Co-authored-by: Mofei Zhang <feifei0@meta.com>Joseph Savona · 7d29ecbe · 2025-08-21
- 1.6ETV[compiler] Add snap subcommand to minimize a test input (#35663) Snap now supports subcommands 'test' (default) and 'minimize`. The minimize subcommand attempts to minimize a single failing input fixture by incrementally simplifying the ast so long as the same error occurs. I spot-checked it and it seemed to work pretty well. This is intended for use in a new subagent designed for investigating bugs — fixture simplification is an important part of the process and we can automate this rather than light tokens on fire. Example Input: ```js function Component(props) { const x = []; let result; for (let i = 0; i < 10; i++) { if (cond) { try { result = {key: bar([props.cond && props.foo])}; } catch (e) { console.log(e); } } } x.push(result); return <Stringify x={x} />; } ``` Command output: ``` $ yarn snap minimize --path .../input.js Minimizing: .../input.js Minimizing................ --- Minimized Code --- function Component(props) { try { props && props; } catch (e) {} } Reduced from 16 lines to 5 lines ``` This demonstrates things like: * Removing one statement at at time * Replacing if/else with the test, consequent, or alternate. Similar for other control-flow statements including try/catch * Removing individual array/object expression properties * Replacing single-value array/object with the value * Replacing control-flow expression (logical, consequent) w the test or left/right values * Removing call arguments * Replacing calls with a single argument with the argument * Replacing calls with multiple arguments with an array of the arguments * Replacing optional member/call with non-optional versions * Replacing member expression with the object. If computed, also try replacing w the key * And a bunch more strategies, see the codeJoseph Savona · d4a325df · 2026-02-03
- 1.5ETV[Flight] Add more DoS mitigations to Flight Reply, and harden Flight (#35632) This fixes security vulnerabilities in Server Functions. --------- Co-authored-by: Sebastian Markbåge <sebastian@calyptus.eu> Co-authored-by: Josh Story <josh.c.story@gmail.com> Co-authored-by: Janka Uryga <lolzatu2@gmail.com> Co-authored-by: Sebastian Sebbie Silbermann <sebastian.silbermann@vercel.com>Hendrik Liebau · 10680271 · 2026-01-26
- 1.4ETV[DevTools] Use use() instead of throwing a Promise in Caches (#34033)Sebastian Markbåge · 5d7e8b90 · 2025-07-29
- 1.4ETVAdd Flight SSR benchmark fixture (#36180) This PR adds a benchmark fixture for measuring the performance overhead of the React Server Components (RSC) Flight rendering compared to plain Fizz server-side rendering. ### Motivation Performance discussions around RSC (e.g. #36143, #35125) have highlighted the need for reproducible benchmarks that accurately measure the cost that Flight adds on top of Fizz. This fixture provides multiple benchmark modes that can be used to track performance improvements across commits, compare Node vs Edge (web streams) overhead, and identify bottlenecks in Flight serialization and deserialization. ### What it measures The benchmark renders a dashboard app with ~25 components (16 client components), 200 product rows with nested data (~325KB Flight payload), and ~250 Suspense boundaries in the async variant. It compares 8 render variants: Fizz-only and Flight+Fizz, across Node and Edge stream APIs, with both synchronous and asynchronous apps. ### Benchmark modes - **`yarn bench`** runs a sequential in-process benchmark with realistic Flight script injection (tee + `TransformStream`/`Transform` buffered injection), matching what real frameworks do when inlining the RSC payload into the HTML response for hydration. - **`yarn bench:bare`** runs the same benchmark without script injection, isolating the React-internal rendering cost. This is best for tracking changes to Flight serialization or Fizz rendering. - **`yarn bench:server`** starts an HTTP server and uses `autocannon` to measure real req/s at `c=1` and `c=10`. The `c=1` results provide a clean signal for tracking React-internal changes, while `c=10` reflects throughput under concurrent load. - **`yarn bench:concurrent`** runs an in-process concurrent benchmark with 50 in-flight renders via `Promise.all`, measuring throughput without HTTP overhead. - **`yarn bench:profile`** collects CPU profiles via the V8 inspector and reports the top functions by self-time along with GC pause data. - **`yarn start`** starts the HTTP server for manual browser testing. Appending `.rsc` to any Flight URL serves the raw Flight payload. ### Key findings during development On Node 22, the Flight+Fizz overhead compared to Fizz-only rendering is roughly: - **Without script injection** (`bench:bare`): ~2.2x for sync, ~1.3x for async - **With script injection** (`bench:server`, c=1): ~2.9x for sync, ~1.8x for async - **Edge vs Node** adds another ~30% for sync and ~10% for async, driven by the stream plumbing for script injection (tee + `TransformStream` buffering) The async variant better represents real-world applications where server components fetch data asynchronously. Its lower overhead reflects the fact that Flight serialization and Fizz rendering can overlap with I/O wait times, making the added Flight cost a smaller fraction of total request time. The benchmark also revealed that the Edge vs Node gap is negligible for Fizz-only rendering (~1-2%) but grows to ~15% for Flight+Fizz sync even without script injection. With script injection (tee + `TransformStream` buffering), the gap roughly doubles to ~30% for sync. The async variants show smaller gaps (~5% without, ~10% with injection).Hendrik Liebau · 1b45e243 · 2026-04-02
- 1.3ETV[assert helpers] react-dom (pt3) (#31983) moar assert helpers this finishes all of react-dom except the server integration tests which are tricky to convertRicky · 03e4ec2d · 2025-01-05
- 1.2ETV[compiler] Migrate CompilerError.invariant to new CompilerDiagnostic infra (#34403) Mechanical PR to migrate existing invariants to use the new CompilerDiagnostic infra @josephsavona added. Will tackle the others at a later time. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34403). * #34409 * #34404 * __->__ #34403lauren · 474f2584 · 2025-09-06
- 1.2ETV[assert helpers] react-dom (pt 1) (#31897) Converts ~half of react-dom testsRicky · a7c898d8 · 2025-01-02
- 1.0ETV[compiler] Improve IIFE inlining (#33726) We currently inline IIFEs by creating a temporary and a labeled block w the original code. The original return statements turn into an assignment to the temporary and break out of the label. However, many cases of IIFEs are due to inlining of manual `useMemo()`, and these cases often have only a single return statement. Here, the output is cleaner if we avoid the temporary and label - so that's what we do in this PR. Note that the most complex part of the change is actually around ValidatePreserveExistingMemo - we have some logic to track the IIFE temporary reassignmetns which needs to be updated to handle the simpler version of inlining. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33726). * __->__ #33726 * #33725Joseph Savona · 956d770a · 2025-07-08
- 1.0ETVInferEffectDeps takes a React.AUTODEPS sigil (#33799) --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/33799). * #33800 * __->__ #33799Jordan Brown · dffacc7b · 2025-07-17
- 1.0ETV[compiler] Upstream experimental flow integration (#34121) all credit on the Flood/ code goes to @mvitousek and @jbrown215, i'm just the one upstreaming itJoseph Savona · c403a7c5 · 2025-08-06
- 1.0ETVAdd ref to Fragment (#32465) *This API is experimental and subject to change or removal.* This PR is an alternative to https://github.com/facebook/react/pull/32421 based on feedback: https://github.com/facebook/react/pull/32421#pullrequestreview-2625382015 . The difference here is that we traverse from the Fragment's fiber at operation time instead of keeping a set of children on the `FragmentInstance`. We still need to handle newly added or removed child nodes to apply event listeners and observers, so we treat those updates as effects. **Fragment Refs** This PR extends React's Fragment component to accept a `ref` prop. The Fragment's ref will attach to a custom host instance, which will provide an Element-like API for working with the Fragment's host parent and host children. Here I've implemented `addEventListener`, `removeEventListener`, and `focus` to get started but we'll be iterating on this by adding additional APIs in future PRs. This sets up the mechanism to attach refs and perform operations on children. The FragmentInstance is implemented in `react-dom` here but is planned for Fabric as well. The API works by targeting the first level of host children and proxying Element-like APIs to allow developers to manage groups of elements or elements that cannot be easily accessed such as from a third-party library or deep in a tree of Functional Component wrappers. ```javascript import {Fragment, useRef} from 'react'; const fragmentRef = useRef(null); <Fragment ref={fragmentRef}> <div id="A" /> <Wrapper> <div id="B"> <div id="C" /> </div> </Wrapper> <div id="D" /> </Fragment> ``` In this case, calling `fragmentRef.current.addEventListener()` would apply an event listener to `A`, `B`, and `D`. `C` is skipped because it is nested under the first level of Host Component. If another Host Component was appended as a sibling to `A`, `B`, or `D`, the event listener would be applied to that element as well and any other APIs would also affect the newly added child. This is an implementation of the basic feature as a starting point for feedback and further iteration.Jack Pope · 6aa8254b · 2025-03-12
- 0.9ETV[compiler] Avoid failing builds when import specifiers conflict or shadow vars (#32663) Avoid failing builds when imported function specifiers conflict by using babel's `generateUid`. Failing a build is very disruptive, as it usually presents to developers similar to a javascript parse error. ```js import {logRender as _logRender} from 'instrument-runtime'; const logRender = () => { /* local conflicting implementation */ } function Component_optimized() { _logRender(); // inserted by compiler } ``` Currently, we fail builds (even in `panicThreshold:none` cases) when import specifiers are detected to conflict with existing local variables. The reason we destructively throw (instead of bailing out) is because (1) we first generate identifier references to the conflicting name in compiled functions, (2) replaced original functions with compiled functions, and then (3) finally check for conflicts. When we finally check for conflicts, it's too late to bail out. ```js // import {logRender} from 'instrument-runtime'; const logRender = () => { /* local conflicting implementation */ } function Component_optimized() { logRender(); // inserted by compiler } ```mofeiZ · c61e75b7 · 2025-03-24
- 0.9ETV[compiler] Hoist dependencies from functions more conservatively (#32616) Alternative to facebook/react#31584 which sets enableTreatFunctionDepsAsConditional:true` by default. This PR changes dependency hoisting to be more conservative while trying to preserve an optimal "happy path". We assume that a function "is likely called" if we observe the following in the react function body. - a direct callsite - passed directly as a jsx attribute or child - passed directly to a hook - a direct return A function is also "likely called" if it is directly called, passed to jsx / hooks, or returned from another function that "is likely called". Note that this approach marks the function definition site with its hoistable properties (not its use site). I tried implementing use-site hoisting semantics, but it felt both unpredictable (i.e. as a developer, I can't trust that callbacks are well memoized) and not helpful (type + null checks of a value are usually colocated with their use site) In this fixture (copied here for easy reference), it should be safe to use `a.value` and `b.value` as dependencies, even though these functions are conditionally called. ```js // inner-function/nullable-objects/assume-invoked/conditional-call-chain.tsx function Component({a, b}) { const logA = () => { console.log(a.value); }; const logB = () => { console.log(b.value); }; const hasLogged = useRef(false); const log = () => { if (!hasLogged.current) { logA(); logB(); hasLogged.current = true; } }; return <Stringify log={log} shouldInvokeFns={true} />; } ``` On the other hand, this means that we produce invalid output for code like manually implementing `Array.map` ```js // inner-function/nullable-objects/bug-invalid-array-map-manual.js function useFoo({arr1, arr2}) { const cb = e => arr2[0].value + e.value; const y = []; for (let i = 0; i < arr1.length; i++) { y.push(cb(arr1[i])); } return y; } ```mofeiZ · 6584a6ee · 2025-03-18
- 0.9ETVStop creating Owner Stacks if many have been created recently (#32529) Co-authored-by: Jack Pope <jackpope1@gmail.com>Sebastian "Sebbie" Silbermann · 4a9df081 · 2025-03-23
- 0.9ETV[compiler] Fix error description inconsistency (#34404) Small fix to make all descriptions consistently printed with a single period at the end. Ran `grep -rn "description:" packages/babel-plugin-react-compiler/src --include="*.ts" --exclude-dir="__tests__" | grep '\.\s*["\`]'` to find all descriptions ending in a period and manually fixed them. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34404). * #34409 * __->__ #34404lauren · 80d7aa17 · 2025-09-06
- 0.8ETV[rust-compiler] Carry uninspected AST subtrees as raw JSON text (#36730) Stacked on #36729 (upstream rejects cross-fork base branches, so this targets main as a draft; the first commit belongs to the parent PR. Review the last three commits. Will rebase and mark ready when #36729 lands.) Unmodeled AST subtrees (type annotations, class bodies, unknown statements) were stored as `serde_json::Value` trees: every node allocated through a `Map<String, Value>`, and pass-through subtrees were repeatedly traversed by code that never looks inside them. They are now `RawNode`, a newtype over `Box<RawValue>` holding the original JSON text verbatim. Design notes, since two obvious alternatives fail: - Bare `RawValue` fields break under `#[serde(tag = "type")]` enums: internally-tagged deserialization buffers content into serde's private `Content` tree, which `RawValue` cannot read from. `RawNode::deserialize` instead streams whatever deserializer it is handed through `serde_transcode` into a fresh JSON string, which works behind tagged enums, `flatten`, and `from_value` alike. - Default-limit reparses break deep ASTs: internal `RawNode` reparse sites use `from_json_str_unbounded` (disables serde_json's 128-level recursion limit, matching how the top-level parse is configured); regression-tested with a 400-deep statement chain. `parse_value` fails loudly on malformed text rather than masking corruption with `Value::Null`; RawNode holds valid JSON by construction. Size-neutral in the shipped binary; the win is structural (no speculative `Value` trees on the hot path, pass-through subtrees stay untouched text). Verified on this exact tree: cargo workspace tests, both snap channels 1804/1804. --------- Co-authored-by: Boshen <1430279+Boshen@users.noreply.github.com>lauren · b49e0415 · 2026-06-11