Problem/Motivation

When rendering an entity list e.g. in views,
- all the entities are loaded in one query - good
- when rendering all **dependent** entities are loaded one by one - bad

Proposed resolution

- Before rendering, ask the entity display / formatters, which dependent entities are required, and preload them.

This issue is about adding and leveraging the necessary API to do that.

Remaining tasks

...

User interface changes

None.

Introduced terminology

TBD

API changes

API additions TBD.

Data model changes

None.

Release notes snippet

Entity list renderers can now load all required entities in one go.

Comments

geek-merlin created an issue. See original summary.

geek-merlin’s picture

Title: Find a way to collect and preload dependent entities required to render » Find a way to collect and preload all required entities before rendering
Issue tags: +Performance
catch’s picture

We already have the API for this to an extent - it gets the first level of entity references, see EntityReferenceFormatterBase::prepareView().

@berdir has been working one that recurses down #3558370: Expand preloading in formatter.

The issue with views is it doesn't use any of this, because the entity view row plugin can only operate row by row. There would need to be some way for it to operate on the full result set.

berdir’s picture

See also some thoughts in #3548293: Use #lazy_builder / placeholdering for entity rendering. The preload works well for ERR/paragraphs, because paragraphs aren't render cached, so there's a big benefit in collecting all referenced entities. If we're going to render a bunch of paragraphs, including nested ones, there are reliable big gains in that, which is what the ERR issue is about.

However, with regular references, it gets more complicated. There's a good chance that those are already render cached, and then loading their referenced entities will look good in a cold cache performance test, but in practice, we might load things that we won't need. We already have built-in bulk loading if things are rendered in fibers, so getting more things to be build within separate fibers is likely better to only preload things that we will actually need.

I'm not sure there's clear benefit to doing more and it might be better to focus on the lazy builder/fiber issues instead of more explicit preloading in core. See also https://www.md-systems.ch/en/blog/2025-12-16/performance-improvements-dr....

catch’s picture

Yes if we can get #3548293: Use #lazy_builder / placeholdering for entity rendering working we could even potentially remove the prepareView step/API from entity references and rely on that, but we'd need to get all the way through before it's worth looking at. It would potentially simplify things a bit though, not that fiber preloading isn't complicated but it all happens in one place.

berdir’s picture

See my comment in #3558370: Expand preloading in formatter, I don't think we really need to wait on deprecating prepareView() and buildMultiple(). It's incompatible with render caching. We already realized that back in 2017 in #2843565: getViewBuilder('node')->viewMultiple() bypasses render cache that viewMultiple() is incompatible with render caching and I already suggested back then to deprecate it, where we committed a fix but had to revert it because it broke comments.

I think we should pick that back up and instead of fixing it, deprecate it, that should make it easier to deal with BC-breaking changes. And once that happened, keeping prepareView() is pointless as it can not possibly be called on more than one entity at once. Only question is how exactly we handle deprecating that, how do we detect when we need to call it, where do we deprecate it.

catch’s picture

Oh yes good point, let's try to revive that one then.

geek-merlin’s picture

Thanks a lot for the pointers!

> prepareView() and buildMultiple(). It's incompatible with render caching

"Incompatible" would mean that having both is impossible. How that?
The linked issues only say render caching has to be adjusted for that.

As @yched points out, it's a question about cold cache performance, critical in high-cardinality environments:
#2843565-15: getViewBuilder('node')->viewMultiple() bypasses render cache

On the overall "entity render cache kills multiple-entity view building" issue :

We kind of accepted that tradeoff after we discovered it, on the grounds that the multiple-view optimization we lost only impacted render-cache misses. But that's assuming there is a reasonably high hit ratio on entity render cache :-)

If I'm not mistaken, that assumes :
- uncacheable / high-granularity formatters do the right job of being placeholdered
- the entities' cacheability (and notably the cacheability of 'view' AccessResults) is not too granular either.

Anecdotical evidence: I worked on several projects with such high-cardinality views. So my experiance is not "that's neglegible".

catch’s picture

The issue is that on a render cache hit, you end up preloading entities that don't need to be rendered. If the entire list of entities is render cached at a higher level (dynamic page cache or page cache), then the full process including the preloading gets skipped, but at the same level of entity rendering/preloading ,there is no way to do 'selective preloading' or not that we've found. So there's always a trade-off between cold cache / warm cache performance.

#3518668: Use Fibers for rendering views rows bypasses this problem because the entities are loaded 'on demand', but then also held via fibers until the last possible moment and loaded together. In 11.3 this already works for different entities in different blocks, what we don't have it implemented for is views listings or multiple cardinality entity reference fields. The Fibers logic is undeniably complex, but we should be able to remove all the individual 'fiber loops' once we have #3394423: Adopt the Revolt event loop for async task orchestration (and tbh I'm hesitant to add the views one without that issue landing, or at least any more after that one). The actual entity and path alias fiber-multiple-loading logic is not very complex though, that was the easy bit compared to all the rest.

berdir’s picture

The *only* places in core that currently call viewMultiple() in non-test code is in comment module, to render a comment thread. And it actively relies on render caching not working properly to do some shenanigans with threads.

Anything else, specifically including views, renders them one-by-one using ->view(). That's why I'm proposing to deprecate viewMultiple(). Even in core, fixing it broke two different entity types and in contrib/custom code, there might be more. Deprecating is easier in that it will still work if called, but the few places in core that do call it will no longer do so and affected cases have time to adjust.

The idea with the fiber-based bulk loading + combined with more fibers and lazy builders is this scenario: Rendering a list of 10 linked node teasers, each with a media (or whatever variation of this you can think of, blocks in canvas/layout builder, regular content blocks, views or entity reference fields, ...)

cold cache:
1. 10 nodes are bulk-loaded
2. 10 fibers are set up, one for each node, to render them.
3. one-by-one, they reach the point where they load the media entity, wait for the others until the last one gets there, and it goes back to the first, which loads all 10 medias together.
4. That one then also needs to load the file entity, again it stops there until it restarts on the first fiber, loading all 10 file entities together
5. now they all need to look up the alias, again, they stop until all reached that point, restart and load all 10 aliases together
6 (repeat for used terms and other references).

warm cache:
1. 10 nodes are bulk-loaded
2. 10 fibers are set up, one for each node, to render them.
3. render cache hits, done.

it can also be anything in between, maybe 4 or just one of the nodes is a miss as they were invalidated and so on.

Active preloading doesn't know if the render caches are going to be a hit and on which entities and will end up loading the medias and maybe even files when they aren't needed.

There are cases where this won't be that perfect, for example if you have a multiple/variable amount of references and can't group them all. ERR/Paragraphs is a typical case for this as the paragraphs aren't render cached on their own, so we know we can preload stuff there more aggressively.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.