Rapscallion
Overview
Rapscallion is a React VirtualDOM renderer for the server. Its notable features are as follows:
- Rendering is asynchronous and non-blocking.
- With no concurrent renders, Rapscallion is roughly the same speed as React's
renderToString
. - With 10 concurrent renders, Rapscallion is roughly twice the speed of
renderToString
. - It provides a streaming interface so that you can start sending content to the client immediately.
- It provides a stream templating feature, so that you can wrap your component's HTML in boilerplate without giving up benefits of streaming.
- It provides a component caching API to further speed-up your rendering.
Big thanks to @briancavalier for his fantastic Most.js library!
Table of Contents
Installation
Using npm:
$ npm install --save rapscallion
In Node.js:
const {
renderToStream,
renderToString,
toNodeStream,
streamTemplate,
tuneAsynchronicity
} = require("rapscallion");
// ...
API
renderToString
renderToString(VirtualDomNode) -> Promise<String>
This function evaluates a React VirtualDOM Element, and returns a Promise that resolves to the component's evaluated HTML string.
Example:
renderToString(<MyComponent {...props} />)
.then(htmlString => console.log(htmlString));
renderToStream
renderToStream(VirtualDomNode) -> MostStream<StringSegment>
This function evalues a React VirtualDOM Element, and returns a Most.js stream. This stream will emit string segments of HTML as the DOM tree is asynchronously traversed and evaluated.
Example:
const componentStream = renderToStream(<MyComponent prop="stuff" />);
componentStream.observe(segment => process.stdout.write(segment));
toNodeStream
toNodeStream(MostStream<StringSegment>) -> NodeStream<StringSegment>
This function translates Most.js streams to Node streams. You'll be able to pipe the output of these Node streams to an HTTP Response object or to disk.
Example:
app.get('/example', function(req, res){
const componentHtmlStream = renderToStream(<MyComponent />);
toNodeStream(componentHtmlStream).pipe(res);
});
streamTemplate
streamTemplate`TEMPLATE LITERAL` -> MostStream<StringSegment>
See the section below for usage instructions.
tuneAsynchronicity
tuneAsynchronicity(PositiveInteger)
Rapscallion allows you to tune the asynchronicity of your renders. By default, rapscallion batches events in your stream of HTML segments. These batches are processed in a synchronous-like way. This gives you the benefits of asynchronous rendering without losing too much synchronous rendering performance.
The default value is 100
and equates to the approximate speed-performance of React's renderToString
. With this value, Rapscallion takes about the same amount of time as React to render a VirtualDOM tree.
However, you may want to change this number if your server is under heavy load. Possible values are the set of all positive integers. Lower numbers will be "more asynchronous" (shorter periods between I/O processing) and higher numbers will be "more synchronous" (higher performance).
Caching
Caching is performed on a per-component level, is completely opt-in, and should be used judiciously. The gist is this: you define a cacheKey
prop on your component, and that component will only be rendered once for that particular key. cacheKey
can be set on both React components and html React elements.
If you cache components that change often, this will result in slower performance. But if you're careful to cache only those components for which 1) a cacheKey
is easy to compute, and 2) will have a small set of keys (i.e. the props don't change often), you can see considerable performance improvements.
Example:
const Child = ({ val }) => (
<div>
ComponentA
</div>
);
const Parent = ({ toVal }) => (
<div cacheKey={ `Parent:${toVal}` }>
{
_.range(toVal).map(val => (
<Child cacheKey={ `Child:${val}` } key={val} />
))
}
</div>
);
Promise.resolve()
// The first render will take the expected duration.
.then(() => renderToString(<Parent toVal={5} />))
// The second render will be much faster, due to multiple cache hits.
.then(() => renderToString(<Parent toVal={6} />))
// The third render will be near-instantaneous, due to a top-level cache hit.
.then(() => renderToString(<Parent toVal={6} />));
Stream templates
With React's default renderToString
, it is a common pattern to define a function that takes the rendered output and inserts it into some HTML boilerplate; <html>
tags and the like.
Rapscallion allows you to stream the rendered content of your components as they are generated. However, this makes it somewhat less simple to wrap that component in your HTML boilerplate.
Fortunately, Rapscallion provides stream templates. They look very similar to normal template strings, with a couple of exceptions.
- You add
streamTemplate
as a template literal tag. - Using template literals' expression interpolation, you can insert streams into the template.
- You can also insert functions into the template that will be evaluated after the previous content has been streamed.
The return value is a Most.js stream.
That may not be clear in the abstract, so here's an example:
import { renderToStream, streamTemplate, toNodeStream } from "rapscallion";
// ...
app.get('/example', function(req, res){
// ...
const store = createStore(/* ... */);
const componentHtmlStream = renderToStream(<MyComponent store={store} />);
const responseStream = streamTemplate`
<html>
<body>
${componentHtmlStream}
<script>
window._initialState = ${() => JSON.stringify(store.getState())};
</script>
</body>
</html>
`;
toNodeStream(responseStream).pipe(res);
});
Note that the template includes both a stream of HTML text (componentHtmlStream
) and a function that evaluates to the store's state - something you'll often want to do with SSR.
Benchmarks
The below benchmarks do not represent a typical use-case. Instead, they represent the absolute best case scenario for component caching - up to 2,100x faster!
However, you'll note that even without caching, a concurrent workload will be processed in roughly half the time of React, without any of the blocking!
Starting benchmark for 10 concurrent render operations...
renderToString took 6.595226639 seconds.
rapscallion, no caching took 3.490436779 seconds.
rapscallion, caching DIVs took 0.814969248 seconds.
rapscallion, caching DIVs (second time) took 0.002369651 seconds.
rapscallion, caching Components took 0.123907298 seconds.
rapscallion, caching Components (second time) took 0.003139111 seconds.