DEV Community

Cover image for 🧠 A New React and the Old Cache
Anton Korzunov
Anton Korzunov

Posted on • Updated on

🧠 A New React and the Old Cache

Is React 🚀 FAST, or React is 🐌 SLOW? It's super easy to answer this question, just a no brainer.

  • React is fast. Full stop. React is fast when it could be fast: not displaying too much data, and not animating something in 60 frames per second. There is always a faster kid on the block(Svelte?), but React is fast enough almost for any case.
  • React is slow. Another full stop. It's not a big deal to make it slow - inline 1000 SVGs and use React to render them all, or render 100 pages simultaneously on a Server Side.

In short - React is 👍 as a Client Side Library, but for Server Side Rendering - oh, it will 🔥 burn your CPU, and AWS bill. Not because it slow, but because everything has limits.

That's obvious - you cannot compare situations when ALL CPUs are working for a ONE customer sake (in browser), and the situating, when you have to render the same, but for a dozen clients simultaneously.

That's also obvious how to mitigate the problem - it was know for generation, and for react itself it’s the main idea around handling unneeded updates - memoization. Or, in terms of old school SSR - just cache.
Cache for small blocks, cache for big blocks, cache for pages, data and intermediate results. Cache as much as possible, and let render results of a one customer speed up render for another customer.

Caching is a well-known pattern, and always was saving a day! For decades! For everyone! Except for React.

React days

So, caching in React? Do you remember an API for it? No, you don’t. There isn’t any.

You still able to cache renderToString result - not a big deal, or cache renderToNodeStream as mxstbr did for Spectrum last year.

But do we need caching?

Yes - React 15 was REALLY SLOW, and React 16, even if it is much faster, still require some time to render the result.
Anything requires some time. Everything has requirements and limits.

Caching in React

As long as the problem existed, there were many devs, with a caching background(from previous experiences in different frameworks or languages) to solve it - new patterns and libraries emerged.

Rapscallion

Alternative to React-DOM. Our hero. Saved millions of CPU cycles. Was able to handle data loading on the component level, and caching the result as well.

Worked perfectly. Till React 16. Not worked after.

GitHub logo FormidableLabs / rapscallion

Asynchronous React VirtualDOM renderer for SSR.

Rapscallion

CircleCI Join the chat at https://gitter.im/FormidableLabs/rapscallion

Overview

Rapscallion is a React VirtualDOM renderer for the server. Its notable features are as follows:

  • Rendering is asynchronous and non-blocking.
  • Rapscallion is roughly 30% faster than renderToString.
  • It provides a streaming interface so that you can start sending content to the client immediately.
  • It provides a 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.

Table of Contents

Installation

Using npm:

$ npm install --save rapscallion

In Node.js:

const {
  render,
  template
} = require("rapscallion");
// ...

API

render

render(VirtualDomNode) -> Renderer

This function returns a Renderer, an interface for rendering your VirtualDOM element. Methods are enumerated below.


Renderer#toPromise

Another great tool for Component Level caching. Another tool which hacks into React and trying change the way it works. Yet another tool which does not works​ with React 16.

GitHub logo electrode-io / electrode-react-ssr-caching

Optimize React SSR with profiling and component caching

electrode-react-ssr-caching NPM version Build Status Dependency Status

Support profiling React Server Side Rendering time and component caching to help you speed up SSR.

Installing

npm i electrode-react-ssr-caching

Usage

Note that since this module patches React's source code to inject the caching logic, it must be loaded before the React module.

For example:

import SSRCaching from "electrode-react-ssr-caching";
import React from 'react';
import ReactDOM from 'react-dom/server';

Profiling

You can use this module to inspect the time each component took to render.

import SSRCaching from "electrode-react-ssr-caching";
import { renderToString } from "react-dom/server";
import MyComponent from "mycomponent";
// First you should render your component in a loop to prime the JS engine (i.e: V8 for NodeJS)
for( let i = 0; i < 10; i ++ ) {
    renderToString(<MyComponent />);
}
SSRCaching.clearProfileData();
SSRCaching.enableProfiling();
const html

react-ssr-optimization

A magic system for memoization and templatenization from Walmart Labs. Was once broken during React 14-15 migration, and didn't get React 16 support.

GitHub logo walmartlabs / react-ssr-optimization

React.js server-side rendering optimization with component memoization and templatization

React Server-Side Rendering Optimization Library

This React Server-side optimization library is a configurable ReactJS extension for memoizing react component markup on the server. It also supports component templatization to further caching of rendered markup with more dynamic data. This server-side module intercepts React's instantiateReactComponent module by using a require() hook and avoids forking React.

Build Status version License

Why we built it

React is a best-of-breed UI component framework allowing us to build higher level components that can be shared and reused across pages and apps. React's Virtual DOM offers an excellent development experience, freeing us up from having to manage subtle DOM changes. Most importantly, React offers us a great out-of-the-box isomorphic/universal JavaScript solution. React's renderToString(..) can fully render the HTML markup of a page to a string on the server. This is especially important for initial page load performance (particularly for mobile users with low bandwidth) and search engine indexing and ranking…

React-component-caching

TLDR: Actually a working one!

GitHub logo rookLab / react-component-caching

Speedier server-side rendering with component caching in React 16

React Component Caching

Overview

React Component Caching is a component-level caching library for faster server-side rendering with React 16.

  • Use any of React's four server-side rendering methods. Rendering is asynchronous.
  • Cache components using a simple or template strategy.
  • Choose from three cache implementations (LRU, Redis, or Memcached).

Installation

Using npm:

$ npm install react-component-caching

Usage

In Node rendering server:

Instantiate a cache and pass it to any rendering method (renderToString, renderToStaticMarkup, renderToNodeStream, or renderToStaticNodeStream) as a second argument. Wherever you would use ReactDOM.renderToString, use ReactCC.renderToString.

Note: All of these methods are asynchronous, and return a promise. To use them, await the response before rendering

