vuejs / core

🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
https://vuejs.org/
MIT License
46.37k stars 8.14k forks source link

@vue/server-renderer doesn't have support for component-level caching #1791

Open RomyPr opened 4 years ago

RomyPr commented 4 years ago

What problem does this feature solve?

ServerCacheKey is useful feature for large projects with SSR. Will it be added to @vue/server-renderer?

What does the proposed API look like?

https://ssr.vuejs.org/guide/caching.html#component-level-caching

CyberAP commented 4 years ago

I wonder if it makes sense to support this in a form of a built-in component instead of a component option? It would then be possible to use promises to get a cache key:

<template>
  <server-cache :key="cacheKey">
    <!-- rendered on cache miss, otherwise cached stringified component is used -->
    <heavy-component v-bind="$props" />
  </server-cache>
</template>

<script>
  export default {
    async setup(props) {
      const cacheKey = ref(null);
      cacheKey.value = await CachingService.get(props.id);
      return { cacheKey }
    }
  }
</script>

It also makes caching more versatile because it's not bound to the component.

yaquawa commented 3 years ago

Hi, had any progress on this?? or any plan to implement this in the future or not? It was possible on Vue2, now how to do this on Vue3?

vertcitron commented 3 years ago

Hi everybody and @yyx990803. We need to migrate a very large scale SSR app written in Vue 2.6 / TS to Vue 3, and our Vue 2.6 makes an heavy use of component caching to ensure the best performance. In consequence we can't wait this feature to be added, and we need to try to implement it ourself. Before starting, I'd like to know the best way to achieve it with the goal to get the solution retained to be included in the library and available later to the community. There are several ways to get it done :

The second solution seems likely to me, but at this time I can't figure out how to get the rendered string in the wrapper's render function without modifying the renderToString function in the server renderer...

EDIT : to give a better understanding, here are some measures on the on the landing page of galerieslafaytette.com (a big french department store). these measures were made on the webapp deployed on a GKE kubernetes workload, with it's autoscaler setup for the test to give always only one pod. We fire it it with npx autocannon -c 40 -r 1 -t 20 the_technical_url_of_the_deployment, which means 40 simultaneous users firing each one 1 request per second during 20 seconds. The time is the first server response time (the SSR page itself) without subsequent asset loading and the rate is the number of request the pod has been able to serve per second, both averaged through the 20 seconds :

As you can see, migrating to Vue 3 has a benefit, but not enough regarding the micro-caching available on the Vue 2 server-renderer...

vertcitron commented 3 years ago

@CyberAP Your idea is very good, and I think I'll try to implement it this way, and maybe I will not have to modify the server-renderer package, if I'm able to call the renderToString of the server renderer function from the render function of this cache wrapper on the default slot content... And on Client Side, the cache wrapper component should be totally transparent...

CyberAP commented 3 years ago

I am using serverCacheKey extensively in Vue 2 and I really hope that the caching interface would support stream piping as well as returning just strings. This siginifcantly boosts app performance for streaming rendering if you're caching large html chunks and don't want to deal with unnecessary IO from copying strings back and forth.

The implementation could look like this:

const app = createApp({
  ssrCache: {
    get(key) {
      if (!canStream) return resolveItem(key) // resolves to string
      else return getItemStream(key) // resolves to ReadableStream
    },
    set(key, value) { cacheItem(key, value) },
  },
});
gokalpfirat05 commented 2 years ago

Any progress on this request? I guess there are tons of applications use this feature at Vue 2 but it's missing in Vue 3 strangely. I don't know if it's not implemented on purpose. @yyx990803

PatrikRacsko commented 2 years ago

@CyberAP Your idea is very good, and I think I'll try to implement it this way, and maybe I will not have to modify the server-renderer package, if I'm able to call the renderToString of the server renderer function from the render function of this cache wrapper on the default slot content... And on Client Side, the cache wrapper component should be totally transparent...

Hi everybody and @yyx990803. We need to migrate a very large scale SSR app written in Vue 2.6 / TS to Vue 3, and our Vue 2.6 makes an heavy use of component caching to ensure the best performance. In consequence we can't wait this feature to be added, and we need to try to implement it ourself. Before starting, I'd like to know the best way to achieve it with the goal to get the solution retained to be included in the library and available later to the community. There are several ways to get it done :

  • Reproduce the vue 2.6 server-renderer behavior, by passing the cache and its options to the renderer constructor, and implementing the serverCacheKey option. It could just be enhanced by passing the cache client we want and not only the LRU in-memory cache, which could remain useful in certain situations, but could also be for example a Redis client to share caching between different pods of a kubernetes workload for example.
  • Go to a new way of caching, implementing a wrapper component like presented above, making use of a render function which decides to use the renderer or a cached rendered string. It could be also an elegant solution because it could be implemented as a plugin, staying independant from the base libraries.

The second solution seems likely to me, but at this time I can't figure out how to get the rendered string in the wrapper's render function without modifying the renderToString function in the server renderer...

