React: Add hooks to ReactDOMServer to support caching

Created on 27 Nov 2017  路  8Comments  路  Source: facebook/react

Do you want to request a feature or report a bug?
feature

What is the current behavior?
react-dom SSR performance could be improved using server-side cache, but currently the
ReactPartialRenderer is currently not accessible from the react-dom package.

Desired behavior
On the server only, it would be nice if _plugins_ could be used to improve render performance. Currently the only way to do this would be to externally maintain a renderer implementation. However, the ReactPartialRenderer already contains all the behavior required to support plugins - with a little refactoring.

I've refactored the ReactPartialRenderer and created a proof of concept for supporting _plugins_ for react server side rendering. You can see the ReactPartialPluginRenderer in this fork, its comprised of 3 different commits:

  • #1: Strict refactoring of ReactPartialRenderer, the only addition is exporting the ReactPartialRenderer from the react-dom server package
  • #2: Create the ReactPartialPluginRenderer by extending the refactored ReactPluginRenderer, and introduce a plugin interface
  • #3: Proof of concept plugin implementations and application example.

You can view instructions for running the example in the repo.

I understand that exporting ReactPartialRenderer exposes the internal API, which is far from ideal. Is there any scenario in which ReactPartialRenderer would be made to be accessible from the react-dom package? Or would a plugin implementation similar to above be required to maintain its own forked ReactPartialRenderer? Maintaining the plugin renderer in its own repo isn't a problem, but It would be great if plugins could be used without needing to maintain the core server renderer.

Thanks, Adam.

Server Rendering Feature Request

Most helpful comment

Here's a first draft of a react SSR cache hooks API.

API design goals:
  1. SSR caching is performed at the frame level (of the ReactPartialRenderer), where each frame represents a react element

    • only complete/closed frames can be cached (this greatly simplifies the SSR cache API)
  2. SSR caching is performed by a CacheStrategy implementation

    • cache strategies are passed as options to renderToString() and renderToStream()
  3. Cache strategies should be capable using either react components or configuration to enable caching.

  4. Beyond supporting simple per-component caching, a cache strategy _should be able to support_ templates, where a component can be rendered to become a (cached) template, and templates can then have content injected - this has the potential to _drastically_ improve SSR performance.

CacheStrategy API

interface CacheStrategy {

  /**
   * Gets the cache strategy state for a component.
   *
   * ReactPartialRenderer hook: resolveElement (called during every resolveElement invocation)
   *
   * @param component
   * @param props
   * @param context
   * @returns {*} if undefined is returned, the cache strategy render method will not be invoked for this component.
   */
  getCacheState(component: ReactNode, props: Object, context: Object): any,

  /**
   * Renders an element using a cache strategy.
   *
   * ReactPartialRenderer hook: renderFrame (called when rendering a frame that has assigned 'cacheState')
   *
   * @param element to render
   * @param context to use for rendering
   * @param cacheState the state returned by the getCacheState() method
   * @param renderUtils to simplify rendering of cached component
   * @returns {string} the rendered component
   */
  render(element: ReactElement, context: Object, cacheState: mixed, renderUtils: CacheRenderUtils): string,
}

Cache Strategy Notes

#getCacheState()
  • Determines if a component supports caching, and returns component specific cache state
  • Hook must be called by the ReactPartialRenderer every time an element is resolved
  • Returning undefined indicates that the component does not support caching
#render()
  • handles rendering for a component that supports caching
  • Hook must be called by ReactPartialRenderer when rendering a frame that has cacheState
  • receives the cacheState returned by getCacheState()
  • receives renderUtils, utility methods for rendering cached components that abstracts renderer internals

Template example

<Header avatar={UserAvatar} />

The Header can be rendered as a template that allows the avatar prop to be injected each render. This prevents the need to render the Header on each request.

The same approach could be applied to more complex components, such as a <Product ... /> or <Comments .../> component, greatly reducing SSR time.

RenderUtils can be used to simplify template support.

CacheRendererUtils

// Utility methods for rendering that abstract renderer internals
type CacheRenderUtils = {

  /**
   * Renders the current frame element and all its children, allowing props to be overridden.
   *
   * @param props
   * @param context
   * @returns {string} the rendered element output
   */
  renderCurrentElement: (props?: Object, context?: Object) => string,

  /**
   * Renders the provided element and all its children.
   *
   * @param element
   * @param context
   * @param domNamespace
   * @returns {string} the rendered element output
   */
  renderElement: (element: ReactElement, context?: Object, domNamespace?: string) => string,

  /**
   * Logs a warning if the base context is modified during the provided render function.
   *
   * NOTE: This only logs warning messages in development.
   *
   * @param baseContext the expected context throughout the entire render method
   * @param render method
   * @param [messageSuffix] {string} the message to log
   * @returns {string} the render output
   */
  warnIfRenderModifiesContext: (baseContext: Object, render: (ctx: Object) => string, messageSuffix?: string) => string,
};

Render Util Notes

#renderCurrentElement()
  • renders the current element, allowing props and context to be modified
