Description
Version
Reproduction link
https://jsfiddle.net/anc6Lf23/2/
Steps to reproduce
- Open the JSFiddle
- You will see the time it takes to compute a value without caching of computed properties (~1 second) in the output
- Set "CACHE = true" in the first line of the JS part to see the time it takes to compute the value with caching (~2 ms)
This issue concerns SSR but for simplicity I created the fiddle which emulates the behavior of the server render:
CACHE = true
– the behavior we usually have in the clientCACHE = false
– the behavior during SSR
What is expected?
I would expect computed properties to have comparable performance characteristics on the server and client, so that I don't need to write custom code.
I.e. I would expect computed properties to be cached during SSR.
What is actually happening?
Computed properties are not cached and therefore have drastically different performance characteristics in some cases.
Description if the issue
Since computed properties are not cached during SSR some components unexpectedly take significantly longer to render. This is the case if it is heavy to compute the property or if it is accessed a lot.
I would usually expect a computed property to have constant time complexity (O(1)
) no matter how often we access it. But on the server it suddenly becomes linear time complexity (O(n)
). This is especially critical when the computed is accessed in a loop. When we have multiple computed properties relaying on each other, each containing loops, this gets really bad. Then it has polynomial time with the exponent being the amount of nested computed properties (E.g. O(n^3)
for three levels of computes, like in the JSFiddle)
Real world example
I noticed this issue because our server renderer suddenly took multiple seconds (5-8 seconds) to respond after enabling a new component for SSR. Normally rendering the app on the server takes about 100ms.
The effected component is part of a proprietary code base but it is similar to the one in the JSFiddle. You can see the component in production here:
- Open: https://www.ikea.com/de/de/bereiche/wohnzimmer/
- Click on the "Serien" button
- The flyout content is the effected component
After finding out about this I also investigated other occurrences of this:
In our Vuex store we did not have any nested getters with loops, like described above, however some of the getters are somewhat heavy to compute (~1ms) and accessed a lot in various templates. So I decided to introduce a very simple custom caching layer to our store. This sped up server rendering by about 20%.
This could also be a low hanging fruit for optimizing SSR performance:
Based on analyzing our own app, I would roughly estimate that caching all computed properties could speed up server rendering by about 30% in an average Vue.js app.
For me this issue was hard to understand because the affected code seemed harmless at first.
Mitigation
To mitigate this issue you can move access to computes out of loops: access them once, store them in a local variable and then use this variable in the loop. This is generally a good idea since any look up of a property on a Vue.js VM has a small cost.
However this is not possible if you have a loop inside your templates.
References
-
The code that controls this behavior:
vue/src/core/instance/state.js
Lines 208 to 219 in 0948d99
-
The issue that lead to this behavior being introduced: store logic is not reactive in data prefetching steps while doing ssr vuex#877
-
The commit that introduced this behavior 06741f3
-
Earlier occasion of someone stumbling over this: