Git Product home page Git Product logo

react-esi's Introduction

React ESI: Blazing-fast Server-Side Rendering for React and Next.js

CI status Coverage Status npm version MIT Licence

React ESI is a super powerful cache library for vanilla React and Next.js applications, that can make highly dynamic applications as fast as static sites.

It provides a straightforward way to boost your application's performance by storing fragments of server-side rendered pages in edge cache servers.
It means that after the first rendering, fragments of your pages will be served in a few milliseconds by servers close to your end users!
It's a very efficient way to improve the performance and the SEO of your websites and to dramatically reduce both your hosting costs and the energy consumption of these applications. Help the planet, use React ESI!

Because it is built on top of the Edge Side Includes (ESI) W3C specification, React ESI natively supports most of the well-known cloud cache providers including Cloudflare Workers, Akamai and Fastly. Of course, React ESI also supports the open source Varnish cache server that you can use in your own infrastructure for free (configuration example).

Also, React ESI allows the specification of different Time To Live (TTL) per React component and generates the corresponding HTML asynchronously using a secure (signed) URL. The cache server fetches and stores in the cache all the needed fragments (the HTML corresponding to every React component), builds the final page and sends it to the browser. React ESI also allows components to (re-)render client-side without any specific configuration.

ESI example

Schema from The Varnish Book

Discover React ESI in depth with this presentation

Examples

Install

Using NPM:

$ npm install react-esi

Or using Yarn:

$ yarn add react-esi

Or using PNPM:

$ pnpm add react-esi

Usage

React ESI provides a convenient Higher Order Component that will:

  • replace the wrapped component with an ESI tag server-side (don't worry React ESI also provides the tooling to generate the corresponding fragment);
  • render the wrapped component client-side, and feed it with the server-side computed props (if any).

React ESI automatically calls a static async method named getInitialProps() to populate the initial props of the component. Server-side, this method can access to the HTTP request and response, for instance, to set the Cache-Control header, or some cache tags.

These props returned by getInitialProps() will also be injected in the server-side generated HTML (in a <script> tag). Client-side the component will reuse the props coming from the server (the method will not be called a second time). If the method hasn't been called server-side, then it will be called client-side the first time the component is mounted.

The Higher Order Component

// pages/App.jsx
import withESI from "react-esi/lib/withESI";
import MyFragment from "../components/MyFragment";

const MyFragmentESI = withESI(MyFragment, "MyFragment");
// The second parameter is an unique ID identifying this fragment.
// If you use different instances of the same component, use a different ID per instance.

export const App = () => (
  <div>
    <h1>React ESI demo app</h1>
    <MyFragmentESI greeting="Hello!" />
  </div>
);
// components/MyFragment.jsx
import React from "react";

export default class MyFragment extends React.Component {
  render() {
    return (
      <section>
        <h1>A fragment that can have its own TTL</h1>

        <div>{this.props.greeting /* access to the props as usual */}</div>
        <div>{this.props.dataFromAnAPI}</div>
      </section>
    );
  }

  static async getInitialProps({ props, res }) {
    return new Promise((resolve) => {
      if (res) {
        // Set a TTL for this fragment
        res.set("Cache-Control", "s-maxage=60, max-age=30");
      }

      // Simulate a delay (call to a remote service such as a web API)
      setTimeout(
        () =>
          resolve({
            ...props, // Props coming from index.js, passed through the internal URL
            dataFromAnAPI: "Hello there",
          }),
        2000
      );
    });
  }
}

The initial props must be serializable using JSON.stringify(). Beware Map, Set, and Symbol!

Note: for convenience, getInitialProps() has the same signature as the Next.js one. However, it's a totally independent and standalone implementation (you don't need Next.js to use it).

Serving the Fragments

To serve the fragments, React ESI provides a ready-to-use controller compatible with Express, check out the full example.

Alternatively, here is a full example using a Next.js server:

Features

  • Support Varnish, Cloudflare Workers, Akamai, Fastly, and any other cache systems having ESI support
  • Written in TypeScript
  • Next.js-friendly API

Environment Variables

