[Performance] One cache write can produce N React commits for N independent useQuery consumers
Summary
While upgrading from Apollo Client 3.6.9 to 3.14.1 in a React 18 application, we observed that one logical cache update could produce substantially more React commits when multiple independently mounted components called useQuery for the same query.
We created a public reproduction that removes network and application code:
- Runnable benchmark: https://superliaye.github.io/apollo-react-commit-benchmark/
- Source and methodology: https://github.com/superliaye/apollo-react-commit-benchmark
The benchmark performs exactly one changed cache.writeQuery. All consumers receive the same final value and the final UI is identical.
The stable result is the number of query-result commits:
Independent useQuery consumers |
3.6.9 | 3.14.1 (same as v4) | 3.14.1 grouped-delivery diagnostic |
|---|---|---|---|
| 1 | 1 | 1 | 1 |
| 2 | 1 | 2 | 1 |
| 4 | 1 | 4 | 1 |
| 8 | 1 | 8 | 1 |
The fixture also performs downstream derived-state work after each query commit. Including that work, React Profiler reports 2 total commits for 3.6.9 versus 2N for stock 3.14.1.
Source-level explanation
Both versions asynchronously notify independently watched queries. The React-facing update path differs:
Apollo Client 3.6.9
The observer stores the result and calls an ordinary state setter through setTick:
In the tested React 18/Chrome environment, these ordinary updates remain pending and coalesce into one render.
Apollo Client 3.14.1
The observer passes React’s handleStoreChange callback into setResult:
React’s native useSyncExternalStore path calls forceStoreRerender, which schedules SyncLane work:
In this reproduction, React flushes each update before the next independently scheduled observer delivery:
delivery 1 → handleStoreChange(SyncLane) → query commit 1
delivery 2 → handleStoreChange(SyncLane) → query commit 2
...
delivery N → handleStoreChange(SyncLane) → query commit NThe lane attribution is based on the pinned Apollo and React sources. The benchmark does not independently manipulate React lanes.
Causal diagnostic
The benchmark includes a third arm using the same Apollo Client 3.14.1 package, component tree, React path, values, and delivery order.
The diagnostic changes only when the pending observer callbacks are delivered: it drains them together within one shared task.
This restores the single query-result commit for every tested subscriber count without dropping, reordering, or deduplicating values.
The diagnostic uses Apollo private objects and adds a task of delay. It is evidence for the cause, not a proposed production patch.
We understand that PR #11083 addressed update-ordering correctness. We are not suggesting blindly restoring the older setter path.
Expected behavior / questions
We expected one cache write affecting equivalent query consumers to be publishable in one React commit, rather than one synchronous commit per independently scheduled delivery.
Could the maintainers help clarify:
- Is one
SyncLanecommit per affectedObservableQueryan expected consequence of the currentuseSyncExternalStoreintegration? - Is there a supported way for Apollo to group cache-driven observer deliveries before invoking their
handleStoreChangecallbacks while preserving the ordering guarantees from #11083? - Does Apollo Client 4 intentionally retain or change this behavior?
Reproduction steps
- Open https://superliaye.github.io/apollo-react-commit-benchmark/.
- Leave the default subscriber counts (
1,2,4,8) and 400 rows per subscriber. - Click Run proof.
- Compare Apollo Client 3.6.9, Apollo Client 3.14.1, and the grouped-delivery diagnostic.
- At eight consumers, observe one query-result commit for 3.6.9, eight for stock 3.14.1, and one for the grouped-delivery diagnostic.
To run locally:
git clone https://github.com/superliaye/apollo-react-commit-benchmark.git
cd apollo-react-commit-benchmark
bun run check
bun run devScope
This reproduction does not claim that every Apollo Client 3.14.1 application is slower. The effect requires fan-out across multiple independently mounted query consumers, and the user-visible cost depends on the work performed by the affected React tree.
The deterministic evidence is the commit sequence. Millisecond measurements vary by browser and machine and are treated as secondary evidence.
Versions
@apollo/client: 3.14.1- Comparison baseline: 3.6.9
- React: 18.3.1
Source: apollographql/apollo-client