During Shopware Community Unconference 2022, we heard a lot about an internal performance week at Shopware, where they took tie to optimize different parts of the core of the Shopware platform. These were mostly performance problems that also affect our customers. Naturally, we are super excited about them their release in version 6.4.11. In this blog post we will look at one improvement to category navigation loading.
Comparing two Callgraphs of NavigationLoader
If you have a category-heavy Shopware 6 store, fetching and rendering the navigation menu would be a slow operation, we have seen from 100-500ms depending on the number of categories for different customers. Even smaller category trees required a disproportionate amount of time to render. We verified this with a demo shop, which the following numbers are taken from.
Comparing the performance of the NavigationLoader::loadTree function before and after the update to 6.4.11 shows roughly 15% performance gain for a store with 3097 categories (generated by the framework:demodata command).
We can see the gain by searching for all calls to the NavigationLoader class in the callgraph table view.
The whole tree fetching takes 163ms in the old code and just 14ms in the new code. What was changed?
Reducing Loop Complexity to Improve Performance
The commit to fix this performance bottleneck is not straightforward by glancing at it, but with a bit of time we can see that instead of an O(N^2) loop through all categories to build the navigation tree, it is now iterated only O(2N) times. See the Big O Notation for complexity of an algorithm if you are unfamiliar with this notation.
A quadratic complexity means that for each category all categories are iterated once again. For 2 categories we have 4 iterations, for 1,000 categories we have 1,000,000 iterations. This perfectly explains why the performance gets so much slower for large numbers of categories and you can see evidence of this in the Tideways Callgraph screenshot above.
CategoryEntity::getParentId was called 277409 times more often in the old code. This is a very simple getter, but the number of calls in the old algorithm made up for 41ms of execution, 25% of the 163ms the NavigationLoader took in total.
The new algorithm only iterates over all the categories twice.
The benefit of this performance improvement cannot be understated because it is a positive effect on all uncached pages in the store: Homepage, category pages, product pages, everything.
How was this data generated and measured?
Using the Ansible automation of the Shopware 6 Benchmarking Tool, we have set up a 2 machine cluster on Digitalocean for this test, with one web node including Redis and one database node with MySQL, both having 4 Cores, 8 GB RAM, and SSDs. The shown numbers should not be considered as absolute improvements, instead, you should see a similar performance improvement in relation to the total request time.