forked from vercel/next.js
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add an example using ESI cache (with React ESI) (vercel#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.
- Loading branch information
1 parent
d14d170
commit e0896e5
Showing
14 changed files
with
369 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"presets": [ | ||
"next/babel" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
/node_modules | ||
/dist | ||
.next |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`) | ||
}) | ||
}) |