React ESI can be configured using environment variables:

  • REACT_ESI_SECRET: a secret key used to sign the fragment URL (default to a random string, it's highly recommended to set it to prevent problems when the server restart, or when using multiple servers)
  • REACT_ESI_PATH: the internal path used to generate the fragment, should not be exposed publicly (default: /_fragment)

Passing Attributes to the <esi:include> Element

To pass attributes to the <esi:include> element generated by React ESI, pass a prop having the following structure to the HOC:

{
  esi: {
    attrs: {
      alt: "Alternative text",
      onerror: "continue"
    }
  }
}

Troubleshooting

The Cache is Never Hit

By default, most cache proxies, including Varnish, never serve a response from the cache if the request contains a cookie. If you test using localhost or a similar local domain, clear all pre-existing cookies for this origin. If the cookies are expected (e.g.: Google Analytics or ad cookies), then you must configure properly your cache proxy to ignore them. Here are some examples for Varnish.

Design Considerations

To allow the client-side app to reuse the props fetched or computed server-side, React ESI injects <script> tags containing them in the ESI fragments. After the assembling of the page by the cache server, these script tags end up mixed with the legit HTML. These tags are automatically removed from the DOM before the rendering phase.

Going Further

React ESI plays very well with advanced cache strategies including:

  • Cache invalidation (purge) with cache tags (Varnish / Cloudflare)
  • Warming the cache when data changes in the persistence layer (Varnish)

Give them a try!

Vue.js / Nuxt

We love Vue and Nuxt as much as React and Next, so we're currently porting React ESI for this platform. Contact us if you want to help!

Credits

Created by Kévin Dunglas. Sponsored by Les-Tilleuls.coop.

react-esi's People

Contributors

cecileamrl avatar dependabot[bot] avatar dpnolte avatar dunglas avatar fabious avatar j3m5 avatar jamyouss avatar naholyr avatar paulmolin avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

react-esi's Issues

[SEO] Blank page rendered by Googlebot

Hi,

When using the Google webmaster tool to inspect the specific URL. Google bot can not capture the screenshot of the website. It impacts to SEO. Check it out here.

It's reasonable because the source code now is downloaded to client-side like below, so if using Chrome dev tool to inspect the page, it will also show the same result in the Preview tab:

...
<esi:include src='/_fragment'>

You say "It's a very efficient way to improve the performance and the SEO of your websites".
The site was sped up, but SEO.

Am I doing something wrong? Is there any solution to resolve this case?
Thank you very much for your time.

React 18 support

How does this look for supporting react 18 and server components? I also noticed that trying to install this with React 18 is currently broken as the peer dependency on react 17 does not allow it to install.

image

Is there any downsides of opting-out from default Next.js server?

Is there any downsides of opting-out from default Next.js server?

On Next.js website it's stated:

Before deciding to use a custom server please keep in mind that it should only be used when the >integrated router of Next.js can't meet your app requirements. A custom server will remove >important performance optimizations, like serverless functions and Automatic Static Optimization.

cloudflare workers

This can be used to create an ssr application using cloudflare workers?
I want React PWA, next.js, cloudflare workers

Example without NextJS ?

Thanks for great idea and implementation API.
I have one question about applicability to non-next projects.
Does the library work with RazzleJS or similar SSR frameworks ? As i see the idea could be universally frameworkless, so it could be super useful for other users also.

Error with multiple esi-includes on one page

Hello @dunglas

Thanx for your great work!

I have a php website where I want to include multiple esi-includes :
<esi:include src="/esi/fragment?param=1" alt="Alternative text1" /> <esi:include src="/esi/fragment?param=2" alt="Alternative text2" />

On the other hand I have nextjs in place with version 9.1.5 with customized server.js:
server.js
`const express = require('express')
const next = require('next')
const { path, serveFragment } = require('react-esi/lib/server')

const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

app.prepare().then(() => {
const server = express()

server.use((req, res, next) => {
// Send the Surrogate-Control header to announce ESI support to proxies (optional with Varnish)
res.set('Surrogate-Control', 'content="ESI/1.0"')
next()
})

server.get(path, (req, res) =>
serveFragment(
req,
res,
fragmentID => require(./components/${fragmentID}).default
)
)
server.get('*', handle)

server.listen(3000, err => {
if (err) throw err
console.log(> Ready on http://localhost:3000)
})
})`

package.json
{ "name": "nextson", "version": "1.0.0", "private": true, "main": "dist/server.js", "scripts": { "dev": "node server.js", "build_old": "next build", "start_old": "next start", "build": "next build", "start": "NODE_ENV=production node server.js" }, "dependencies": { "@zeit/next-css": "^1.0.1", "@zeit/next-sass": "^1.0.1", "dotenv-webpack": "1.5.7", "express": "^4.17.1", "isomorphic-unfetch": "^3.0.0", "next": "^9.1.5", "nextjs-redirect": "^1.0.2", "node-sass": "^4.13.0", "react": "^16.9.0", "react-dom": "^16.9.0", "react-esi": "^0.2.0", "react-slick": "^0.25.2", "slick-carousel": "^1.8.1", "url-loader": "^2.2.0" }, "devDependencies": { "@babel/cli": "^7.6.0", "@babel/node": "^7.6.1" } }

pages/hoc/fragment.js
`import React from 'react';
import withESI from 'react-esi';
import Layout from '../../components/layout';
import myfragment from '../../components/myfragment';

//const MyFragmentESI = withESI(myfragment, 'myfragment');
// The second parameter is an unique ID identifying this fragment.
// If you use different instances of the same component, use a different ID per instance.

class Fragment extends React.Component {
static getInitialProps({query}) {
return {query}
}

render() {
    console.log(this.props.query.param);
    const name = 'myfragment_'+this.props.query.param;
    console.log(name);
    const MyFragmentESI = withESI(myfragment, name);
    return(
      <Layout>
        <h1>React ESI demo app</h1>
        <MyFragmentESI greeting="test greeting!" />
      </Layout>
    );
}

}

export default Fragment;
`

components/layout.js
import Link from 'next/link'

export default ({ children, title = 'This is the default title' }) => (

Home {' '} | About {' '} | Contact
{children}

<footer>{'I`m here to stay'}</footer>
)

components/myfragment.js
`import React from 'react';

export default class myfragment extends React.Component {

constructor(props) {
super(props)
this.click = this.click.bind(this)
}

click() {

}

render() {
return (


A fragment that can have its own TTL


{this.props.greeting}

{this.props.dataFromAnAPI}

Take the Shot!

);
}

static async getInitialProps({ props, req, res }) {
if (res) {
res.setHeader('Surrogate-Control','content="ESI/1.0"');
}

return new Promise(resolve => {
  if (res) {
    // Set a TTL for this fragment
    res.setHeader('Cache-Control', 's-maxage=60, max-age=30');
  }

  // Simulate a delay (call to a remote service such as a web API)
  setTimeout(
    () =>
      resolve({
        ...props, // Props coming from index.js, passed through the internal URL
        dataFromAnAPI: 'Hello there'
      }),
    2000
  );
});

}
}
`

varnish settings:
/etc/varnish/default.vlc
`

backend nextjs1 {
.host = "127.0.0.1";
.port = "3001";
.max_connections = 100;
.first_byte_timeout = 800s;
}

sub vcl_recv {
...
if ( req.url ~ "^/_next/") {
set req.url = regsub(req.url, "^/nextjs/", "/");
set req.backend_hint = nextjs.backend();
return (hash);
}

if (req.url ~ "^/esi/") {
    set req.url = regsub(req.url, "^/esi/", "/hoc/");
    # Send Surrogate-Capability headers to announce ESI support to backend
    set req.http.Surrogate-Capability = "key=ESI/1.0";
    set req.backend_hint = nextjs.backend();
    return (hash);
} 

...
}
sub vcl_backend_response {

set beresp.do_esi = true;

...
}

`
Result:
Screenshot 2019-12-16 at 23 00 16

To u see by change what I am doing wrong?
The ssr part is not working properly!

getting following error:
Screenshot 2019-12-16 at 23 01 37

Thanx for your help! Would love to get your library into production

Cheers Klausi

Maintenance: Dependencies, React 18, New Next.js Example, and Hook Implementation

Update React ESI for compatibility with Latest Dependencies and React 18, Provide Next.js Example, Consider Implementing Hook

The task at hand involves updating React ESI to ensure compatibility with the latest dependencies, with a particular emphasis on React 18.

In addition to this, a new example will be provided within the official Next.js examples to replace the previous one that was outdated.

The final part of the task involves the potential implementation of a hook as an alternative to the existing HOC method. Progress on these tasks will be tracked here.

Tasks:

  • Update React ESI to be compatible with latest dependencies
  • Use renderToPipeableStream instead of deprecated renderToNodeStream
  • Validate compatibility with React 18
  • Add a new example to official Next.js examples
  • Evaluate and potentially implement a hook as an alternative to HOC

Very large ESI are truncated

Hello,

First thanks for you work and in particular this implementation of ESI with React (and Next.js in my case).

I am currently facing an issue with contents that have very large size (about 2mb), generated HTML from ESI is missing some ending tags.
I have a Content component that is used to create the HOC (ContentESI). This component calls an API to get its data in getInitialProps. The return of API is complete, the props in window.__REACT_ESI__ are complete. Content component is using many others components to render parts of the API response.
When I render Content without ESI, generated HTML is complete. But when I use the HOC, generated HTML is missing data at the end (so in my case, closing tags).

My knowledge in Node.js & React are very limited, I don't know where to start. I think that it is an issue with Stream, Transform or renderToNodeStream (based on my reading of server.tsx).

I have done tests with:

  • Node.JS 12.x, React 16.x, Next.js 9.5, React ESI 0.3
  • Node.JS 12.x, React 17.x, Next.js 9.5, React ESI 0.3
  • Node.JS 14.x, React 16.x, Next.js 9.5, React ESI 0.3
  • Node.JS 12.x, React 17.x, Next.js 9.5, React ESI 0.3
  • Node.JS 12.x, React 16.x, Next.js 9.5, React ESI 0.2

[Question] how to transpile 'components/Fragment.tsx'

I'm learning react-esi from this react-esi-demo

//server.js
...
server.get(path, (req, res) =>
    serveFragment(
      req,
      res,
      fragmentID => require(`./components/${fragmentID}`).default
    )
  )
...

In server.js, the resovle will get Component from dist which was transpiled by babel.

//.babelrc
{
  "presets": ["next/babel"]
}

When fragment component is js(In this README.md, the example also uses js), babel works well. But when fragment component is tsx, babel doesn't handle it. And when I import less in components/Fragment.tsx, the style file will not be transpiled to dist.

Could you please tell me what should I do, thx!

Handling complex project structure

Two pieces of the README first:

withESI usage:

const MyFragmentESI = withESI(MyFragment, 'MyFragment');
// The second parameter is an unique ID identifying this fragment.
// If you use different instances of the same component, use a different ID per instance.

serving the fragments:

// "fragmentID" is the second parameter passed to the "WithESI" HOC, the root component used for this fragment must be returned
fragmentID => require(`./components/${fragmentID}`).default) 

First though is that the fragmentID is more than just an ID, from this example it seems like it needs to match the filename for the dynamic require to work properly.
If I end up using different ID per instance as suggested I don't know how I'm gonna map the fragmentID to the relevant import path.

I'm working with a very complex project structure with custom resolvers using environment variables so I don't have a flat /components/* folder.

Is there a way to build a map fragmentID => requirePath before starting the server ?

I've seen such algorithm with gettext-extractor where we look for every call of the "getText" function in the source files.
Something like this:

JsExtractors.callExpression(['getText'], {
    arguments: {
      text: 0,
      context: 1,
    },
  })

We could look for every usage of withESI maybe ?

Maybe I'm missing something obvious to solve this, let me know

RFC: Roadmap for React-ESI

RFC: Roadmap for React-ESI

We are seeking feedback on the proposed roadmap for React-ESI and welcome your suggestions to refine it. The items listed below are not ranked in any particular order, and we're open to removing, adding or rearranging them based on priority and community feedback.

  • Dual ESM/CJS Build or ESM Only: Consider publishing react-esi with support for both module systems or transition fully to ESM to align with modern JavaScript standards.

  • Improve ESM Compatibility: Explore various strategies to enhance ESM compatibility, including:

    • Replacing dynamic require in the WithESI component with dynamic import, noting the shift to asynchronous code.
    • Making server-side code isomorphic and potentially leverage code splitting, thereby minimizing or eliminating the need for server-side bundling.
    • Investigating alternative solutions that could offer a balanced approach without significant trade-offs.
  • Function Component for WithESI: Propose a redesign of the WithESI component as a function component, embracing React's modern features and hooks.

  • NextJS-Specific Component: If necessary, introduce a component tailored for NextJS, especially considering the new App Router setup. This would avoid mixing NextJS-specific code into the base component and ensure a clean separation of concerns.

  • Simplify ESI Fragments Distribution in NextJS: Explore NextJS's App Router and API Routes for ESI Fragments, eliminating the need for a custom server. This simplifies deployment and removes the necessity for users to bundle Fragment components in a separate build step, streamlining the development process.

  • Development Dockerfile: Add a Dockerfile designed for development purposes. This would facilitate real-time testing of React-ESI integration and modifications directly within the examples, enhancing the development workflow.

  • E2E Testing: Implement end-to-end (E2E) tests based on the examples. These tests should be configured to run in continuous integration (CI) environments to ensure that new changes do not break existing functionalities, specially in production builds.

  • React/Vite Example: Update our examples to include a React/Vite setup. This reflects current trends in development practices and could simplify the build and bundling process, providing a more streamlined and efficient development experience.

  • Enhance Code Coverage Reporting: Switch to Codecov GitHub Action for uploading code coverage reports. This update replaces the outdated Coveralls integration, providing more accurate and up-to-date coverage analysis directly within our CI/CD pipeline.

  • Expand Ecosystem with Vue-ESI for Vue3 and NuxtJS: Develop a Vue3 and NuxtJS compatible version of React-ESI. This extension aims to bring the benefits of ESI to the Vue community, enhancing server-side rendering capabilities and component caching. Additionally, create an example project to demonstrate Vue-ESI's integration and usage, serving as a reference and starting point for developers.

Your input is invaluable to us as we aim to make React-ESI more robust and aligned with the needs of the developer community. Please share your thoughts, suggestions, or any additional items you believe should be considered for the roadmap.

How to get and serve HTML fragments?

I was wondering how can I render an HTML fragment which is not a component...

For example:
Let's say that example.com/esi-fragment is serving

<div> content </div>

So, as per ESI i can do something like this in an example.com page

<esi:include src="/esi-fragment"/>

And I should see the div rendered in my page.

In all the examples I see online I only can see react components rendered... Which is working, but not really what I want to do.... Is there something I'm getting wrong?

Thanks!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.