Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 6 commits into from
Feb 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
{
dunglas marked this conversation as resolved.
Show resolved Hide resolved
"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:
dunglas marked this conversation as resolved.
Show resolved Hide resolved

```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 <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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not needed: babel src -d dist &&

Copy link
Member

@timneutkens timneutkens Feb 8, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's weird to run babel over the src directory 🤔 You'd be triple parsing and compiling.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that React ESI needs to be able to serve the components (src/components) directly. Is it possible to reuse the ones compiled by Next?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see, that makes it indeed harder, all components are put into a single bundle per page and are not available to be required outside of the bundle, so that is an issue.

"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}`)
})
})