const ReactCC = require("react-component-caching");
const cache = new ReactCC.ComponentCache();
app.get('/example', async (req,res) => {
    const renderString = await ReactCC.

Citing Speedier Server-Side Rendering in React 16 with Component Caching

Sasha Aickin introduced and demoed component caching in a 2016 talk on ways to speed up server-side rendering. Soon afterwards, Walmart Labs created a component caching library with a profiling feature to time renders. The idea received attention at React Amsterdam last year, and it has surfaced in discussions on Github. A component caching API was also incorporated in Formidable’s React server renderer, Rapscallion.
With the release of React 16 last September, many of these efforts have been due for a reboot. React has improved the speed of its server-side rendering methods and now offers two streaming methods. We were curious if it was possible to harness the power of component caching with these new and improved methods. So we built React Component Caching to give developers the ability to do just that.


But, you know, this library is quite complex inside, and would be broken on next React release - 100%. And user-facing API is also quite complex... But it's working, working with memcache or other caching libraries and was a single saviour... until today.

React-Prerendered-Component

Then - let me introduce another approach to a component level caching in React, which will never be broken. Never by design.

import {CachedLocation, NotCacheable} from 'react-prerendred-component';

const MyCachedComponent = () => (
  <CachedLocation cacheKey="MyCachedComponent">
    any code you want
  </CachedLocation>
);

// component like this shall not be cached.
const SomeNonPureImportantComponent = () => (
 <NotCacheable>hey {global.userName}</NotCacheable>
);

Simple? Easy? How does it work?

How it works

  1. CachedLocation will wrap your component with <x-cached-store>{children}<x-cached-store>
  2. result of renderToString would be analyzed for such markers, and marker's content would be moved into the cache. For renderToStream the same is working via transform streams.
  3. if key was present in the store, then CachedLocation would render <x-cached-restore-key />, and the same post-filter would replace this text by the stored value.

This works on HTML/stream transformation level, and don't rely on ANY React internals.

The same is working for Client Side! CachedLocation will just wrap your data with a div and store/restore data using dangerouslySetInnerHTML. For example, you might cache those big SVGs, I was mentioning in the beginning.

<CachedLocation cacheKey="svg-fileName">
  // react component first time, just a string second.
  <MySVRGComponent />
</CachedLocation>

renderToString(...);

// - first render, cache is empty, making a full render
// <x-cached-store-1><svg..../></x-cached-store-1>
// - cache stored, extra tags removed
// <svg .../> 

renderToString(...);

// - second render, cache exists - restore entity
// <x-cached-restore-1/>
// - we know what to restore
// <svg ...

While CachedLocation is adding extra tags during the render - they would be removed later. It's a transparent process!

It is all pure HTML based solution, which works after React did the job.

XSS my friend?

So - let's recall it once again - CachedLocation will put some tag in result HTML and some dummy(it is) RegEx will do the rest.

What if your dear customer will do the same? What if the same tag would be found in JSON, script and visa versa? Of course, it will blow :P

So - in the real world this library would not use so obvious tag names, but will use nanoid to generate unique tag names every render. So - no XSS and no false positives are possible, relax.

<x-cached-supersecretstring-11 /> // it's "safe"

The only, hm, problem here - now caches, generated in different threads, are not compatible with each other, and so in case of distributed cache you have to specify super-secret-seed by your own.

Templatezation

It's a long, and probably not quite a correct, term stand for less variative memoization.

For example - header of dev.to contains your avatar, but everything else is the same - you may "template it" into the static and cached <Header/>. What would happen if you will cache header in a per-user basis? Well, cache is not infinite.

Picture from Scaling React Server-Side Rendering.

Instead of having 100500 different <Button>unuqieText</Button> you might just have <Button>#text#</Button> - so you will memoize only complex html markup, adding some "variables" using simple replace methods.

While using string replaces is not quite safe, this library gives you a few components to inject Placeholders, which uses Context API to drill down the props, and requires just a small code style changes to be used.

const UserHeader = () => (
 <div>
   .....
   <Placeholder name="userName"/>
   ....
 </div>
)

renderToString(<CacheLocation variables={{userName:'Joe'}}><UserHeader/></CacheLocation>);

// <x-cached-store-1 userName="Joe">....{x-cached-placeholder-userName}...</x-cached-store-1>

renderToString(<CacheLocation variables={{userName:'Jack'}}><UserHeader/></CacheLocation>);

// <x-cached-restore-1 userName="Jack"/>
// ....{x-cached-placeholder-userName}...
// ....Jack...

This is a very powerful tactic to reduce memory usage and skyrocket your application via transforming your brand new React App to old school template engine.

Which is just 🚀VERY🚀FAST🚀.

PS: This could give you sub-millisecond SSR, react-component-caching have proved it.
PPS: But this is not working yet for the client-side cache. Using variables for CSR caching will disable caching.

Another name of this technique is cache interpolation, and if you want to TRULY understand everything behind fragment caching - read The Article.

Where is the cache?

Another cool moment this approach is a cache. A simple React 16/Suspense compatible cache model.

  • synchronously get the cache
  • synchronously set the cache.
  • No cache available? - Throw a promise!

You cannon use react-cache, as long as it utilizes a different model (there is no set), but any other cache, including in-memory or shared-memory ones - easy.

But not memcache, as long as asynchronius cache is not supported.

Prerendered component gives you the power of component memoization, and templatization both for Server and Client, and would not break in the future, just because it built that way - to never let you down.

Give it a try.
Or just think about it.

GitHub logo theKashey / react-prerendered-component

🤔Partial hydration and caching in a pre-suspense era

React Prerendered Component


Partial Hydration and Component-Level Caching

Idea

In short: dont try to run js code, and produce a react tree matching pre-rendered one but use pre-rendered html until js code will be ready to replace it. Make it live.

What else could be done on HTML level? Caching, templatization, and other good things to 🚀, just in a 3kb*.

Prerendered component

Render something on server, and use it as HTML on the client

  • Server side render data
    • call thisIsServer somewhere, to setup enviroment.
    • React-prerendered-component will leave trails, wrapping each block with div with known id.
  • Hydrate the client side
    • React-prerendered-component will search for known ids, and read rendered HTML back from a page.
  • You site is ready
    • React-prerendered-components are ready. They are rendering a pre-existing HTML you send from a server.
  • Once any component ready to be replaced - hydrate
    • But not before. That's the…

PS: By the way, react-prerendered-component was a hero for other article:

Top comments (6)

Collapse
 
gohulk profile image
Aravind Kamarajugadda

Hello Anton , Very interesting article focusing on Cache. Have you checked Apollo Client to cache on client side. if you are using client side rendering then you can use Apollo client side cache along with graphQL.

Collapse
 
thekashey profile image
Anton Korzunov

That’s apples and oranges.
Apollo cache is about caching data, and then using it to render a view.
This solution is about caching view, but not the data.
You may mix them together, and, get data and view caching simultaneously.
Even more - view layer caching will cut off all nested graphQL connectors and, yeah, 🦄🎉🔥.

TLDR - no, Apollo cache has nothing with this one.

Collapse
 
gohulk profile image
Aravind Kamarajugadda

Thanks for correcting on this. How does caching view works? Is this more specific to server side rendering? I have no knowledge about server side rendering and never came across caching view on client side implementation. All my implementations are using reactJS and GatsbyJS to render on client side.

Thread Thread
 
thekashey profile image
Anton Korzunov

That would take a while, but read Scaling React Server-Side Rendering/Component Caching.

Collapse
 
revskill10 profile image
Truong Hoang Dung

As the old quote said: "There're two things that are hard in Computer Science, it's naming and cache invalidation".

Caching is not hard.
Invalidate cache is hard.

Collapse
 
thekashey profile image
Anton Korzunov

Not only invalidation. Let imagine - you don't have a cached value, so you have generate it. Obviously, that decision would be made in 10 threads at 10 servers simultaneously :)

Cache invalidation is not a real problem - cache management, oh dear, is.