#renderElement()
  • renders the provided element
  • allows arbitrary elements to be rendered by cache strategies
  • this enables cache strategies to be created that supports injecting element(s) into cached content (aka, a template)

    • this method can be used to render the element(s) that will be injected into a template

#warnIfRenderModifiesContext()
  • enables a cache strategy to determine if the context is modified while rendering an element (and all its children)
  • this is useful for a cache strategy that supports injecting element(s) into cached content (aka, a template)

    • if the context is changed when rendering a template, injected elements may not render consistently

      on the server and client

    • this method can be used to wrap renderCurrentElement or renderElement and log a warning if rendering on server/client are at risk of being inconsistent.

Hook usage example

import { renderToString } from 'react-dom/server';

// No cache strategies
renderToString(<App ... />);

// Using cache strategy
renderToString(<App ... />, { cacheStrategy: new ExampleCacheStrategy() });

I've created a proof of concept you can view in this forked branch. It includes an example CacheStrategy implementation and basic app in the SSR plugins fixture.

The proof of concept app includes 3 approaches to caching:

  1. Using a static getCacheKey = (props) => 'key'; method on any component
  2. Using a <Cache ...> component
  3. Using a <CacheTemplate> component to create templates from components that support injecting content into the cached template.

I'm sure you'll have many questions, let me know what you think.

Adam.

All 8 comments

I don't think we intend to support any sort of extensible plugin architecture. But we might support a limited set of hooks necessary e.g. for caching. Can you propose a specific API for those?

Here's a first draft of a react SSR cache hooks API.

API design goals:
  1. SSR caching is performed at the frame level (of the ReactPartialRenderer), where each frame represents a react element

    • only complete/closed frames can be cached (this greatly simplifies the SSR cache API)
  2. SSR caching is performed by a CacheStrategy implementation

    • cache strategies are passed as options to renderToString() and renderToStream()
  3. Cache strategies should be capable using either react components or configuration to enable caching.

  4. Beyond supporting simple per-component caching, a cache strategy _should be able to support_ templates, where a component can be rendered to become a (cached) template, and templates can then have content injected - this has the potential to _drastically_ improve SSR performance.

CacheStrategy API

interface CacheStrategy {

  /**
   * Gets the cache strategy state for a component.
   *
   * ReactPartialRenderer hook: resolveElement (called during every resolveElement invocation)
   *
   * @param component
   * @param props
   * @param context
   * @returns {*} if undefined is returned, the cache strategy render method will not be invoked for this component.
   */
  getCacheState(component: ReactNode, props: Object, context: Object): any,

  /**
   * Renders an element using a cache strategy.
   *
   * ReactPartialRenderer hook: renderFrame (called when rendering a frame that has assigned 'cacheState')
   *
   * @param element to render
   * @param context to use for rendering
   * @param cacheState the state returned by the getCacheState() method
   * @param renderUtils to simplify rendering of cached component
   * @returns {string} the rendered component
   */
  render(element: ReactElement, context: Object, cacheState: mixed, renderUtils: CacheRenderUtils): string,
}

Cache Strategy Notes

#getCacheState()
  • Determines if a component supports caching, and returns component specific cache state
  • Hook must be called by the ReactPartialRenderer every time an element is resolved
  • Returning undefined indicates that the component does not support caching
#render()
  • handles rendering for a component that supports caching
  • Hook must be called by ReactPartialRenderer when rendering a frame that has cacheState
  • receives the cacheState returned by getCacheState()
  • receives renderUtils, utility methods for rendering cached components that abstracts renderer internals

Template example

<Header avatar={UserAvatar} />

The Header can be rendered as a template that allows the avatar prop to be injected each render. This prevents the need to render the Header on each request.

The same approach could be applied to more complex components, such as a <Product ... /> or <Comments .../> component, greatly reducing SSR time.

RenderUtils can be used to simplify template support.

CacheRendererUtils

// Utility methods for rendering that abstract renderer internals
type CacheRenderUtils = {

  /**
   * Renders the current frame element and all its children, allowing props to be overridden.
   *
   * @param props
   * @param context
   * @returns {string} the rendered element output
   */
  renderCurrentElement: (props?: Object, context?: Object) => string,

  /**
   * Renders the provided element and all its children.
   *
   * @param element
   * @param context
   * @param domNamespace
   * @returns {string} the rendered element output
   */
  renderElement: (element: ReactElement, context?: Object, domNamespace?: string) => string,

  /**
   * Logs a warning if the base context is modified during the provided render function.
   *
   * NOTE: This only logs warning messages in development.
   *
   * @param baseContext the expected context throughout the entire render method
   * @param render method
   * @param [messageSuffix] {string} the message to log
   * @returns {string} the render output
   */
  warnIfRenderModifiesContext: (baseContext: Object, render: (ctx: Object) => string, messageSuffix?: string) => string,
};

Render Util Notes

#renderCurrentElement()
  • renders the current element, allowing props and context to be modified
#renderElement()
  • renders the provided element
  • allows arbitrary elements to be rendered by cache strategies
  • this enables cache strategies to be created that supports injecting element(s) into cached content (aka, a template)

    • this method can be used to render the element(s) that will be injected into a template

