This pull request has been designated for reprocessing. Please check back in a few minutes.

[Ledgers] Write Ledger entries to the response stream #37590

Open
acdlite opened 3:07pm on September 11, 2026 wants to merge 978 Ξ” into facebook/react ledgers/2-dedupe-map from
ledgers/3-write-entries

Pull Request Overview

  • Opened on September 11, 2026
  • Status Open
  • Commit count 2 with first commit September 11, 2026

Total Delta

978 Total Diff Delta

Open Days

Open 6 weekdays

Test Delta

0 Diff Delta in Test Files
Breakdown by Phase

How long has this pull request spent in each phase of its lifecycle?

Data pending calculation for pull request

Author avatar

[Ledgers] Write Ledger entries to the response stream

Implements addToLedger and writes Ledger entries to the response stream. The decoding side is not yet implemented; will be added in a subsequent PR.

Rendering model

The rendering model for Ledgers reuses existing concepts and machinery as closely as possible. Ledgers are streamed incrementally like all other data in Flight. The server does not buffer ledger values (except during the flushing window). Materializing an accumulated ledger value is handled on the client, rather than maintaining a running balance on the server. This is a core part of the design: Ledgers never block the rendering of the main data response, nor do they affect Flight's ability to cache them.

Similar to preload hints, a Ledger entry is written to the stream using a separate set of response rows from the main data. The client can decode these rows without having to decode the corresponding data. This is important because it preserves the laziness of the client's processing work. Likewise, the work needed to materialize a Ledger on the client (i.e. traversing the graph and reducing the entries to an accumulated total) is lazily performed only if something awaits the result.

In general, a key design principle is to avoid new bookkeeping or complexity on the server by deferring the computation to the client. Additional work during the decode is relatively cheap, especially since it's lazy. Whereas any additional complexity on the server risks compromising streaming or caching performance.

Unit tracking

The main part of the design that's novel to Ledgers is that we must track which "unit" of work spawned a Ledger entry. A unit is any reusable bit of rendering computation, i.e. a Flight task or a React.cache entry. This is required to enable caching and deduping of Ledger entries.

Unit tracking is the one part of the design that requires overhead that is not proportional to Ledger usage, because the units must be tracked even before we know any entries have been written. For example, we must be able to dedupe a cached object before it has fully finished rendering, and therefore before knowing whether it will write Ledger entries later in the stream. However, the overhead is measured to be relatively minimal, and while it's not proportional to Ledger usage, it is proportional to caching and deduping.

I used synthetic benchmarks to measure the impact while iterating on the implementation. On a realistically shaped page (about 1,000 components, 170 KB of payload, 500 React.cache hits), unit tracking adds on the order of 20 Β΅s to about a millisecond of Flight serialization, roughly 2%, and under 0.1% of a request that waits on data.

Theoretically, in the future we could use Units for replaying other kinds of render effects, like prefetch hints. Currently there's no reason to do this for hints because hints are intentionally hoisted to the top of the response.

Additions to row protocol



  • K: Declares a Ledger type's identity. The body includes Ledger's kind (Bit, Set, etc).


  • Y: Represents a captureLedgers instance. The body refers to the Ledger type declaration (its K row).


  • Q: Declares a Unit, written lazily the first time a Ledger entry is rendered inside it.


  • Z: Data entries added to a Ledger. One per Unit per Ledger per flush. (Notice this is not the same as once per addToLedger call. If multiple addToLedger calls to the same Ledger happen in the same flush window, they will get batched into a single Z row.)


  • F: Represents a reused reference. One per Unit per flush. This combined with the Q rows are what enables Ledgers to be cached and reused across different parts of the tree.

No comments have been left on this PR.