Sylius: [RFC] Provide an HTTP Cache system

Created on 18 Jan 2019  ·  22Comments  ·  Source: Sylius/Sylius

Info

Sylius version: ^1.4.x
Status: Draft

Summary

Add a way to serve at least, the following catalog pages with an HTTP Cache system :

  • Homepage
  • Taxon
  • Product

Introduction

Web performance should no longer be considered as a "nice to have" in 2019. This must be an obligation for modern tools like Sylius. This is especially important when talking about e-commerce projects because the display performance has direct impacts for both customers and e-merchants, for example:

  • Product views by visitor
  • Better conversion rate
  • In SEO, Google crawl budget optimized
  • Better Adwords Ads notation
  • IT less stressed, cost reduction

References

Fullstack Symfony projects with nice HTTP Cache implementations
https://github.com/sulu/sulu/tree/develop/src/Sulu/Bundle/HttpCacheBundle
https://github.com/ezsystems/ezplatform-http-cache
https://github.com/api-platform/core/tree/master/src/HttpCache

Approach

Sending cache information is something quite easy to do, especially with Symfony HTTP Cache. The caching configuration can be easier if we use the FOSHttpCacheBundle. It provides some nice ways to add cache headers to the Response.

As always, the tricky part is to invalid cache when some operations are done in the back-end. The invalidation could be done by invalidating some routes, but in my opinion, it will be hard to maintain for developers.

I would like to rely more on the tags system :

For example, if a page in a channel (id:1) needs taxon (id:5) and 2 products (id:7, id:8) to be rendered, then it will be tag like this :

channel-1, taxon-5, product-7, product-8

By tagging all the pages like this, the invalidation of multiple pages at the same time could be more easy.

Tags are supported by Symfony Reverse Proxy and Varnish (with xkey module) but, for now, I advice to stay focus only on getting something that works with Symfony Reverse Proxy.

As Sylius is done to solve 80% of common problems, we need to provide a way to implement the tagging of custom pages too.

Even if it seems pretty hard, tests have to be written (Spec / Behat)

Impact

If we want the default Sylius theme to work with this system, the layout should be cleaned to remove the customer context data and load those by async calls

The csrf token should not be cached into the pages (add to cart form) Some ideas are provided in Symfony Security Doc

The flash messages have to be handled, maybe with the FOSHttpCacheBundle way

POC

I'm running only some tests in local env with the Sylius demo data and got some nice results :

  • Homepage goes from 120ms to 20-25ms
  • Taxon page goes from 200ms to 20-25ms
  • Product page goes from 455ms to 20-25ms

Future Scope

  • Could be use to send http cache header when you work with Resources
    REST API
  • Provide a default varnish vcl file that works with xkey module
  • Provide a panic button in admin to reset a whole channel cache
  • ...
Do not stale RFC Roadmap

Most helpful comment

Without going into discussion if it is supposed to go to the core or a plugin, I'd like to see Varnish support from start.

Taking eZ Platform as an example, Symfony reverse proxy is in most cases not enough for production, so Varnish support via xkey module would definitely help.

All 22 comments

Sounds very interesting to me! My personal preference would be to have it as a plugin, but I can also see it land in core. Nothing to add at the moment, proposal is very complete.

My preference will go to Plugin too, but the installation could be more tricky because you will have to modify some files like "public/index.php" or "src/Kernel.php" by yourself to extend the right classes. It would be great to have some feedbacks from the Sylius team and community to see what we could do next.

My opinion is it should be enabled by default in the Sylius core, because this is like you said @kgonella it should no longer be considered as a "nice to have" feature.

What do you think about it @pamil @lchrusciel @pjedrzejewski @Zales0123 ?

Without going into discussion if it is supposed to go to the core or a plugin, I'd like to see Varnish support from start.

Taking eZ Platform as an example, Symfony reverse proxy is in most cases not enough for production, so Varnish support via xkey module would definitely help.

