Add an example using ESI cache (with React ESI) (#6225)

[React ESI](https://github.com/dunglas/react-esi) is a brand new cache library for vanilla React and Next.js applications, that can make highly dynamic applications as fast as static sites by leveraging the open Edge Server Include specification.

https://github.com/dunglas/react-esi

Because this spec is widespread, 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 provided).

This PR shows how to integrate React ESI with Next.js.
This commit is contained in:
Kévin Dunglas 2019-02-22 09:32:51 +01:00 committed by Tim Neutkens
parent d14d1704cb
commit e0896e5dbe
14 changed files with 369 additions and 0 deletions

View file

@ -45,3 +45,5 @@ React Server Side rendering is very costly and takes a lot of server's CPU power
That's what this example demonstrate.
This app uses Next's [custom server and routing](https://github.com/zeit/next.js#custom-server-and-routing) mode. It also uses [express](https://expressjs.com/) to handle routing and page serving.
Alternatively, see [the example using React ESI](../with-react-esi/).

View file

@ -0,0 +1,5 @@
{
"presets": [
"next/babel"
]
}

3
examples/with-react-esi/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/node_modules
/dist
.next

View file

@ -0,0 +1,13 @@
FROM cooptilleuls/varnish:6.0-alpine AS varnish
COPY docker/varnish/default.vcl /usr/local/etc/varnish/default.vcl
FROM node:11.5-alpine as node
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
COPY . ./
RUN yarn install
RUN yarn build
CMD yarn start

View file

@ -0,0 +1,42 @@
[![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/zeit/next.js/tree/master/examples/with-react-esi)
# React ESI example
# Example app with prefetching pages
## How to use
### Using `create-next-app`
Execute [`create-next-app`](https://github.com/segmentio/create-next-app) with [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) or [npx](https://github.com/zkat/npx#readme) to bootstrap the example:
```bash
npx create-next-app --example with-react-esi with-react-esi-app
# or
yarn create next-app --example with-react-esi with-react-esi-app
```
### Download manually
Download the example:
```bash
curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-react-esi
cd with-react-esi
```
### Starting the Varnish cache server
A Docker setup containing Varnish with [the appropriate config](docker/varnish/default.vcl) and Node is provided.
Run the following command to start the project:
```bash
docker-compose up
```
## The idea behind the example
React Server Side rendering is very costly and takes a lot of server's CPU power for that.
One of the best solutions for this problem is cache fragments of rendered pages, each fragment corresponding to a component subtree.
This example shows how to leverage [React ESI](https://github.com/dunglas/react-esi) and the Varnish HTTP accelerator to improve dramatically the performance of an app.
The example (and the underlying lib) can work with any ESI implementation, including Akamai, Fastly and Cloudflare Workers.

View file

@ -0,0 +1,22 @@
version: '3.4'
services:
node:
build:
context: .
target: node
ports:
- "8080:80" # To debug
varnish:
build:
context: .
target: varnish
depends_on:
- node
volumes:
- ./docker/varnish/:/usr/local/etc/varnish:ro
tmpfs:
- /usr/local/var/varnish:exec
ports:
- "80:80"

View file

@ -0,0 +1,24 @@
vcl 4.0;
import std;
backend node {
.host = "node";
.port = "80";
}
sub vcl_backend_response {
# Enable ESI support
if (beresp.http.Surrogate-Control ~ "ESI/1.0") {
unset beresp.http.Surrogate-Control;
set beresp.do_esi = true;
}
}
sub vcl_recv {
# Remove cookies to prevent a cache miss, you maybe don't want to do this!
unset req.http.cookie;
# Announce ESI support to Node (optional)
set req.http.Surrogate-Capability = "key=ESI/1.0";
}

View file

@ -0,0 +1,20 @@
{
"name": "with-react-esi",
"author": "Kévin Dunglas <dunglas@gmail.com>",
"main": "dist/server.js",
"dependencies": {
"express": "^4.16.4",
"next": "^7.0.2",
"react": "^16.7.0",
"react-dom": "^16.7.0",
"react-esi": "^0.1"
},
"scripts": {
"build": "babel src -d dist && next build dist",
"start": "NODE_ENV=production node dist/server.js"
},
"devDependencies": {
"@babel/cli": "^7.2.3",
"@babel/node": "^7.2.2"
}
}

View file

@ -0,0 +1,50 @@
import React from 'react'
// functional component
const BreakingNews = props => (
<section>
<h1>Breaking News</h1>
{props.news &&
props.news.map((breaking, i) => (
<article key={i}>
<h1>{breaking.title}</h1>
<p>{breaking.body}</p>
</article>
))}
We are <b>{process.browser ? 'client-side' : 'server-side'}</b> (now, check
the source of this page)
<div>
<small>generated at {new Date().toISOString()}</small>
</div>
</section>
)
BreakingNews.getInitialProps = async ({ props, req, res }) => {
if (res) {
// server-side, we always want to serve fresh data for this block!
res.set('Cache-Control', 's-maxage=0, maxage=0')
}
return new Promise(resolve =>
// Simulate a delay (slow network, huge computation...)
setTimeout(
() =>
resolve({
...props, // Props from the main page, passed through the internal fragment URL server-side
news: [
{
title: 'Aenean eleifend ex',
body: 'Proin commodo ullamcorper cursus.'
},
{
title: 'Morbi rutrum tortor nec eros vestibulum',
body: 'Maecenas gravida eu sapien quis sollicitudin.'
}
]
}),
5000
)
)
}
export default BreakingNews

View file

@ -0,0 +1,55 @@
import React from 'react'
/**
* Return the top articles of the month. Can be cached 1 hour.
*/
export default class TopArticles extends React.Component {
static async getInitialProps ({ props, req, res }) {
if (res) {
// server side, cache this fragment for 1 hour
res.set('Cache-Control', 'public, s-maxage=3600')
}
// Fetch the articles from a remote API, it may take some time...
return new Promise(resolve => {
// Simulate a delay (slow network, huge computation...)
setTimeout(
() =>
resolve({
...props, // Props from the main page, passed through the internal fragment URL server-side
articles: [
{
title: 'Lorem ipsum dolor',
body: 'Phasellus aliquet pellentesque dolor nec volutpat.'
},
{
title: 'Donec ut porttitor nisl',
body: 'Praesent vel odio vel dui pellentesque sodales.'
}
]
}),
2000
)
})
}
render () {
return (
<section>
<h1>Top articles</h1>
{this.props.articles &&
this.props.articles.map((article, i) => (
<article key={i}>
<h1>{article.title}</h1>
<p>{article.body}</p>
</article>
))}
This block has been generated the first time as an include of{' '}
<b>{this.props.from}</b>.
<div>
<small>generated at {new Date().toISOString()}</small>
</div>
</section>
)
}
}

View file

@ -0,0 +1,37 @@
import React from 'react'
/**
* Return the weather. This component is not loaded on the homepage, to test that getInitialProps works client-side too.
*/
export default class TopArticles extends React.Component {
static async getInitialProps ({ props, req, res }) {
// Fetch the weather from a remote API, it may take some time...
return new Promise(resolve => {
console.log(process.browser ? 'client-side' : 'server-side')
// Simulate a delay (slow network, huge computation...)
setTimeout(
() =>
resolve({
...props, // Props from the main page, passed through the internal fragment URL server-side
weather: 'sunny ☀️'
}),
2000
)
})
}
render () {
console.log(process.browser ? 'client-side' : 'server-side')
return (
<section>
<h1>Weather</h1>
{this.props.weather}
<div>
<small>generated at {new Date().toISOString()}</small>
</div>
</section>
)
}
}

View file

@ -0,0 +1,33 @@
import withESI from 'react-esi'
import React from 'react'
import Link from 'next/link'
import BreakingNews from '../components/BreakingNews'
import TopArticles from '../components/TopArticles'
import Weather from '../components/Weather'
const BreakingNewsESI = withESI(BreakingNews, 'BreakingNews')
const TopArticlesESI = withESI(TopArticles, 'TopArticles')
const WeatherESI = withESI(Weather, 'Weather')
const Article = () => (
<div>
<h1>An article</h1>
<main>This a specific article of the website!</main>
{/* TODO: introduce a layout */}
<TopArticlesESI from='the article page' />
<BreakingNewsESI />
<WeatherESI />
<Link href='/'>
<a>Go back to the homepage</a>
</Link>
</div>
)
Article.getInitialProps = async function ({ res }) {
if (res) res.set('Cache-Control', 's-maxage: 10, maxage: 0')
return {}
}
export default Article

View file

@ -0,0 +1,31 @@
import withESI from 'react-esi'
import React from 'react'
import Link from 'next/link'
import BreakingNews from '../components/BreakingNews'
import TopArticles from '../components/TopArticles'
const BreakingNewsESI = withESI(BreakingNews, 'BreakingNews')
const TopArticlesESI = withESI(TopArticles, 'TopArticles')
const Index = () => (
<div>
<h1>React ESI demo app</h1>
<main>
<p>Welcome to my news website!</p>
<Link href='/article'>
<a>Go to an article</a>
</Link>
</main>
{/* TODO: introduce a layout */}
<TopArticlesESI from='the main page' />
<BreakingNewsESI />
</div>
)
Index.getInitialProps = async function ({ res }) {
if (res) res.set('Cache-Control', 's-maxage: 10')
return {}
}
export default Index

View file

@ -0,0 +1,32 @@
import express from 'express'
import next from 'next'
import { path, serveFragment } from 'react-esi/lib/server'
const dev = process.env.NODE_ENV !== 'production'
const port = parseInt(process.env.PORT, 10) || (dev ? 3000 : 80)
const app = next({ dev, dir: dev ? 'src/' : 'dist/' })
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(port, err => {
if (err) throw err
console.log(`> Ready on http://localhost:${port}`)
})
})