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:
ReactPartialRenderer
, the only addition is exporting the ReactPartialRenderer
from the react-dom server packageReactPartialPluginRenderer
by extending the refactored ReactPluginRenderer
, and introduce a plugin interfaceYou 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.
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.
SSR caching is performed at the frame
level (of the ReactPartialRenderer), where each frame represents a react element
SSR caching is performed by a CacheStrategy
implementation
options
to renderToString()
and renderToStream()
Cache strategies should be capable using either react components or configuration to enable caching.
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.
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,
}
ReactPartialRenderer
every time an element is resolvedundefined
indicates that the component does not support caching ReactPartialRenderer
when rendering a frame that has cacheState
cacheState
returned by getCacheState()
renderUtils
, utility methods for rendering cached components that abstracts renderer internals<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.
// 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,
};
context
is modified while rendering an element (and all its children)renderCurrentElement
or renderElement
and log a warning if rendering on server/client are at risk of being inconsistent.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:
static getCacheKey = (props) => 'key';
method on any component<Cache ...>
component<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 Template
s.
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 Placeholder
s 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.
Most helpful comment
Here's a first draft of a react SSR cache hooks API.
API design goals:
SSR caching is performed at the
frame
level (of the ReactPartialRenderer), where each frame represents a react elementSSR caching is performed by a
CacheStrategy
implementationoptions
torenderToString()
andrenderToStream()
Cache strategies should be capable using either react components or configuration to enable caching.
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
Cache Strategy Notes
#getCacheState()
ReactPartialRenderer
every time an element is resolvedundefined
indicates that the component does not support caching#render()
ReactPartialRenderer
when rendering a frame that hascacheState
cacheState
returned bygetCacheState()
renderUtils
, utility methods for rendering cached components that abstracts renderer internalsTemplate example
The
Header
can be rendered as a template that allows theavatar
prop to be injected each render. This prevents the need to render theHeader
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 simplifytemplate
support.CacheRendererUtils
Render Util Notes
#renderCurrentElement()
#renderElement()
#warnIfRenderModifiesContext()
context
is modified while rendering an element (and all its children)on the server and client
renderCurrentElement
orrenderElement
and log a warning if rendering on server/client are at risk of being inconsistent.Hook usage example
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:
static getCacheKey = (props) => 'key';
method on any component<Cache ...>
component<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.