I can only agree that cache invalidation is hard :)

So it is smart to rely on proven things like the xkey tagging by FOSHttpCacheBundle which simplifies this a lot.

It is also nice to have some easy adhoc command for clearing cache from the command line and from the admin interface.

I was just wondering if there was some good reason for not having Edge Side Includes enabled in Sylius by default?

It would seem that it can be enabled. Allowing partial views to be cached on a gateway like Varnish. When there is no cache gateway available (the Surrogate-Capability request header is not set to something containing ESI/1.0), Symfony automatically falls back to the InlineFragmentRenderer by default anyway.

Enabling it would entail:

  • enabling the esi renderer in config/packages/framework.yaml
  • using the twig function render_esi() instead of render() where applicable/desired

To me it seems that it would not impact the default behavior of Sylius in any way, but maybe someone has some good arguments on why it should not ever be enabled by default.

BTW I think that for implementing caching in Sylius you need to consider that full page caching is not feasible. There are some dynamic elements on every page (the user menu in the right top and perhaps the currency picker for example). You already mentioned the flasher.

So in order to implement HTTP caching you are going to have to use some kind of technique like ESI, SSI, etc.. to be able to cache only partial views or fragments.

As for the POC. It sounds great, but can we see the evidence somewhere? It would be awesome to be able to see the code as well and perhaps it will also be easier for people to form an opinion on your solution and provide feedback (or maybe even help you build/finish it).

Not trying to rub anyone the wrong way, but these are my thoughts about the current state of this RFC.

Unless I'm missing something I don't see the advantage of using ESI. At least in a default Sylius setup (It might make more sense in a complex microservices architecture). The point here is where we do the final composition, at the browser level (async calls) or at the gateway level (ESI).

If we do it at the browser level, we can serve the main content to the user (and google) straight from the cache gateway. The perceived speed is lightning fast and the user can start consuming the requested content right away while we load the partials (user menu, shopping cart, etc.) asynchronously.

On the other hand, with ESI we always have to reach the backend, holding Varnish back from returning the composed page until all partials have been processed. We save some load to the backend, but the page load time will be higher.

I truly think that it depends on your cache rules. You can put short cache times (or proper invalidation) on the partials as well, allowing the whole content to be served from the cache gateway.
Also you have to look at the work involved in both solutions. When you go with async calls to load the dymanic partials, you introduce more complexity to the frontend, have to rewrite a lot to achieve it, as with using ESI you can suffiece with replacing the render() calls with render_esi() calls.
Besides that if you look at performance in web applications in general, the heavy part is usually DOM modifications (the reason that frameworks like react.js have a virtual DOM), I can't back this up with numbers, but chances are that the gateway can assemble the page faster as it doesn't use a DOM.

Using ESI for things in twig templates is not a bad idea because twig templates ready use partials in them by calling controller actions for things like taxonomy menu, product listings and things like that - almost all hot paths can be cached without much effort. It also knows about all the passed params, so you can cache search results too without any issues. Mosr parts of the app are easily cachable to the tune enable esi, enable cache kernel, wrap an action in call in a decorator that sets appropriate headers, replace render call with render_esi, done. Literally takes 2-3 minutes. Saved me just on taxonomy menu rendering on a bigger catalog 400 ms.
Also render_esi is a thin wrapper around render call so you can just flip the config switch and caching will be disabled and render_esi will work like regular render without any issues.

I think it's obvious that support for both, ESI rendering of fragments and full page caching is needed :)

@emodric For which pages would you want to use full page caching?

I haven't used Sylius all that much, so I can only speak from eZ Platform experience, but eZ Platform benefits A LOT from full page caching of categories which list content, as well as full views of articles with complicated data model. Combine that with user specific caches via ESI calls, and you have a powerful caching mechanism with best features taken from both worlds.

