Use `TransferState` to hand server-rendered Firestore data to the browser, instead of reading every document twice
- 主要语言
- TypeScript
- 星标
- 7.8k
- 派生
- 2.2k
- 平均合并
- 22 小时 28 分钟
- 30 天内合并 PR
- 6
描述
### 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. `TransferState` appears in `src/` 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](https://github.com/angular/angularfire/issues/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 with `querySnapshotFromJSON`.
- 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 `limit` clause 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 `getCountFromServer` cannot 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 `limitToLast` query 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](https://github.com/angular/angularfire/issues/1225) asked for this in 2017.
- [#2018](https://github.com/angular/angularfire/pull/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](https://github.com/angular/angularfire/pull/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 `Timestamp` and `DocumentReference`.
- 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.
贡献指南
评估
这个 Issue 还没有评估数据。