Performance: Reuse bulk reads in field menus and breadcrumbs#217
Conversation
Performance vs main3 scenarios with notable timing changes.
Full numbers in the job summary. |
|
|
||
| The shell already keeps a few bulk queries warm: active pages and collections from the sidebar, plus the open collection's fields from `CollectionFieldsProvider`. When a component needs one of those records, read it from the bulk instead of calling `useEntityRecord` by id. | ||
|
|
||
| WordPress core-data tracks bulk and per-id resolvers separately ([gutenberg#19153](https://github.com/WordPress/gutenberg/issues/19153)), so a per-id read can make another HTTP request even when the record is already in the bulk cache. The duplicate request grows with surfaces that render one cell per record, like a wide column header. |
There was a problem hiding this comment.
Is this the best Gutenberg issue to point to?
There was a problem hiding this comment.
I think so, it's the best reference I could find that clearly explains why this behavior is expected by design. Also, the guy answering seems very knowledgeable 😄
Do you recall any other "canonical" source?
There was a problem hiding this comment.
The thing is that I don't know if I agree with what 2019-me said. 😄 I think I focused on one aspect of the question (resolvers vs. selectors, and avoiding multiple requests when one is still ongoing) but didn't consider whether core-data could be smarter about the queried-items cache.
I also don't believe that I said anything about there being separate caches for individually requested vs. paged records (if anything, I suggested the opposite: "Calling a selector with a query that has already been satisfied will not trigger a new unnecessary request."), and yet the takeaway in this outdated hunk was seemingly contradictory: "so a per-id read can make another HTTP request even when the record is already in the bulk cache".
That's why I was asking about a proper issue about this: is the takeaway true? Because it struck me as something that I expected to have already been solved in core-data. If it's a real issue, it should be tracked. If not... well, then why was there a need to come up with tricks in this PR? For example, does the choice of queryArgs passed to useEntityRecords play a role in the fact that a subsequent call to useEntityRecord (singular) triggers a request?
For core-data stuff I trust other people much much more than myself. Jarda comes to mind, for example.
There was a problem hiding this comment.
Sharing more insight from a quick exploration:
- fresh site containing only one post, Hello World, with ID 1
- call
getEntityRecords('postType', 'post')-> fetches a page consisting of[{ id:1, slug: 'hello-world', ... }] - call
getEntityRecord('postType', 'post', 1)-> no request triggered, value returned immediately - call
getEntityRecord('postType', 'post', 2), which does not exist (and thus wasn't in the cache) -> request triggered -> failure
So what if there are timing issues involved? What if a select call for a single record happens while there's ongoing resolution of a page of records of the same entity?
Well, then core-data triggers the request for the single record, it won't attempt to wait for the page of records to resolve. This seems entirely reasonable. Could something like this be affecting performance in Cortext?
There was a problem hiding this comment.
Exactly, queryArgs is what's preventing the cache from hitting. The bulk request passes { context, _fields } to get the meta and marks getEntityRecord(kind, name, id, normalizedQuery) resolved per id. The per-id hook calls getEntityRecord(kind, name, id) with no query, so the arg shapes don't match.
Do you agree that this is probably worth an upstream issue? Something along the lines of “useEntityRecord should accept the same query useEntityRecords does.”
There was a problem hiding this comment.
It's definitely worth consulting someone more knowledgeable, just because conceptually "query args" don't really apply to per-id requests, even though the problem of cache keying is valid. Maybe we decide that passing a query is good enough, or maybe we add to useEntityRecord a more explicit parameter like "cache key", "query key", etc.
What
This trims duplicate requests in the shell. Field column menus now reuse the field data already loaded for the open collection, and breadcrumbs reuse the page and collection lists the shell already has. If a direct URL points to a page or collection outside those lists, breadcrumbs still fetch just that one record.
Why
The UI does not change. The shell was making duplicate REST requests for data it had already fetched in bulk: every column header was requesting its own field record, and breadcrumbs were re-fetching the active page or collection. The duplicates scale with the table's column count, so trimming them keeps the cost from growing with the schema.
Results
Measured in wp-env with the demo seed. The test loaded the Welcome page, which embeds three collection previews, then opened the Tracks collection. Across those four collections, the shell renders about 43 field column headers.
GET /wp/v2/crtxt_fields?include=...requests stayed the same. Those already happened onmain; this PR removes the duplicate per-field requests on top.GET /wp/v2/crtxt_fields/<id>requests dropped from43to0. Those requests came from column headers asking core-data for a field by ID, even though the bulk field request had already returned it.Savings scale with the number of column headers rendered, so a table with 100 columns drops 100 individual requests instead of 43.
Testing Instructions