OK let's differentiate ESI caching from full page caching in the following way:
Full page cache: caches the full page, including inline rendered fragments, no user context is considered
ESI cache: caches only fragments, user context can be considered

You can have ESI caching without caching full pages, however you cannot have ESI caching when caching a full page (as the fragments come from a different source, you are not storing a full page in the cache, but a page with partials/fragments that are to be assembled by the gateway).

To be quite honest I think starting off by using ESI caching for the horizontal/vertical taxons menu and perhaps the product lists as mentioned before would be a good (and small enough) start. From that point on iterations can be made to introduce more complex caching of for example the user context based partials. Otherwise you might have a very big scope to work on from the start (without any real results untill you finish all of the work). Taking small steps is much better IMO.

Currently we have some views that can be cached pretty much out of the box, so why wait for a full on caching solution?

Also to get an overview of what needs to done in total, it would be wise to map out the pages in Sylius and mark the partials without user context, the partials with user context and the rest. To be able to check if we're actually improving the performance we could tag them with some average load/render times. That way we don't have to talk about caching in general, but very specific on what to cache where and how, within Sylius, not some other platform.

For me the reason to use ESI caching on the menu's for example is because they do not depend on the user context and I had to modify them to only show taxons in the menu's that actually have enabled products with a translation in the current language. So the added query and rendering time were slowing down the load time of a lot of pages significantly.

however you cannot have ESI caching when caching a full page

Hm, not sure if I understood you correctly, but it is possible to have caching enabled for the full page with some ESI blocks. The full page will cache the output with ESI tags, then fragments will be requested independently. So, depending on your needs you can have:

  • full page with no cache and ESI blocks with cache
  • full page with some cache and ESI blocks with no cache
  • and full page with cache and ESI blocks with cache but different strategy (shorter or longer ttls, etc)

This is all possible, and we use this everyday

@ilukac I think you're already answering your own question by saying that the "full page" is cached including the esi tags. The fact that the page that is cached contains the esi tags already implies that it is not the "full" page that is cached, but a page containing references to fragments that are also cached. Anyway call it what you want to call it.

My original point was that caching a full page (including it's rendered partials/fragments) is not feasible, because on pretty much every page in Sylius there are partials/fragments that depend on user context. So that problem has to be solved somehow. Introducing async cals and using AJAX is an option for that and so is using ESI/SSI. So if anything let's discuss the pros and cons of those solutions and try to make some well informed decisions?

I already made some points in favor of using ESI in this comment: https://github.com/Sylius/Sylius/issues/10106#issuecomment-466634369

It looks like @psihius is on board with that, however it also seems that @anthid for example is not.

I did not expect there would be this much discussion about what a full page cache is and what not, nor do I feel it is a very useful discussion, sorry guys, I truly wish you'd respond in a more cooperative way, like expressing your opinion/views on how to go about designing and implementing this feature. Instead of bickering over semantics.

Well, @4c0n I am also sometimes frustrated with losing time on discussion, but its necessary that we all have a common understanding of the topic and call the same things with the same name. Otherwise, we will pull in different directions and produce futile work. I commented because I didn't understand what you meant and wanted to make sure we are speaking about the same thing. I will let Sylius guys decide on the naming, it is their domain.

Whether or not to cache the full page (defining caching strategies on the controller level) depends on the use case. If there is a lot of user information that needs to be rendered by that controller, caching it will be a challenge.

To conclude from my side, enabling ESI blocks on some parts of the page will definitely bring some speed, so go for it 👍 :)

Thank you for your opinion @ilukac. You're right about getting on the same page.

I'm also very curious about the opinion of the Sylius team, they have not responded to this after being tagged 28 days ago. @kgonella have you heard from any of them about this, since?

This issue has been automatically marked as stale because it has not had any recent activity. It will be closed in a week if no further activity occurs. Thank you for your contributions.

Do not stale ?

Was this page helpful?
0 / 5 - 0 ratings