#warnIfRenderModifiesContext()
  • enables a cache strategy to determine if the context is modified while rendering an element (and all its children)
  • this is useful for a cache strategy that supports injecting element(s) into cached content (aka, a template)

    • if the context is changed when rendering a template, injected elements may not render consistently

      on the server and client

    • this method can be used to wrap renderCurrentElement or renderElement and log a warning if rendering on server/client are at risk of being inconsistent.

Hook usage example

import { renderToString } from 'react-dom/server';

// No cache strategies
renderToString(<App ... />);

// Using cache strategy
renderToString(<App ... />, { cacheStrategy: new ExampleCacheStrategy() });

I've created a proof of concept you can view in this forked branch. It includes an example CacheStrategy implementation and basic app in the SSR plugins fixture.

The proof of concept app includes 3 approaches to caching:

  1. Using a static getCacheKey = (props) => 'key'; method on any component
  2. Using a <Cache ...> component
  3. Using a <CacheTemplate> component to create templates from components that support injecting content into the cached template.

I'm sure you'll have many questions, let me know what you think.

Adam.

The Header can be rendered as a template that allows the avatar prop to be injected each render. This prevents the need to render the Header on each request.

I am not sure I understand what you mean, could you describe this more detailed? Thanks!

@NE-SmallTown, here it is in more detail. Just to be clear, the template is an example of a cache strategy implementation - it's not part of the cache hooks API, it is simply an example of what's possible using the cache hooks API.

Using the header/avatar example, we convert this:

<Header avatar={UserAvatar} />

To a cached template using a cache strategy implementation that supports Templates.
Here is what an implementation might look like:

<Template 
  cacheKey={()=>'header'} 
  component={Header} 
  templateProps={ /* passed to component, will be cached as part of the template*/ } 
  injectedProps={ avatar: Avatar /* passed to component, not cached */ } />

On the first render (for a given cache key), the component is rendered to a template (where a template is a tokenized string) using a Placeholder that replaces each of the _injectedProps_ (you can see an example of this here). The Placeholder acts as a token that is replaced by injected content for future renders utilizing the cached template.

The template output is cached and used to render all future Header components (with the same cache key), and the Placeholders in the template are replaced with the rendered output of the _injectedProps_. This prevents the Header from being processed with lifecycle methods on the server - while still being able to accept props that change on each render (as a side note, the _injectedProps_ could also use cache strategies for rendering).

This minimizes the CPU bound activity of a server render and reduces server render time. In the proof of concept linked above the render time is reduced > 60% (conservative measure of very basic comparisons) using the template approach for a component that is comprised of a small number of child components, as the number of the Template _component_ children increase the time saved should be even greater - instead of rendering the component a cached string is being used.

For this to work consistently for each render, its important that no _middle components_ (components between the Template _component_ and any _injectedProps_) change the context being used to render the template. If the context is not changed, then all template renders will be consistent (The Placeholder can be used to ensure no additional props are applied to any _injectedProps_ within the template _component_). This is why the warnIfRenderModifiesContext method is defined in the CacheRenderUtils above, the method is needed to guarantee template renders are consistent.

This could be taken even further, and cached templates could be written to a cache server (mem-cache, redis, etc..) and re-used across server instances.

Hope that's clear. Let me know if you need any more info.

@adam-26 Thanks for your clarification!

IMO, from your code comments:

// This component uses a component to cache the content of that component.
// - templates allow content to be injected, which is useful if a complex component is mostly static
// for many requests but requires small amount(s) of dynamic content for each individual request.

This is what I think about, does it means this plugin just apply to stateless/functional component? I mean for the complex/huge logic component(i.e. include many methods/events or hocs), we can't use the cache plugin and also should not use it.

On the other hand, even for the simple component, we also need to distinguish carefully the templateProps and injectedProps, or it will don't work as we expected.

By the way, I think I need some time to understand your comment and example :) , and possibly need to clone and run it. But anyway, thanks~

@NE-SmallTown, in response to your question

This is what I think about, does it means this plugin just apply to stateless/functional component? I mean for the complex/huge logic component(i.e. include many methods/events or hocs), we can't use the cache plugin and also should not use it.

It's easier to use the plugin with stateless/functional components, but the cache plugin can be used with complex components. However any side-effects need to be managed. For example, if the component creates/populates any application state (such as redux store data) then that application state would also need to be cached for future renders.

On the other hand, even for the simple component, we also need to distinguish carefully the templateProps and injectedProps, or it will don't work as we expected.

Yes, this is true - but caching generally requires some type of configuration.

Could you please turn this into an RFC?
https://github.com/reactjs/rfcs

I鈥檒l close this issue because we鈥檙e trying to funnel all specific API proposals to the RFC repo. Please open one there if you鈥檙e interested! Thanks.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

krave1986 picture krave1986  路  3Comments

zpao picture zpao  路  3Comments

jimfb picture jimfb  路  3Comments

zpao picture zpao  路  3Comments

trusktr picture trusktr  路  3Comments