Skip to content

Commit 36abdc7

Browse files
arunodarauchg
authored andcommitted
Prefetch pages with Service Workers (#375)
* Register the service worker. * Update prefetcher code to do prefetching. * Implement the core prefetching API. support "import <Link>, { prefetch } from 'next/prefetch'" * Implement a better communication system with the service worker. * Add a separate example for prefetching * Fix some typos. * Initiate service worker support even prefetching is not used. This is pretty important since initiating will reset the cache. If we don't do this, it's possible to have old cached resources after the user decided to remove all of the prefetching logic. In this case, even the page didn't prefetch it'll use the previously cached pages. That because of there might be a already running service worker. * Use url module to get pathname. * Move prefetcher code to the client from pages Now we also do a webpack build for the prefetcher code. * Add prefetching docs to the README.md * Fix some typo. * Register service worker only if asked to prefetch We also clean the cache always, even we initialize the service worker or not.
1 parent 422c631 commit 36abdc7

File tree

17 files changed

+431
-2
lines changed

17 files changed

+431
-2
lines changed

README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,58 @@ Each top-level component receives a `url` property with the following API:
182182
- `pushTo(url)` - performs a `pushState` call that renders the new `url`. This is equivalent to following a `<Link>`
183183
- `replaceTo(url)` - performs a `replaceState` call that renders the new `url`
184184

185+
### Prefetching Pages
186+
187+
When you are switching between pages, Next.js will download new pages from the server and render them for you. So, it'll take some time to download. Because of that, when you click on a page, it might wait few milliseconds (depending on the network speed) before it render the page.
188+
189+
> Once the Next.js has download the page, it'll reuse it in the next time when you navigate to that same page.
190+
191+
This is a problem specially in UX wise. "Prefetching Pages" is one of our solutions for this problem. With this, Next.js will prefetch pages behind the scene using the support of [Service Workers](https://developers.google.com/web/fundamentals/getting-started/primers/service-workers).
192+
193+
#### Declarative API
194+
195+
You can simply ask Next.js to prefetch pages using `next/prefetch`. See:
196+
197+
```jsx
198+
import Link from 'next/prefetch'
199+
200+
// This is the header component
201+
export default () => (
202+
<div>
203+
<Link href='/'>Home</Link>
204+
<Link href='/about'>Home</Link>
205+
<Link href='/contact'>Home</Link>
206+
</div>
207+
)
208+
```
209+
210+
Here you are using `<Link>` from `next/prefetch` instead of `next/link`. It's an extended version of `next/link` with prefetching support.
211+
212+
Then Next.js will start to prefetch all the pages behind the scene. So, when you click on any of the link it won't need to do a network hit to fetch the page.
213+
214+
If you need, you could stop prefetching like this:
215+
216+
```jsx
217+
<Link href='/contact' prefetch={false}>Home</Link>
218+
```
219+
220+
#### Imperative API
221+
222+
You can get started with prefetching using `<Link>` pretty quickly. But you may want to prefetch based on your own logic. (You may need to write a custom prefetching `<Link>` based on [premonish](https://github.com/mathisonian/premonish).)
223+
224+
Then you can use the imperative API like this:
225+
226+
```jsx
227+
import { prefetch } from 'next/prefetch'
228+
229+
prefetch('/')
230+
prefetch('/features')
231+
```
232+
233+
When you simply run `prefetch('/page_url')` we'll start prefetching that page.
234+
235+
> We can only do this, if `prefetch` is called when loading the current page. So in general, make sure to run `prefetch` calls in a common module all of your pages import.
236+
185237
### Error handling
186238

187239
404 or 500 errors are handled both client and server side by a default component `error.js`. If you wish to override it, define a `_error.js`:

client/next-prefetcher.js

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/* global self */
2+
3+
const CACHE_NAME = 'next-prefetcher-v1'
4+
5+
self.addEventListener('install', () => {
6+
console.log('Installing Next Prefetcher')
7+
})
8+
9+
self.addEventListener('activate', (e) => {
10+
console.log('Activated Next Prefetcher')
11+
e.waitUntil(Promise.all([
12+
resetCache(),
13+
notifyClients()
14+
]))
15+
})
16+
17+
self.addEventListener('fetch', (e) => {
18+
e.respondWith(getResponse(e.request))
19+
})
20+
21+
self.addEventListener('message', (e) => {
22+
switch (e.data.action) {
23+
case 'ADD_URL': {
24+
console.log('CACHING ', e.data.url)
25+
sendReply(e, cacheUrl(e.data.url))
26+
break
27+
}
28+
case 'RESET': {
29+
console.log('RESET')
30+
sendReply(e, resetCache())
31+
break
32+
}
33+
default:
34+
console.error('Unknown action: ' + e.data.action)
35+
}
36+
})
37+
38+
function sendReply (e, result) {
39+
const payload = { action: 'REPLY', actionType: e.data.action, replyFor: e.data.id }
40+
result
41+
.then((result) => {
42+
payload.result = result
43+
e.source.postMessage(payload)
44+
})
45+
.catch((error) => {
46+
payload.error = error.message
47+
e.source.postMessage(payload)
48+
})
49+
}
50+
51+
function cacheUrl (url) {
52+
const req = new self.Request(url, {
53+
mode: 'no-cors'
54+
})
55+
56+
return self.caches.open(CACHE_NAME)
57+
.then((cache) => {
58+
return self.fetch(req)
59+
.then((res) => cache.put(req, res))
60+
})
61+
}
62+
63+
function getResponse (req) {
64+
return self.caches.open(CACHE_NAME)
65+
.then((cache) => cache.match(req))
66+
.then((res) => {
67+
if (res) {
68+
console.log('CACHE HIT: ' + req.url)
69+
return res
70+
} else {
71+
console.log('CACHE MISS: ' + req.url)
72+
return self.fetch(req)
73+
}
74+
})
75+
}
76+
77+
function resetCache () {
78+
let cache
79+
80+
return self.caches.open(CACHE_NAME)
81+
.then((c) => {
82+
cache = c
83+
return cache.keys()
84+
})
85+
.then(function (items) {
86+
const deleteAll = items.map((item) => cache.delete(item))
87+
return Promise.all(deleteAll)
88+
})
89+
}
90+
91+
function notifyClients () {
92+
return self.clients.claim()
93+
.then(() => self.clients.matchAll())
94+
.then((clients) => {
95+
const notifyAll = clients.map((client) => {
96+
return client.postMessage({ action: 'NEXT_PREFETCHER_ACTIVATED' })
97+
})
98+
return Promise.all(notifyAll)
99+
})
100+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Example app with prefetching pages
2+
3+
This example features:
4+
5+
* An app with four simple pages
6+
* It will prefetch all the pages in the background except the "contact" page
7+
8+
## How to run it
9+
10+
```sh
11+
npm install
12+
npm run dev
13+
```
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import React from 'react'
2+
import Link, { prefetch } from 'next/prefetch'
3+
4+
// Prefetch using the imperative API
5+
prefetch('/')
6+
7+
const styles = {
8+
a: {
9+
marginRight: 10
10+
}
11+
}
12+
13+
export default () => (
14+
<div>
15+
{ /* Prefetch using the declarative API */ }
16+
<Link href='/'>
17+
<a style={styles.a} >Home</a>
18+
</Link>
19+
20+
<Link href='/features'>
21+
<a style={styles.a} >Features</a>
22+
</Link>
23+
24+
<Link href='/about'>
25+
<a style={styles.a} >About</a>
26+
</Link>
27+
28+
<Link href='/contact' prefetch={false}>
29+
<a style={styles.a} >Contact (<small>NO-PREFETCHING</small>)</a>
30+
</Link>
31+
</div>
32+
)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "with-prefetching",
3+
"version": "1.0.0",
4+
"scripts": {
5+
"dev": "next",
6+
"build": "next build",
7+
"start": "next start"
8+
},
9+
"dependencies": {
10+
"next": "*"
11+
},
12+
"author": "",
13+
"license": "ISC"
14+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import React from 'react'
2+
import Header from '../components/Header'
3+
4+
export default () => (
5+
<div>
6+
<Header />
7+
<p>This is the ABOUT page.</p>
8+
</div>
9+
)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import React from 'react'
2+
import Header from '../components/Header'
3+
4+
export default () => (
5+
<div>
6+
<Header />
7+
<p>This is the CONTACT page.</p>
8+
</div>
9+
)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import React from 'react'
2+
import Header from '../components/Header'
3+
4+
export default () => (
5+
<div>
6+
<Header />
7+
<p>This is the FEATURES page.</p>
8+
</div>
9+
)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import React from 'react'
2+
import Header from '../components/Header'
3+
4+
export default () => (
5+
<div>
6+
<Header />
7+
<p>This is the HOME page</p>
8+
</div>
9+
)

gulpfile.js

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ gulp.task('copy-bench-fixtures', () => {
7272

7373
gulp.task('build', [
7474
'build-dev-client',
75-
'build-client'
75+
'build-client',
76+
'build-prefetcher'
7677
])
7778

7879
gulp.task('build-dev-client', ['compile-lib', 'compile-client'], () => {
@@ -133,6 +134,44 @@ gulp.task('build-client', ['compile-lib', 'compile-client'], () => {
133134
.pipe(notify('Built release client'))
134135
})
135136

137+
gulp.task('build-prefetcher', ['compile-lib', 'compile-client'], () => {
138+
return gulp
139+
.src('client/next-prefetcher.js')
140+
.pipe(webpack({
141+
quiet: true,
142+
output: { filename: 'next-prefetcher-bundle.js' },
143+
plugins: [
144+
new webpack.webpack.DefinePlugin({
145+
'process.env': {
146+
NODE_ENV: JSON.stringify('production')
147+
}
148+
})
149+
],
150+
module: {
151+
loaders: [
152+
{
153+
test: /\.js$/,
154+
exclude: /node_modules/,
155+
loader: 'babel',
156+
query: {
157+
'babelrc': false,
158+
'presets': [
159+
['env', {
160+
'targets': {
161+
// All browsers which supports service workers
162+
'browsers': ['chrome 49', 'firefox 49', 'opera 41']
163+
}
164+
}]
165+
]
166+
}
167+
}
168+
]
169+
}
170+
}))
171+
.pipe(gulp.dest('dist/client'))
172+
.pipe(notify('Built release prefetcher'))
173+
})
174+
136175
gulp.task('test', () => {
137176
return gulp.src('./test')
138177
.pipe(jest.default({

0 commit comments

Comments
 (0)