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:
parent
d14d1704cb
commit
e0896e5dbe
14 changed files with 369 additions and 0 deletions
|
@ -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/).
|
||||
|
|
5
examples/with-react-esi/.babelrc
Normal file
5
examples/with-react-esi/.babelrc
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"presets": [
|
||||
"next/babel"
|
||||
]
|
||||
}
|
3
examples/with-react-esi/.gitignore
vendored
Normal file
3
examples/with-react-esi/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
/node_modules
|
||||
/dist
|
||||
.next
|
13
examples/with-react-esi/Dockerfile
Normal file
13
examples/with-react-esi/Dockerfile
Normal 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
|
42
examples/with-react-esi/README.md
Normal file
42
examples/with-react-esi/README.md
Normal 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.
|
22
examples/with-react-esi/docker-compose.yml
Normal file
22
examples/with-react-esi/docker-compose.yml
Normal 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"
|
24
examples/with-react-esi/docker/varnish/default.vcl
Normal file
24
examples/with-react-esi/docker/varnish/default.vcl
Normal 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";
|
||||
}
|
20
examples/with-react-esi/package.json
Normal file
20
examples/with-react-esi/package.json
Normal 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"
|
||||
}
|
||||
}
|
50
examples/with-react-esi/src/components/BreakingNews.js
Normal file
50
examples/with-react-esi/src/components/BreakingNews.js
Normal 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
|
55
examples/with-react-esi/src/components/TopArticles.js
Normal file
55
examples/with-react-esi/src/components/TopArticles.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
37
examples/with-react-esi/src/components/Weather.js
Normal file
37
examples/with-react-esi/src/components/Weather.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
33
examples/with-react-esi/src/pages/article.js
Normal file
33
examples/with-react-esi/src/pages/article.js
Normal 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
|
31
examples/with-react-esi/src/pages/index.js
Normal file
31
examples/with-react-esi/src/pages/index.js
Normal 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
|
32
examples/with-react-esi/src/server.js
Normal file
32
examples/with-react-esi/src/server.js
Normal 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}`)
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue