Skip to content

Commit

Permalink
Add an example using ESI cache (with React ESI) (vercel#6225)
Browse files Browse the repository at this point in the history
[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.
  • Loading branch information
dunglas authored and timneutkens committed Feb 22, 2019
1 parent d14d170 commit e0896e5
Show file tree
Hide file tree
Showing 14 changed files with 369 additions and 0 deletions.
2 changes: 2 additions & 0 deletions examples/ssr-caching/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 5 additions & 0 deletions examples/with-react-esi/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"presets": [
"next/babel"
]
}
3 changes: 3 additions & 0 deletions examples/with-react-esi/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/node_modules
/dist
.next
13 changes: 13 additions & 0 deletions examples/with-react-esi/Dockerfile
Original file line number Diff line number Diff line change
@@ -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 changes: 42 additions & 0 deletions examples/with-react-esi/README.md
Original file line number Diff line number Diff line change
@@ -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 changes: 22 additions & 0 deletions examples/with-react-esi/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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 changes: 24 additions & 0 deletions examples/with-react-esi/docker/varnish/default.vcl
Original file line number Diff line number Diff line change
@@ -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 changes: 20 additions & 0 deletions examples/with-react-esi/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "with-react-esi",
"author": "Kévin Dunglas <[email protected]>",
"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 changes: 50 additions & 0 deletions examples/with-react-esi/src/components/BreakingNews.js
Original file line number Diff line number Diff line change
@@ -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 changes: 55 additions & 0 deletions examples/with-react-esi/src/components/TopArticles.js
Original file line number Diff line number Diff line change
@@ -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 changes: 37 additions & 0 deletions examples/with-react-esi/src/components/Weather.js
Original file line number Diff line number Diff line change
@@ -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 changes: 33 additions & 0 deletions examples/with-react-esi/src/pages/article.js
Original file line number Diff line number Diff line change
@@ -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 changes: 31 additions & 0 deletions examples/with-react-esi/src/pages/index.js
Original file line number Diff line number Diff line change
@@ -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 changes: 32 additions & 0 deletions examples/with-react-esi/src/server.js
Original file line number Diff line number Diff line change
@@ -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}`)
})
})

0 comments on commit e0896e5

Please sign in to comment.