Skip to content

Commit

Permalink
Major: Rework exports; Add throttle and limit options (#120)
Browse files Browse the repository at this point in the history
* [major] rewrite w/ new exports & options;

- add `throttle` option
- add `limit` option
- export `prefetch` directly
- export old `default` as `listen` method
- remove `urls` option (temp?)
- return early if no IO support
- return `reset` function if okay

* chore: remove `static-url-list` test

* chore: update usage

* golf: shave 4B ~

* fix: xbrowser throw on `set.size` getter

* chore: shuffle "main" location

* feat: add `onError` option for prefetch catch

* fix: expand timeout

* feat: allow `prefetch` to accept url[] list

* fix: remove TODO notes

* golf: always handle `prefetch` as array

* chore: complete tests

* chore: add `throttle` tests

* chore: update docs

* fix: separate listen() vs prefetch() callers

* docs: format API docs differently (#1)
  • Loading branch information
lukeed authored and addyosmani committed Sep 13, 2019
1 parent d75188d commit 4044de0
Show file tree
Hide file tree
Showing 22 changed files with 505 additions and 190 deletions.
187 changes: 144 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Quickstart:
<script src="dist/quicklink.umd.js"></script>
<!-- Initialize (you can do this whenever you want) -->
<script>
quicklink();
quicklink.listen();
</script>
```

Expand All @@ -53,37 +53,129 @@ For example, you can initialize after the `load` event fires:
```html
<script>
window.addEventListener('load', () =>{
quicklink();
quicklink.listen();
});
</script>
```

ES Module import:

```js
import quicklink from "quicklink/dist/quicklink.mjs";
quicklink();
import { listen, prefetch } from "quicklink";
```

The above options are best for multi-page sites. Single-page apps have a few options available for using quicklink with a router:

* Call `quicklink()` once a navigation to a new route has completed
* Call `quicklink()` against a specific DOM element / component
* Call `quicklink({urls:[...]})` with a custom set of URLs to prefetch
* Call `quicklink.listen()` once a navigation to a new route has completed
* Call `quicklink.listen()` against a specific DOM element / component
* Call `quicklink.prefetch()` with a custom set of URLs to prefetch

## API

`quicklink` accepts an optional options object with the following parameters:
### quicklink.listen(options)
Returns: `Function`

* `el`: DOM element to observe for in-viewport links to prefetch
* `urls`: Static array of URLs to prefetch (instead of observing `document` or a DOM element links in the viewport)
* `timeout`: Integer for the `requestIdleCallback` timeout. A time in milliseconds by which the browser must execute prefetching. Defaults to 2 seconds.
* `timeoutFn`: Function for specifying a timeout. Defaults to `requestIdleCallback`. Can also be swapped out for a custom function like [networkIdleCallback](https://github.com/pastelsky/network-idle-callback) (see demos)
* `priority`: Boolean specifying preferred priority for fetches. Defaults to `false`. `true` will attempt to use the `fetch()` API where supported (rather than rel=prefetch)
* `origins`: Static array of URL hostname strings that are allowed to be prefetched. Defaults to the same domain origin, which prevents _any_ cross-origin requests.
* `ignores`: A RegExp, Function, or Array that further determines if a URL should be prefetched. These execute _after_ origin matching.
A "reset" function is returned, which will empty the active `IntersectionObserver` and the cache of URLs that have already been prefetched. This can be used between page navigations and/or when significant DOM changes have occurred.

#### options.el
Type: `HTMLElement`<br>
Default: `document.body`

The DOM element to observe for in-viewport links to prefetch.

#### options.limit
Type: `Number`<br>
Default: `Infinity`

The _total_ requests that can be prefetched while observing the `options.el` container.

#### options.throttle
Type: `Number`<br>
Default: `Infinity`

The _concurrency limit_ for simultaneous requests while observing the `options.el` container.

#### options.timeout
Type: `Number`<br>
Default: `2000`

The `requestIdleCallback` timeout, in milliseconds.

> **Note:** The browser must be idle for the configured duration before prefetching.
#### options.timeoutFn
Type: `Function`<br>
Default: `requestIdleCallback`

A function used for specifying a `timeout` delay.<br>
This can be swapped out for a custom function like [networkIdleCallback](https://github.com/pastelsky/network-idle-callback) (see demos).

By default, this uses [`requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback) or the embedded polyfill.

#### options.priority
Type: `Boolean`<br>
Default: `false`

Whether or not the URLs within the `options.el` container should be treated as high priority.

When `true`, quicklink will attempt to use the `fetch()` API if supported (rather than `link[rel=prefetch]`).

#### options.origins
Type: `Array<String>`<br>
Default: `[location.hostname]`

A static array of URL hostnames that are allowed to be prefetched.<br>
Defaults to the same domain origin, which prevents _any_ cross-origin requests.

**Important:** An empty array (`[]`) allows ***all origins*** to be prefetched.

#### options.ignores
Type: `RegExp` or `Function` or `Array`<br>
Default: `[]`

Determine if a URL should be prefetched.

When a `RegExp` tests positive, a `Function` returns `true`, or an `Array` contains the string, then the URL is _not_ prefetched.

> **Note:** An `Array` may contain `String`, `RegExp`, or `Function` values.
> **Important:** This logic is executed _after_ origin matching!
#### options.onError
Type: `Function`<br>
Default: None

An optional error handler that will receive any errors from prefetched requests.<br>
By default, these errors are silently ignored.


### quicklink.prefetch(urls, isPriority)
Returns: `Promise`

The `urls` provided are always passed through `Promise.all`, which means the result will always resolve to an Array.

> **Important:** You much `catch` you own request error(s).
#### urls
Type: `String` or `Array<String>`<br>
Required: `true`

One or many URLs to be prefetched.

> **Note:** Each `url` value is resolved from the current location.
#### isPriority
Type: `Boolean`<br>
Default: `false`

Whether or not the URL(s) should be treated as "high priority" targets.<br>
By default, calls to `prefetch()` are low priority.

> **Note:** This behaves identically to `listen()`'s `priority` option.

## TODO

TODO:
* Explore detecting file-extension of resources and using [rel=preload](https://w3c.github.io/preload/) for high priority fetches
* Explore using [Priority Hints](https://github.com/WICG/priority-hints) for importance hinting

Expand All @@ -107,7 +199,7 @@ Alternatively, see the [Intersection Observer polyfill](https://github.com/w3c/I
Defaults to 2 seconds (via `requestIdleCallback`). Here we override it to 4 seconds:

```js
quicklink({
quicklink.listen({
timeout: 4000
});
```
Expand All @@ -117,28 +209,35 @@ quicklink({
Defaults to `document` otherwise.

```js
const elem = document.getElementById('carousel');
quicklink({
el: elem
quicklink.listen({
el: document.getElementById('carousel')
});
```

### Set a custom array of URLs to be prefetched
### Programmatically `prefetch()` URLs

If you would prefer to provide a static list of URLs to be prefetched, instead of detecting those in-viewport, customizing URLs is supported.

```js
quicklink({
urls: ['2.html','3.html', '4.js']
});
// Single URL
quicklink.prefetch('2.html');

// Multiple URLs
quicklink.prefetch(['2.html', '3.html', '4.js']);

// Multiple URLs, with high priority
// Note: Can also be use with single URL!
quicklink.prefetch(['2.html', '3.html', '4.js'], true);
```

### Set the request priority for prefetches
### Set the request priority for prefetches while scrolling

Defaults to low-priority (`rel=prefetch` or XHR). For high-priority (`priority: true`), attempts to use `fetch()` or falls back to XHR.

> **Note:** This runs `prefetch(..., true)` with URLs found within the `options.el` container.
```js
quicklink({ priority: true });
quicklink.listen({ priority: true });
```

### Specify a custom list of allowed origins
Expand All @@ -148,7 +247,7 @@ Provide a list of hostnames that should be prefetch-able. Only the same origin i
> **Important:** You must also include your own hostname!
```js
quicklink({
quicklink.listen({
origins: [
// add mine
'my-website.com',
Expand All @@ -168,7 +267,7 @@ Enables all cross-origin requests to be made.
> **Note:** You may run into [CORB](https://chromium.googlesource.com/chromium/src/+/master/services/network/cross_origin_read_blocking_explainer.md) and [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) issues!
```js
quicklink({
quicklink.listen({
origins: true,
// or
origins: []
Expand All @@ -187,7 +286,7 @@ These filters run _after_ the `origins` matching has run. Ignores can be useful
// - all ".zip" extensions
// - all <a> tags with "noprefetch" attribute
//
quicklink({
quicklink.listen({
ignores: [
/\/api\/?/,
uri => uri.includes('.zip'),
Expand All @@ -201,16 +300,16 @@ You may also wish to ignore prefetches to URLs which contain a URL fragment (e.g
Using `ignores` this can be achieved as follows:

```js
quicklink({
ignores: [
uri => uri.includes('#')
// or RegExp: /#(.+)/
// or element matching: (uri, elem) => !!elem.hash
]
quicklink.listen({
ignores: [
uri => uri.includes('#')
// or RegExp: /#(.+)/
// or element matching: (uri, elem) => !!elem.hash
]
});
```

## Browser support
## Browser Support

The prefetching provided by `quicklink` can be viewed as a [progressive enhancement](https://www.smashingmagazine.com/2009/04/progressive-enhancement-what-it-is-and-how-to-use-it/). Cross-browser support is as follows:

Expand All @@ -225,15 +324,17 @@ Certain features have layered support:

## Using the prefetcher directly

`quicklink` includes a prefetcher that can be individually imported for use in other projects. After installing `quicklink` as a dependency, you can use it as follows:
A `prefetch` method can be individually imported for use in other projects.<br>
This method includes the logic to respect Data Saver and 2G connections. It also issues requests thru `fetch()`, XHRs, or `link[rel=prefetch]` depending on (a) the `isPriority` value and (b) the current browser's support.

After installing `quicklink` as a dependency, you can use it as follows:

```html
<script type="module">
import prefetch from '../src/prefetch.mjs';
const urls = ['1.html', '2.html'];
const promises = urls.map(url => prefetch(url));
Promise.all(promises);
import { prefetch } from 'quicklink';
prefetch(['1.html', '2.html']).catch(err => {
// Handle own errors
});
</script>
```

Expand All @@ -258,7 +359,7 @@ Please note: this is by no means an exhaustive benchmark of the pros and cons of

### Session Stitching

Cross-origin prefetching (e.g a.com/foo.html prefetches b.com/bar.html) has a number of limitations. One such limitation is with session-stitching. b.com may expect a.com's navigation requests to include session information (e.g a temporary ID - e.g b.com/bar.html?hash=<>&timestamp=<>), where this information is used to customize the experience or log information to analytics. If session-stitching requires a timestamp in the URL, what is prefetched and stored in the HTTP cache may not be the same as the one the user ultimately navigates to. This introduces a challenge as it can result in double prefetches.
Cross-origin prefetching (e.g a.com/foo.html prefetches b.com/bar.html) has a number of limitations. One such limitation is with session-stitching. b.com may expect a.com's navigation requests to include session information (e.g a temporary ID - e.g b.com/bar.html?hash=<>&timestamp=<>), where this information is used to customize the experience or log information to analytics. If session-stitching requires a timestamp in the URL, what is prefetched and stored in the HTTP cache may not be the same as the one the user ultimately navigates to. This introduces a challenge as it can result in double prefetches.

To workaround this problem, you can consider passing along session information via the [ping attribute](https://caniuse.com/#feat=ping) (separately) so the origin can stitch a session together asynchronously.

Expand Down
4 changes: 2 additions & 2 deletions demos/basic.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ <h1>Basic demo</h1>
</div>
<script src="../dist/quicklink.umd.js"></script>
<script>
quicklink();
quicklink.listen();
</script>
</body>

</html>
</html>
4 changes: 2 additions & 2 deletions demos/network-idle.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@
<script src="network-idle.js"></script>
<script type="module">
import quicklink from "../dist/quicklink.mjs";
quicklink({ timeoutFn: networkIdleCallback });
quicklink.listen({ timeoutFn: networkIdleCallback });
</script>
</body>

</html>
</html>
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
"name": "quicklink",
"version": "1.0.1",
"description": "Faster subsequent page-loads by prefetching in-viewport links during idle time",
"main": "dist/quicklink.js",
"repository": "https://github.com/GoogleChromeLabs/quicklink.git",
"homepage": "https://github.com/GoogleChromeLabs/quicklink",
"bugs": {
"url": "https://github.com/GoogleChromeLabs/quicklink/issues"
},
"author": "addyosmani <[email protected]>",
"license": "Apache-2.0",
"main": "dist/quicklink.js",
"module": "dist/quicklink.mjs",
"jsnext:main": "dist/quicklink.mjs",
"umd:main": "dist/quicklink.umd.js",
Expand All @@ -19,7 +19,7 @@
"lint-fix": "eslint src/*.mjs test/*.js --fix demos/*.js",
"start": "http-server .",
"test": "yarn run build && mocha test/bootstrap.js --recursive test",
"build": "microbundle src/index.mjs --no-sourcemap",
"build": "microbundle src/index.mjs --no-sourcemap --external none",
"prepare": "yarn run -s build",
"bundlesize": "bundlesize",
"changelog": "yarn conventional-changelog -i CHANGELOG.md -s -r 0",
Expand All @@ -33,6 +33,9 @@
"background",
"speed"
],
"dependencies": {
"throttles": "^1.0.0"
},
"devDependencies": {
"babel-preset-env": "^1.7.0",
"bundlesize": "^0.17.0",
Expand All @@ -53,4 +56,4 @@
"maxSize": "2 kB"
}
]
}
}
Loading

0 comments on commit 4044de0

Please sign in to comment.