-
Notifications
You must be signed in to change notification settings - Fork 407
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[core] Introduce prefetch chunks build (#171)
* Adds prefetch chunks implementation * [fix] unit tests for prefetchchunks (#168) * [fix] unit tests for prefetchchunks * [chore] cleaned up console logs * [infra] default accessor argument added * [fix] mistyped expected value (#169) * [fix] mistyped expected value * [chore] debugging prefetch chunks test * [chore] debugging with normal JS functions * [chore] debugging by adding a real css file * [chore] debugging * [chore] debugging * [chore] debugging * [chore] debugging * [chore] debugging * [chore] debugging * [chore] debugging * [chore] debugging * [chore] debugging tests * [chore] debugging tests * [chore] cleaned up Co-authored-by: Anton Karlovskiy <antonkarlovskiy@outlook.com>
- Loading branch information
1 parent
836f170
commit 301aedb
Showing
8 changed files
with
284 additions
and
6 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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ npm-debug.log* | |
yarn-debug.log* | ||
yarn-error.log* | ||
dist | ||
.DS_Store | ||
|
||
# Runtime data | ||
pids | ||
|
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,141 @@ | ||
/** | ||
* Copyright 2018 Google Inc. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
**/ | ||
import throttle from 'throttles'; | ||
import { priority, supported } from './prefetch.mjs'; | ||
import requestIdleCallback from './request-idle-callback.mjs'; | ||
|
||
// Cache of URLs we've prefetched | ||
// Its `size` is compared against `opts.limit` value. | ||
const toPrefetch = new Set(); | ||
|
||
/** | ||
* Determine if the anchor tag should be prefetched. | ||
* A filter can be a RegExp, Function, or Array of both. | ||
* - Function receives `node.href, node` arguments | ||
* - RegExp receives `node.href` only (the full URL) | ||
* @param {Element} node The anchor (<a>) tag. | ||
* @param {Mixed} filter The custom filter(s) | ||
* @return {Boolean} If true, then it should be ignored | ||
*/ | ||
function isIgnored(node, filter) { | ||
return Array.isArray(filter) | ||
? filter.some(x => isIgnored(node, x)) | ||
: (filter.test || filter).call(filter, node.href, node); | ||
} | ||
|
||
/** | ||
* Prefetch an array of URLs if the user's effective | ||
* connection type and data-saver preferences suggests | ||
* it would be useful. By default, looks at in-viewport | ||
* links for `document`. Can also work off a supplied | ||
* DOM element or static array of URLs. | ||
* @param {Object} options - Configuration options for quicklink | ||
* @param {Object} [options.el] - DOM element to prefetch in-viewport links of | ||
* @param {Boolean} [options.priority] - Attempt higher priority fetch (low or high) | ||
* @param {Array} [options.origins] - Allowed origins to prefetch (empty allows all) | ||
* @param {Array|RegExp|Function} [options.ignores] - Custom filter(s) that run after origin checks | ||
* @param {Number} [options.timeout] - Timeout after which prefetching will occur | ||
* @param {Number} [options.throttle] - The concurrency limit for prefetching | ||
* @param {Number} [options.limit] - The total number of prefetches to allow | ||
* @param {Function} [options.timeoutFn] - Custom timeout function | ||
* @param {Function} [options.onError] - Error handler for failed `prefetch` requests | ||
* @param {Function} [options.prefetchChunks] - Function to prefetch chunks for route URLs (with route manifest for URL mapping) | ||
*/ | ||
export function listen(options) { | ||
if (!options) options = {}; | ||
if (!window.IntersectionObserver) return; | ||
|
||
const [toAdd, isDone] = throttle(options.throttle || 1/0); | ||
const limit = options.limit || 1/0; | ||
|
||
const allowed = options.origins || [location.hostname]; | ||
const ignores = options.ignores || []; | ||
|
||
const timeoutFn = options.timeoutFn || requestIdleCallback; | ||
|
||
const prefetchChunks = options.prefetchChunks; | ||
|
||
const prefetchHandler = urls => { | ||
prefetch(urls, options.priority).then(isDone).catch(err => { | ||
isDone(); if (options.onError) options.onError(err); | ||
}); | ||
}; | ||
|
||
const observer = new IntersectionObserver(entries => { | ||
entries.forEach(entry => { | ||
if (entry.isIntersecting) { | ||
observer.unobserve(entry = entry.target); | ||
// Do not prefetch if will match/exceed limit | ||
if (toPrefetch.size < limit) { | ||
toAdd(() => { | ||
prefetchChunks ? prefetchChunks(entry, prefetchHandler) : prefetchHandler(entry.href); | ||
}); | ||
} | ||
} | ||
}); | ||
}); | ||
|
||
timeoutFn(() => { | ||
// Find all links & Connect them to IO if allowed | ||
(options.el || document).querySelectorAll('a').forEach(link => { | ||
// If the anchor matches a permitted origin | ||
// ~> A `[]` or `true` means everything is allowed | ||
if (!allowed.length || allowed.includes(link.hostname)) { | ||
// If there are any filters, the link must not match any of them | ||
isIgnored(link, ignores) || observer.observe(link); | ||
} | ||
}); | ||
}, { | ||
timeout: options.timeout || 2000 | ||
}); | ||
|
||
return function () { | ||
// wipe url list | ||
toPrefetch.clear(); | ||
// detach IO entries | ||
observer.disconnect(); | ||
}; | ||
} | ||
|
||
|
||
/** | ||
* Prefetch a given URL with an optional preferred fetch priority | ||
* @param {String} url - the URL to fetch | ||
* @param {Boolean} [isPriority] - if is "high" priority | ||
* @param {Object} [conn] - navigator.connection (internal) | ||
* @return {Object} a Promise | ||
*/ | ||
export function prefetch(url, isPriority, conn) { | ||
if (conn = navigator.connection) { | ||
// Don't prefetch if using 2G or if Save-Data is enabled. | ||
if (conn.saveData || /2g/.test(conn.effectiveType)) return; | ||
} | ||
|
||
// Dev must supply own catch() | ||
return Promise.all( | ||
[].concat(url).map(str => { | ||
if (!toPrefetch.has(str)) { | ||
// Add it now, regardless of its success | ||
// ~> so that we don't repeat broken links | ||
toPrefetch.add(str); | ||
|
||
return (isPriority ? priority : supported)( | ||
new URL(str, location.href).toString() | ||
); | ||
} | ||
}) | ||
); | ||
} |
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
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 @@ | ||
{ | ||
"/about": [{ | ||
"type": "style", | ||
"href": "/test/static/css/about.00ec0d84.chunk.css" | ||
}, { | ||
"type": "script", | ||
"href": "/test/static/js/about.921ebc84.chunk.js" | ||
}], | ||
"/blog": [{ | ||
"type": "style", | ||
"href": "/test/static/css/blog.2a8b6ab6.chunk.css" | ||
}, { | ||
"type": "script", | ||
"href": "/test/static/js/blog.1dcce8a6.chunk.js" | ||
}], | ||
"/": [{ | ||
"type": "style", | ||
"href": "/test/static/css/home.6d953f22.chunk.css" | ||
}, { | ||
"type": "script", | ||
"href": "/test/static/js/home.14835906.chunk.js" | ||
}, { | ||
"type": "image", | ||
"href": "/test/static/media/video.b9b6e9e1.svg" | ||
}], | ||
"/blog/:title": [{ | ||
"type": "style", | ||
"href": "/test/static/css/article.cb6f97df.chunk.css" | ||
}, { | ||
"type": "script", | ||
"href": "/test/static/js/article.cb6f97df.chunk.js" | ||
}], | ||
"*": [{ | ||
"type": "script", | ||
"href": "/test/static/js/6.7f61b1a1.chunk.js" | ||
}] | ||
} |
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,59 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<meta charset="utf-8"> | ||
<meta http-equiv="X-UA-Compatible" content="IE=edge"> | ||
<title>Prefetch: Chunk URL list</title> | ||
<meta name="viewport" content="width=device-width, initial-scale=1"> | ||
<link rel="stylesheet" media="screen" href="main.css"> | ||
<script src="https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver"></script> | ||
</head> | ||
|
||
<body> | ||
<a href="/">Home</a> | ||
<a href="/blog">Blog</a> | ||
<a href="/about">About</a> | ||
<section id="stuff"> | ||
<a href="main.css">CSS</a> | ||
</section> | ||
<a href="4.html" style="position:absolute;margin-top:900px;">Link 4</a> | ||
<script src="../dist/chunks/quicklink.umd.js"></script> | ||
<script src="../node_modules/route-manifest/dist/rmanifest.min.js"></script> | ||
<script> | ||
const __defaultAccessor = mix => { | ||
return (mix && mix.href) || mix || ''; | ||
}; | ||
|
||
const prefetchChunks = (entry, prefetchHandler, accessor = __defaultAccessor) => { | ||
const { files } = rmanifest(window._rmanifest_, entry.pathname); | ||
const chunkURLs = files.map(accessor).filter(Boolean); | ||
if (chunkURLs.length) { | ||
console.log('[prefetchChunks] chunkURLs => ', chunkURLs); | ||
prefetchHandler(chunkURLs); | ||
} else { | ||
// also prefetch regular links in-viewport | ||
console.log('[prefetchChunks] regularURL => ', entry.href); | ||
prefetchHandler(entry.href); | ||
} | ||
}; | ||
|
||
const listenAfterFetchingRmanifest = async () => { | ||
if (!window._rmanifest_) { | ||
await fetch('/test/rmanifest.json') | ||
.then(response => response.json()) | ||
.then(data => { | ||
// attach route manifest to global | ||
window._rmanifest_ = data; | ||
}); | ||
} | ||
|
||
quicklink.listen({ | ||
prefetchChunks, | ||
origins: [] | ||
}); | ||
}; | ||
|
||
listenAfterFetchingRmanifest(); | ||
</script> | ||
</body> | ||
</html> |
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