Use `TransferState` to hand server-rendered Firestore data to the browser, instead of reading every document twice
@armando-navarro is already working on this.
Since Aug 31, 2026.
Assessment
This issue has not been assessed yet.
Description
Summary
An Angular app rendering on the server fetches Firestore data, renders the page with it, sends that HTML to the browser, and then fetches the same documents a second time when the app starts.
- Angular already solves this for its own HTTP client. The server's responses go into the page and the browser reads them back instead of repeating the request.
- Nothing does that for Firestore.
TransferStateappears insrc/only in the legacy compat storage pipe. - Apps therefore write the handoff themselves, once per component, or accept the second read.
This proposes an opt-in transfer layer for Firestore reads.
What users hit today
- The server-rendered content disappears for 319 to 506 ms after the app starts, until the browser's own read returns.
- Every document read on the server is read again in the browser, so a page costs two reads per document rather than one.
- The workaround is to write it by hand per component. The sample app in this repo does exactly that in five components, two of them Firestore reads, and #3323 is eight comments of people doing the same thing with mixed results.
What is proposed
An app adds provideFirestoreTransfer() and marks the reads it wants transferred:
docData(ref, { transfer: true })stores the document's values on the server and emits them in the browser as the read's first value, so the rendered content stays on screen.collectionData(query, { transfer: true })stores the query's serialized snapshot (QuerySnapshot.toJSON()), which carries the query itself, and the browser rebuilds a real snapshot from it withquerySnapshotFromJSON.- Reads without the option, and every read in an app that does not add the provider, behave exactly as they do now.
For a query with no limit clause, the browser then attaches its listener with onSnapshotResume, which asks the backend for changes since the server's read rather than for the documents again. That is what removes the second read.
What this will not do
- A query with a
limitclause keeps its second read. Resuming a limited query makes the backend re-send the whole result set, which measured worse than not transferring at all, so the library will transfer the data for the first paint and attach an ordinary listener. Whether resume can support limited queries is a question for the Firestore team. - Aggregate reads such as
getCountFromServercannot participate, because their results have no public serialized form. - A read of a document that does not exist is skipped. There is no value to put in the page, and Firestore's serialized form for a missing document throws when the browser tries to rebuild it.
- A
limitToLastquery is skipped. The serialized query does not record that the limit counted from the end, so the browser would rebuild a query that no longer matches the one the app is running, and the documents would come back in the wrong order. - Data read during a server render that is signed in as the visitor belongs to that visitor, and putting it in the HTML is a hazard wherever that HTML gets cached. The library will warn in development when this happens, and the documentation will cover it.
Earlier attempts in this repo
- #1225 asked for this in 2017.
- #2018 built an experimental version in 2019 and was closed in favor of exploring Firestore bundles, which is the serialized form the query half of this proposal now uses.
- #1547 was the same idea for the Realtime Database and was also left unfinished.
The same gap exists outside Firestore
The gap is library-wide rather than Firestore-only. Firestore is proposed first for three reasons:
- Its values include types that plain JSON loses, such as
TimestampandDocumentReference. - Its queries carry no obvious identity for the browser to match a transferred value against.
- Its reads are billed per document, so the second read has a direct cost.
Realtime Database and Storage are simpler on all three counts, since their values are already JSON or a plain string and the path identifies the read. Both should be able to reuse whatever this builds, and each is worth its own PR rather than being folded into this one.
If you are hitting this today, saying which reads and whether your queries are paginated would help, since the paginated case is the one with an open constraint above.
- Dominant language
- TypeScript
- Stars
- 7.8k
- Forks
- 2.2k
- Avg merge
- 3d 6h
- Merged PRs (30d)
- 5
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from angular/angularfire
-
comp: build/pipeline type: bug version: current (v17+)
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
angular/angularfire#3766 ·
-
comp: schematics type: bug version: current (v17+)
Difficulty 3/5 1-2 days Newbie friendliness 76/100
angular/angularfire#3768 ·
-
comp: docs type: chore version: current (v17+)
Difficulty 4/5 3-5 days Newbie friendliness 58/100
angular/angularfire#3764 ·
-
Six `firebase` entry points have no `@angular/fire` equivalent, so their exports are unreachable Opencomp: core type: feature
angular/angularfire#3755 · 1 assignee ·
-
comp: auth comp: ssr comp: zones type: bug
Difficulty 3/5 1-2 days Newbie friendliness 76/100
angular/angularfire#3748 ·
All issues in angular/angularfire
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
-
bug v2
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
modelcontextprotocol/inspector#2458 · 1 comment ·
-
Difficulty 1/5 Under an hour Newbie friendliness 75/100
railmapgen/rmp-gallery#4068 ·
-
Mend: dependency security vulnerability status: needs triage 🕵️♀️
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
carbon-design-system/ibm-products#9907 ·