Skip to content

Performance: Reuse bulk reads in field menus and breadcrumbs#217

Merged
priethor merged 8 commits into
mainfrom
perf-fetching-fields
May 20, 2026
Merged

Performance: Reuse bulk reads in field menus and breadcrumbs#217
priethor merged 8 commits into
mainfrom
perf-fetching-fields

Conversation

@priethor

Copy link
Copy Markdown
Collaborator

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.

  • The four bulk GET /wp/v2/crtxt_fields?include=... requests stayed the same. Those already happened on main; this PR removes the duplicate per-field requests on top.
  • Later individual GET /wp/v2/crtxt_fields/<id> requests dropped from 43 to 0. 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

  1. Open a collection table and open a field column menu.
  2. Check that rename, edit field, edit options, change type, calculate, duplicate, and delete still use the right field label and type.
  3. Open a page and a collection from the sidebar and check that breadcrumbs still render.
  4. Open a page or collection directly by URL, ideally one outside the first 100 sidebar records, and check that its breadcrumb appears.

@github-actions

github-actions Bot commented May 19, 2026

Copy link
Copy Markdown

Performance vs main

3 scenarios with notable timing changes.

Scenario Workload Timing
Open collection (cold) - ready -42.1%
Create row - rows API -92.2%
Warm shell navigation - ready -38.4%

Full numbers in the job summary.

Comment thread docs/architecture/shell.md Outdated

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the best Gutenberg issue to point to?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sharing more insight from a quick exploration:

Screenshot 2026-05-20 at 12 54 16
  1. fresh site containing only one post, Hello World, with ID 1
  2. call getEntityRecords('postType', 'post') -> fetches a page consisting of [{ id:1, slug: 'hello-world', ... }]
  3. call getEntityRecord('postType', 'post', 1) -> no request triggered, value returned immediately
  4. 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?

Screenshot 2026-05-20 at 13 10 52

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?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.”

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/hooks/useEntityBulks.js Outdated
Comment thread src/components/CollectionFieldsContext.js
@priethor priethor merged commit 793cee0 into main May 20, 2026
11 checks passed
@priethor priethor deleted the perf-fetching-fields branch May 20, 2026 11:27
@priethor priethor added this to the 0.1.0 milestone May 28, 2026
@priethor priethor added type: enhancement Improvement to existing behavior. area: performance Performance budgets, profiling, benchmarks, and regressions. labels May 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: performance Performance budgets, profiling, benchmarks, and regressions. type: enhancement Improvement to existing behavior.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants