Problem/Motivation
Recently we discovered a significant performance issue on a site we maintain. In our case, there were a combination of issues leading to 10+ second loads on a menu page. It turned out we had code that was making many tens of thousands of calls to load a config page value.
While we fixed the underlying issue, we also discovered that the \Drupal\config_pages\ConfigPagesLoaderService::load method doesn't do any caching of loaded entities.
Here's a call graph showing the path looping loading the same entity 10,000 times:

Steps to reproduce
Benchmark code like:
for ($i = 0; $i < 10000; $i++) {
\Drupal::service('config_pages.loader')->load('name_of_form');
}
Proposed resolution
Add or enable the built-in static cache for config pages. Possibly remove loadByProperties for calls where we load by a config page ID.
Data model changes
| Comment | File | Size | Author |
|---|---|---|---|
| 280390222-445b516e-98cc-40ea-b3a6-b1aae928a208.png | 573.8 KB | deviantintegral |
Issue fork config_pages-3399222
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
heddnI'm seeing similar performance issues on a site too. Going to see what can be done about it.
Comment #4
shumer commentedComment #6
shumer commentedHey folks!
@heddn and @deviantintegral would you be able to try the code from the PR? It solves the issue for me.
The other question I have, what was the use case for the ConfigPages for both of you? I mean I can hardly imagine the situation like this, so I'm interested in what are you doing with this module? Hope that will help me to avoid issues like that in future releases.
Thx
Comment #7
shumer commentedComment #8
nicolas bouteille commentedHello,
In the code of the PR 34, I see the $cache variable lives in the config() function.
Am I right to assume that the config page entity will be "cached" only during a single session ? Or maybe even a single request ? And that it only relieves the site when too many loads are done for a single request, but has no effect if multiple visitors load the config page in a short time?
In my website, I would like to check a config page's value on every single request made to the website.
The config page's value will not change frequently at all. So loading the config page entity on every page request or every session is overkill.
Which is why I am considering caching the data I need and removing it from cache whenever the config page is updated.
But I was wondering… do I really need to do this myself or config page / Drupal is already caching stuff?
So I stumbled upon this issue and it looks like no caching is done...
As mentioned above, no cache is made with loadByProperties() unlike load().
So as suggested above, shouldn't we use load() when context is null? Or is it because when context is null, we still need to fetch and specify the default context that we cannot do that?
BTW, on this documentation page : https://www.drupal.org/docs/contributed-modules/config-pages/usage-of-co...
The third option is to get a config page via storage manager:
this does not mention the context... so is the context not that important, or is the documentation incomplete?
If we cannot use load() instead of loadByProperties(), what do you think of caching the config page entity using cache_set() and remove it when it is updated? Or is it too heavy and we'd better only store simple values that we need to access frequently?
Comment #9
nicolas bouteille commentedOk I have spotted that $storage->load() has been overridden in ConfigPagesStorage to actually call ConfigPages::config($id); unless the $id is numeric...
for that reason, calling $storage->load() inside ConfigPages::config would create an infinite loop
also I thought calling $storage->load('my_config_page') directly would be more efficient than using ConfigPagesLoaderService or ConfigPages::config because they use loadByProperties, but in the end all those three methods call loadByProperties in the end...
Comment #10
shumer commentedThe way the entity is loaded here is not like how other entities in Drupal work. The reason for that is that we have Context enabled, so while the entity is being loaded, we need to evaluate which one exactly we need. We can't cache that enity between requests as the context might be different. In case of specific need you can cache the load in the custom code.
When it comes to a static cache we can add that option, but I'm still curious about the use case. I haven't seen anything like this before, despite I'm using that module since 2014... Can anybody from this topic provide me some info about the use case when this performance degradation appears?
Comment #11
shumer commentedFinally got some time to make a deeper look at the issue.
The bottleneck is the entity query inside
loadByProperties()inConfigPages::config(). Each call executes:ConfigPagesType::load($type)- already cached by core's memory cache, no issue here$type->getContextData()- computes context (invokes context plugins)loadByProperties(['type' => ..., 'context' => ...])- runs an entity query (SELECT id FROM config_pages WHERE ...) every single time, then passes the found IDs toloadMultiple()Core's
loadMultiple()does use memory cache for the entity objects themselves - once entity ID 1 is loaded, subsequentload(1)calls hit the cache. ButloadByProperties()always runs the query to find the ID first. So 10,000 calls toConfigPages::config('my_type')means 10,000 identical SQL queries just to discover that the answer is entity ID 1.Proposed solution in MR !34
The MR adds
static $cacheto theconfig()method. This works for the basic case but has issues:ConfigPagesType::load()results - unnecessary, core already handles thisstaticvariables persist for the entire PHP process, which can cause subtle bugs in Drush commands, queue workers, and tests where multiple operations happen in a single processTwo better approaches
Approach A: Cache only the type+context to entity ID mapping
The core insight is that we only need to cache the lookup result - the mapping from
(type, context)toentity_id. Once we have the ID, core's memory cache handles the rest viaload($id).Pros:
load($id)which uses memory cache with proper invalidation - if the entity is saved,resetCache()is called automatically by core, and the nextload($id)returns fresh dataCons:
static- persists for the entire process (affects Drush, tests, queue workers). For config pages this is acceptable since types and contexts rarely change mid-process.getContextData()is still called on every cache miss per type. If context computation is expensive, this could be cached too, but it is typically cheap.Approach B: Use core's entity.memory_cache service
Instead of a
staticvariable, use Drupal'sentity.memory_cacheservice (aMemoryCacheInterfacebacked by PHP array with cache tag support). Store the(type, context) -> entity_idmapping there and tag it with the entity's cache tag for automatic invalidation.Pros:
EntityStorageBase::resetCache()which invalidates theentity.memory_cache:config_pagestag, clearing our lookup cacheCons:
\Drupal::service()in a static method - not ideal for testability but consistent with howconfig()already worksMemoryCacheInterfaceservice alias is deprecated in Drupal 11.3.0 (removed in 13.0.0)I would say we can go with A option and migrate to B later if needed.
Comment #14
shumer commented