EDIT : to give a better understanding, here are some measures on the on the landing page of galerieslafaytette.com (a big french department store). these measures were made on the webapp deployed on a GKE kubernetes workload, with it's autoscaler setup for the test to give always only one pod. We fire it it with npx autocannon -c 40 -r 1 -t 20 the_technical_url_of_the_deployment, which means 40 simultaneous users firing each one 1 request per second during 20 seconds. The time is the first server response time (the SSR page itself) without subsequent asset loading and the rate is the number of request the pod has been able to serve per second, both averaged through the 20 seconds :

  • Vue 2.6 without component-level caching : 1680 ms - 7 req/s
  • Vue 2.6 with component-level caching : 260 ms - 28 req/s
  • Vue 3 without component-level caching: 1230 ms - 11 req/s

As you can see, migrating to Vue 3 has a benefit, but not enough regarding the micro-caching available on the Vue 2 server-renderer...

Any progress on any of the mentioned solutions? we are facing the same problem. The second solution is indeed a great idea!

dulnan commented 1 year ago

Like others I was also looking for a solution for component caching with Vue 3 server-renderer. I maintain nuxt-multi-cache that among other things offers a seamless integration for component caching in Nuxt 2. Currently rewriting the module for Nuxt 3 and had to find a solution for component caching.

Looked into various options but then tried out the idea suggested in one of the comments here: Using a wrapper component that caches the contents of the default slot. After some fiddling around I managed to make it work as part of the rewrite for my module.

Essentially you can use the ssrRenderSlotInner method from vue/server-renderer in your wrapper's setup method, render the slot to string and then return that string as a innerHTML child, while also caching the string. After that you can load the markup from cache.

Currently you'll need to copy paste some code from server-renderer that handles buffers. But I have created a feature request at #7414 that would export two methods so the copy pasting isn't needed anymore. In the issue you'll also find an example that shows the basic idea of this approach.

bgondy commented 1 year ago

Any update on this ?

Does component-level-caching support (with serverCacheKey) function has been dropped as of Vue3 ?

I can't find any information in documentation about this for Vue3/Nuxt3 as of today but this issue. It would be nice to get feedback from the core team about this topic.

AdnanCukur commented 11 months ago

Anyone has a solution for this ?

Oskar-Nilsen-Roos commented 9 months ago

Would also like to see a solution for this

dmytro-grablov commented 5 months ago

Not sure if this is relevant, but based on suggestion from @CyberAP I have the following working example:

<script>

import { renderToString } from '@vue/server-renderer';
import { h, createStaticVNode, getCurrentInstance, createApp } from 'vue';

// Implement your own cache service, pass to the component using provide/inject etc.
const dummyCache = {};

export default {
  name: 'ServerCache',
  props: {
    cacheKey: {
      type: String,
      required: true,
    },
    cacheName: {
      type: [ String, Symbol ],
      required: true,
    },
  },
  computed: {
    cacheEntryKey() {
      return `${this.cacheName}-${this.cacheKey}`;
    }
  },
  async beforeCreate() {
    const isClient = typeof window !== 'undefined';

    if (!this.$slots.default || isClient || typeof dummyCache[this.cacheEntryKey] !== 'undefined') {
      return;
    }
    const vNode = h(this.$slots.default);

    /*
     * wrap vNode into a temporary app and assign original context
     * so that children have access to provides, plugins, etc.
     */
    const tempApp = createApp({ render: () => vNode })
    tempApp._context = getCurrentInstance().appContext;

    const renderedString = await renderToString(tempApp);
    dummyCache[this.cacheEntryKey] = renderedString;
  },

  render() {
    const cachedEntry = dummyCache[this.cacheEntryKey];
    if (typeof cachedEntry !== 'undefined') {
      return createStaticVNode(cachedEntry)
    }
    if (this.$slots.default) {
      return h(this.$slots.default);
    }
    return null;
  },
};
</script>

Then use as

<server-cache cache-name="page-navi" :cache-key="heavyPropsAsString">
    <heavy-component v-bind="heavyProps"></heavy-component>
</server-cache>

So far the prototype worked for me.

Yes, it is using vue internals, but only within itself. And it is not messing with main app instance and hooks. If anything changes you just have to update the implementation of this one component. Or if it broke completely due to change in vue internal APIs, simply remove cache wrapper, yes your app will become slow again, but it won't break.

CyberAP commented 5 months ago

@dmytro-grablov nice implementation. I think you don't even need to create a new app instance for this. We can re-use the internal method.

export default {
  created() {
      let res = '';
      const push = (val) => { res += val };
      this.$.type.ssrRender(this.$.proxy, push, this.$.parent, this.$.attrs);
      console.log(res); // rendered markup
  }
}

Of course this would need some additional handling in case you have an async child.

dmytro-grablov commented 5 months ago

hm, I don't have ssrRender on my component. Is it coming from Nuxt? We do not use Nuxt currently or otherwise I do not know how to access it