You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Idea] Load installed modules once per request for Module::isEnabled() and getModuleIdByName()
#43034
They are called for most widgets and hooks rendered on a page. With the MySQL general log on a stock 9.2.0 (Hummingbird, demo data):
Page
Queries
Name lookups
module_shop lookups
Home
356
23
20
Category
208
17
14
Cart
242
17
14
So 12–15% of the queries on a front page are these single-row lookups. The results are cached per name for the request, so each distinct module still costs its own round trip. Shops with more modules pay more.
The solution
Load the installed modules with their status for the current shop in one query on the first lookup, and answer the following ones from that list:
SELECT m.`id_module`, m.`name`, ms.`id_module`IS NOT NULLAS`enabled`FROM`ps_module` m
LEFT JOIN`ps_module_shop` ms ON ms.`id_module`= m.`id_module`AND ms.`id_shop`=1
The list is kept per shop and dropped by install(), uninstall(), enable(), disable(), disableAllByName() and resetStaticCache(). That also fixes a small inconsistency: today install(), enable() and disable() don't clear the cached isEnabled() answer, so a later call in the same request returns the old state.
On the same pages this gives 1 query instead of 37–43. The rendered HTML is identical.
Alternatives
The Module constructor already builds a similar list ($modules_cache), but it is reset whenever the context controller isn't a legacy controller and carries upgrade state, so reusing it for lookups would be fragile.
ModuleDataProvider::getModuleIdByName() on the Symfony side has the same one-query-per-name pattern (it accounts for the 3 remaining lookups per page). That could be a follow-up.
Additional context
We have run a front-office-only version of this in production on an 8.2 shop since September 2026, where it removed roughly 2.7 million queries a day.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
The problem
Module::getModuleIdByName(),Module::isInstalled()andModule::isEnabled()run one query per module name, andisEnabled()a second one onmodule_shop:They are called for most widgets and hooks rendered on a page. With the MySQL general log on a stock 9.2.0 (Hummingbird, demo data):
So 12–15% of the queries on a front page are these single-row lookups. The results are cached per name for the request, so each distinct module still costs its own round trip. Shops with more modules pay more.
The solution
Load the installed modules with their status for the current shop in one query on the first lookup, and answer the following ones from that list:
The list is kept per shop and dropped by
install(),uninstall(),enable(),disable(),disableAllByName()andresetStaticCache(). That also fixes a small inconsistency: todayinstall(),enable()anddisable()don't clear the cachedisEnabled()answer, so a later call in the same request returns the old state.On the same pages this gives 1 query instead of 37–43. The rendered HTML is identical.
Alternatives
The
Moduleconstructor already builds a similar list ($modules_cache), but it is reset whenever the context controller isn't a legacy controller and carries upgrade state, so reusing it for lookups would be fragile.ModuleDataProvider::getModuleIdByName()on the Symfony side has the same one-query-per-name pattern (it accounts for the 3 remaining lookups per page). That could be a follow-up.Additional context
We have run a front-office-only version of this in production on an 8.2 shop since September 2026, where it removed roughly 2.7 million queries a day.
Do you plan to work on this subject?
All reactions