├── .babelrc.js
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitattributes
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── dependabot.yml
└── workflows
│ ├── ci.yml
│ ├── codeql.yml
│ ├── lint.yml
│ ├── site.yml
│ └── size-limit.yml
├── .gitignore
├── .size-limit.json
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── assets
└── images
│ └── logos
│ ├── banner-white-bg.png
│ ├── banner.png
│ └── quicklink.svg
├── demos
├── basic.html
├── hrefFn
│ ├── 2.html
│ ├── 2.json
│ └── hrefFn_demo.html
├── network-idle.html
├── network-idle.js
├── news-workbox
│ └── README.md
├── news
│ └── README.md
├── spa
│ └── README.md
└── sw.js
├── package-lock.json
├── package.json
├── site
├── .browserslistrc
├── .config
│ ├── configstore
│ │ └── update-notifier-pnpm.json
│ └── glitch-package-manager
├── .eleventy.js
├── .firebaserc
├── .gitignore
├── .stylelintignore
├── .stylelintrc.json
├── LICENSE
├── README.md
├── firebase.json
├── index.njk
├── package-lock.json
├── package.json
├── src
│ ├── _data
│ │ └── site.js
│ ├── _includes
│ │ ├── components
│ │ │ ├── chrome-extension.njk
│ │ │ ├── copy-snippet.njk
│ │ │ ├── download.njk
│ │ │ ├── github-fork.njk
│ │ │ ├── github-star.njk
│ │ │ ├── heading.njk
│ │ │ ├── installation.njk
│ │ │ ├── over-prefetching.njk
│ │ │ ├── react.njk
│ │ │ ├── trusted-by.njk
│ │ │ ├── usage.njk
│ │ │ ├── use-with.njk
│ │ │ ├── why-prefetch.njk
│ │ │ └── why-quicklink.njk
│ │ └── layouts
│ │ │ ├── base.njk
│ │ │ ├── favicons.njk
│ │ │ ├── font-icons
│ │ │ ├── chain-solid.svg
│ │ │ └── download-solid.svg
│ │ │ ├── footer.njk
│ │ │ ├── head.njk
│ │ │ ├── header.njk
│ │ │ ├── highlighted-section-wrapper.njk
│ │ │ ├── normal-section-wrapper.njk
│ │ │ └── social.njk
│ ├── api.njk
│ ├── approach.njk
│ ├── assets
│ │ ├── images
│ │ │ ├── graphs
│ │ │ │ ├── prefetch-improve-svgomg.svg
│ │ │ │ ├── prefetch-quicklink-svgomg.svg
│ │ │ │ ├── prefetching.svg
│ │ │ │ └── what-is-prefetch-svgomg.svg
│ │ │ ├── icons
│ │ │ │ ├── android-chrome-192x192.png
│ │ │ │ ├── android-chrome-512x512.png
│ │ │ │ ├── apple-touch-icon.png
│ │ │ │ ├── favicon-16x16.png
│ │ │ │ ├── favicon-32x32.png
│ │ │ │ ├── favicon.ico
│ │ │ │ └── safari-pinned-tab.svg
│ │ │ ├── logos
│ │ │ │ ├── angular.svg
│ │ │ │ ├── drupal.svg
│ │ │ │ ├── magento.svg
│ │ │ │ ├── quicklink.svg
│ │ │ │ ├── react.svg
│ │ │ │ ├── vue.svg
│ │ │ │ └── wordpress.svg
│ │ │ ├── og-image.png
│ │ │ ├── quicklink-used-logos
│ │ │ │ ├── barefootwine.ca.png
│ │ │ │ ├── hartfordwines.com.png
│ │ │ │ ├── hashnode.com.png
│ │ │ │ ├── matsuda.com.png
│ │ │ │ ├── newegg.com.png
│ │ │ │ ├── oakley.com.png
│ │ │ │ ├── paulrand.design.png
│ │ │ │ ├── quiply.com.png
│ │ │ │ ├── rayban.com.png
│ │ │ │ ├── saintagnes.org.png
│ │ │ │ ├── syfy.com.png
│ │ │ │ ├── vinyla.com.png
│ │ │ │ └── week.co.jp.png
│ │ │ └── screenshots
│ │ │ │ ├── devtools-optimized-1.png
│ │ │ │ ├── devtools-optimized-2.png
│ │ │ │ ├── devtools-unoptimized.png
│ │ │ │ ├── spa-devtools-optimized-1.png
│ │ │ │ ├── spa-devtools-optimized-2.png
│ │ │ │ ├── wpt-metrics-comparison.png
│ │ │ │ ├── wpt-video-comparison.gif
│ │ │ │ └── wpt-visual-comparison.png
│ │ ├── js
│ │ │ └── script.js
│ │ └── styles
│ │ │ ├── _copy-snippet.scss
│ │ │ ├── github-markdown.scss
│ │ │ ├── main.scss
│ │ │ └── vendor
│ │ │ ├── _github-markdown.scss
│ │ │ └── _prism.scss
│ ├── demo.njk
│ ├── index.njk
│ ├── measure.njk
│ ├── robots.njk
│ ├── site.webmanifest
│ └── sitemap.njk
└── watch.json
├── src
├── chunks.mjs
├── index.mjs
├── prefetch.mjs
├── prerender.mjs
├── react-chunks.js
└── request-idle-callback.mjs
├── test
├── fixtures
│ ├── 1.html
│ ├── 2.html
│ ├── 3.html
│ ├── 4.html
│ ├── index.html
│ ├── main.css
│ ├── rmanifest.json
│ ├── test-allow-origin-all.html
│ ├── test-allow-origin.html
│ ├── test-basic-usage.html
│ ├── test-custom-dom-source.html
│ ├── test-custom-href-function.html
│ ├── test-delay.html
│ ├── test-es-modules.html
│ ├── test-ignore-basic.html
│ ├── test-ignore-multiple.html
│ ├── test-limit.html
│ ├── test-node-list.html
│ ├── test-prefetch-chunks.html
│ ├── test-prefetch-duplicate-shared.html
│ ├── test-prefetch-duplicate.html
│ ├── test-prefetch-multiple.html
│ ├── test-prefetch-single.html
│ ├── test-prerender-andPrefetch.html
│ ├── test-prerender-only.html
│ ├── test-prerender-wrapper-multiple.html
│ ├── test-prerender-wrapper-single.html
│ ├── test-same-origin.html
│ ├── test-threshold.html
│ └── test-throttle.html
└── quicklink.spec.js
└── translations
└── zh-cn
└── README.md
/.babelrc.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | presets: [
5 | '@babel/preset-react',
6 | [
7 | '@babel/preset-env',
8 | {
9 | loose: true,
10 | bugfixes: true,
11 | },
12 | ],
13 | ],
14 | };
15 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # https://editorconfig.org/
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | end_of_line = lf
8 | indent_size = 2
9 | indent_style = space
10 | insert_final_newline = true
11 | trim_trailing_whitespace = true
12 |
13 | [*.md]
14 | trim_trailing_whitespace = false
15 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/*.min.js
2 | **/dist/
3 | **/build/
4 | **/vendor/
5 | # explicitly include dot js files
6 | !.*.js
7 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | env: {
5 | browser: true,
6 | es6: true,
7 | node: true,
8 | },
9 | parserOptions: {
10 | sourceType: 'script',
11 | ecmaVersion: 2017,
12 | },
13 | extends: [
14 | 'google',
15 | 'plugin:react/recommended',
16 | ],
17 | settings: {
18 | react: {
19 | version: 'detect',
20 | },
21 | },
22 | rules: {
23 | 'max-len': [
24 | 'warn',
25 | {
26 | // 130 on GitHub, 80 on npmjs.org for README.md code blocks
27 | code: 130,
28 | },
29 | ],
30 | 'arrow-parens': [
31 | 'error',
32 | 'as-needed',
33 | ],
34 | 'space-before-function-paren': [
35 | 'error',
36 | {
37 | anonymous: 'always',
38 | named: 'never',
39 | },
40 | ],
41 | 'no-negated-condition': 'warn',
42 | 'prefer-destructuring': [
43 | 'off',
44 | {
45 | object: true,
46 | array: false,
47 | },
48 | ],
49 | 'prefer-template': 'error',
50 | 'strict': 'error',
51 | 'spaced-comment': [
52 | 'error',
53 | 'always',
54 | {
55 | exceptions: [
56 | '/',
57 | ],
58 | },
59 | ],
60 | },
61 | overrides: [
62 | {
63 | files: [
64 | 'src/**',
65 | ],
66 | parserOptions: {
67 | sourceType: 'module',
68 | },
69 | },
70 | {
71 | files: [
72 | 'site/**',
73 | ],
74 | env: {
75 | node: false,
76 | },
77 | parserOptions: {
78 | sourceType: 'script',
79 | },
80 | rules: {
81 | 'require-jsdoc': 'off',
82 | 'strict': 'error',
83 | },
84 | },
85 | ],
86 | };
87 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Enforce Unix newlines
2 | * text=auto eol=lf
3 |
4 | *.njk linguist-language=js
5 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Something is not working as expected
4 | labels:
5 | ---
6 |
7 | **Before you start**
8 |
9 | Please take a look at the [FAQ](https://github.com/GoogleChromeLabs/quicklink/wiki/FAQ) as well as the already opened issues! If nothing fits your problem, go ahead and fill out the following template:
10 |
11 | **Describe the bug**
12 |
13 | A clear and concise description of what the bug is.
14 |
15 | **To Reproduce**
16 |
17 | Steps to reproduce the behavior.
18 |
19 | **Expected behavior**
20 |
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Version:**
24 |
25 | * OS w/ version: [e.g. iOS 12]
26 | * Browser w/ version [e.g. Chrome 75]
27 |
28 | **Additional context, screenshots, screencasts**
29 |
30 | Add any other context about the problem here.
31 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | labels:
5 | ---
6 |
7 | **Is your feature request related to a problem? Please describe.**
8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
9 |
10 | **Describe the solution you'd like**
11 | A clear and concise description of what you want to happen.
12 |
13 | **Additional context**
14 | Add any other context or screenshots about the feature request here.
15 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: monthly
7 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | workflow_dispatch:
9 |
10 | env:
11 | FORCE_COLOR: 2
12 | NODE: 22
13 |
14 | permissions:
15 | contents: read
16 |
17 | jobs:
18 | run:
19 | runs-on: ubuntu-latest
20 |
21 | steps:
22 | - name: Clone repository
23 | uses: actions/checkout@v4
24 | with:
25 | persist-credentials: false
26 |
27 | - name: Set up Node.js
28 | uses: actions/setup-node@v4
29 | with:
30 | node-version: ${{ env.NODE }}
31 | cache: npm
32 |
33 | - name: Disable AppArmor
34 | run: echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns
35 |
36 | - name: Install dependencies
37 | run: npm ci
38 |
39 | - name: Run tests
40 | run: npm test
41 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL"
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 | schedule:
11 | - cron: "0 0 * * 0"
12 | workflow_dispatch:
13 |
14 | jobs:
15 | analyze:
16 | name: Analyze
17 | runs-on: ubuntu-latest
18 | permissions:
19 | actions: read
20 | contents: read
21 | security-events: write
22 |
23 | steps:
24 | - name: Checkout repository
25 | uses: actions/checkout@v4
26 | with:
27 | persist-credentials: false
28 |
29 | - name: Initialize CodeQL
30 | uses: github/codeql-action/init@v3
31 | with:
32 | languages: "javascript"
33 | queries: +security-and-quality
34 |
35 | - name: Autobuild
36 | uses: github/codeql-action/autobuild@v3
37 |
38 | - name: Perform CodeQL Analysis
39 | uses: github/codeql-action/analyze@v3
40 | with:
41 | category: "/language:javascript"
42 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | workflow_dispatch:
9 |
10 | env:
11 | FORCE_COLOR: 2
12 | NODE: 22
13 |
14 | permissions:
15 | contents: read
16 |
17 | jobs:
18 | lint:
19 | runs-on: ubuntu-latest
20 |
21 | steps:
22 | - name: Clone repository
23 | uses: actions/checkout@v4
24 | with:
25 | persist-credentials: false
26 |
27 | - name: Set up Node.js
28 | uses: actions/setup-node@v4
29 | with:
30 | node-version: "${{ env.NODE }}"
31 | cache: npm
32 |
33 | - name: Install dependencies
34 | run: npm ci
35 |
36 | - name: Lint
37 | run: npm run lint
38 |
--------------------------------------------------------------------------------
/.github/workflows/site.yml:
--------------------------------------------------------------------------------
1 | name: Site
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | workflow_dispatch:
9 |
10 | env:
11 | FORCE_COLOR: 2
12 | NODE: 22
13 |
14 | permissions:
15 | contents: read
16 |
17 | defaults:
18 | run:
19 | working-directory: site
20 |
21 | jobs:
22 | build:
23 | runs-on: ubuntu-latest
24 |
25 | steps:
26 | - name: Clone repository
27 | uses: actions/checkout@v4
28 | with:
29 | persist-credentials: false
30 |
31 | - name: Set up Node.js
32 | uses: actions/setup-node@v4
33 | with:
34 | node-version: ${{ env.NODE }}
35 | cache: npm
36 |
37 | - name: Install dependencies
38 | run: npm ci
39 |
40 | - name: Run tests
41 | run: npm test
42 |
--------------------------------------------------------------------------------
/.github/workflows/size-limit.yml:
--------------------------------------------------------------------------------
1 | name: Size limit
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | workflow_dispatch:
9 |
10 | env:
11 | FORCE_COLOR: 2
12 | NODE: 22
13 |
14 | permissions:
15 | contents: read
16 |
17 | jobs:
18 | size-limit:
19 | runs-on: ubuntu-latest
20 |
21 | steps:
22 | - name: Clone repository
23 | uses: actions/checkout@v4
24 | with:
25 | persist-credentials: false
26 |
27 | - name: Set up Node.js
28 | uses: actions/setup-node@v4
29 | with:
30 | node-version: ${{ env.NODE }}
31 | cache: npm
32 |
33 | - name: Install dependencies
34 | run: npm ci
35 |
36 | - name: Build
37 | run: npm run build
38 |
39 | - name: Run size-limit
40 | run: npm run size
41 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | dist
8 | build
9 | .DS_Store
10 |
11 | # Runtime data
12 | pids
13 | *.pid
14 | *.seed
15 | *.pid.lock
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # Dependency directories
21 | node_modules/
22 |
23 | # Optional npm cache directory
24 | .npm
25 |
26 | # Optional eslint cache
27 | .eslintcache
28 |
29 | # Optional REPL history
30 | .node_repl_history
31 |
32 | # Output of 'npm pack'
33 | *.tgz
34 |
35 | # Yarn Integrity file
36 | .yarn-integrity
37 |
38 | # dotenv environment variables file
39 | .env
40 |
41 | # Local Netlify folder
42 | .netlify
43 |
--------------------------------------------------------------------------------
/.size-limit.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "path": "dist/quicklink.js",
4 | "limit": "2.5 kB",
5 | "gzip": true
6 | },
7 | {
8 | "path": "dist/quicklink.mjs",
9 | "limit": "2.5 kB",
10 | "gzip": true
11 | },
12 | {
13 | "path": "dist/quicklink.modern.mjs",
14 | "limit": "2 kB",
15 | "gzip": true
16 | },
17 | {
18 | "path": "dist/quicklink.umd.js",
19 | "limit": "2.5 kB",
20 | "gzip": true
21 | }
22 | ]
23 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to Contribute
2 |
3 | We'd love to accept your patches and contributions to this project. There are
4 | just a few small guidelines you need to follow.
5 |
6 | ## Contributor License Agreement
7 |
8 | Contributions to this project must be accompanied by a Contributor License
9 | Agreement. You (or your employer) retain the copyright to your contribution,
10 | this simply gives us permission to use and redistribute your contributions as
11 | part of the project. Head over to to see
12 | your current agreements on file or to sign a new one.
13 |
14 | You generally only need to submit a CLA once, so if you've already submitted one
15 | (even if it was for a different project), you probably don't need to do it
16 | again.
17 |
18 | ## Code reviews
19 |
20 | All submissions, including submissions by project members, require review. We
21 | use GitHub pull requests for this purpose. Consult [GitHub
22 | Help](https://help.github.com/articles/about-pull-requests/) for more
23 | information on using pull requests.
24 |
25 | ## Community Guidelines
26 |
27 | This project follows [Google's Open Source Community
28 | Guidelines](https://opensource.google.com/conduct/).
29 |
30 | ## Debugging Quicklink
31 |
32 | The [`test/fixtures/` folder](test/fixtures/) contains several test cases.
33 | Make sure to create a new test when building a new feature.
34 |
35 | Here's an example of how to debug the library by using one of these tests:
36 | [test/fixtures/test-basic-usage.html](test/fixtures/test-basic-usage.html).
37 |
38 | 1. Comment the following block of code at `test/fixtures/test-basic-usage.html`:
39 |
40 | ```js
41 |
42 |
45 | ```
46 |
47 | 2. Add the following snippet in its place, to import the module from its
48 | source file:
49 |
50 | ```js
51 |
55 | ```
56 |
57 | 3. Open [src/index.mjs](src/index.mjs) for edit and replace the following line:
58 |
59 | ```js
60 | import throttle from 'throttles';
61 | ```
62 |
63 | By:
64 |
65 | ```js
66 | import throttle from '../node_modules/throttles/dist/index.mjs'
67 | ```
68 |
69 | 4. Build the project: `npm run build`.
70 |
71 | 5. Start a local server: `npm start`. By default, this will start the local server at
72 | `https://localhost:8080`.
73 |
74 | 6. Open the file where the modifications where made:
75 | `http://localhost:8080/test/fixtures/test-basic-usage.html`.
76 |
77 | 7. Open Chrome DevTools and go the **Sources** tab.
78 |
79 | 8. Under `localhost:8080/src` you can find the unminified versions of the
80 | `Quicklink` files. Now you can use breakpoints and inspect variables to
81 | debug the library.
82 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/assets/images/logos/banner-white-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/quicklink/5f23ff26b445d66cf8b5b3672820e160f4869b41/assets/images/logos/banner-white-bg.png
--------------------------------------------------------------------------------
/assets/images/logos/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/quicklink/5f23ff26b445d66cf8b5b3672820e160f4869b41/assets/images/logos/banner.png
--------------------------------------------------------------------------------
/assets/images/logos/quicklink.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demos/basic.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Basic demo
7 |
8 |
9 |
10 |
11 |
12 |
13 |
Basic demo
14 |
Link 1
15 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
16 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
17 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
18 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
19 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
20 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
21 |
Link 2
22 |
23 |
24 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Eos, quos?
25 |
Link 3
26 |
29 |
30 |
33 |
34 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/demos/hrefFn/2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Prefetch experiments
7 |
8 |
9 |
10 |
11 |
12 | Page 2
13 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Soluta est sint assumenda corrupti minima aut, magnam
14 | totam beatae ullam ea iste voluptatum iusto expedita animi rem vitae rerum atque nemo!
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/demos/hrefFn/2.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "API Target to Prefetch Example",
3 | "description": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Soluta est sint assumenda corrupti minima aut, magnam totam beatae ullam ea iste voluptatum iusto expedita animi rem vitae rerum atque nemo!"
4 | }
5 |
--------------------------------------------------------------------------------
/demos/hrefFn/hrefFn_demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Basic demo
7 |
8 |
9 |
10 |
11 |
12 |
13 |
Basic demo
14 |
15 | Link 1
16 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
17 | tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
18 | quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
19 | consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
20 | cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
21 | proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
22 |
23 |
24 |
25 |
35 |
36 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/demos/network-idle.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | network-idle-callback demo
7 |
8 |
9 |
20 |
21 |
22 |
23 | This demo uses network-idle-callback to only
24 | prefetch when network activity goes idle in the current tab.
25 | Link 1
26 | Link 2
27 | Link 3
28 |
31 | Link 4
32 |
33 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/demos/network-idle.js:
--------------------------------------------------------------------------------
1 | // This script is a localized version of the upstream
2 | // https://github.com/pastelsky/network-idle-callback
3 | // which fixes issues with browser importing of the
4 | // above dependency. It is hopefully temporary.
5 |
6 | 'use strict';
7 |
8 | const DOMContentLoad = new Promise(resolve => {
9 | document.addEventListener('DOMContentLoaded', resolve);
10 | });
11 |
12 | navigator.serviceWorker.getRegistration()
13 | .then(registration => {
14 | if (!registration) {
15 | console.warn('`networkIdleCallback` was called before a service worker was registered.');
16 | console.warn('`networkIdleCallback` is ineffective without a working service worker');
17 | }
18 | });
19 |
20 | /**
21 | * networkIdleCallback works similar to requestIdleCallback,
22 | * detecting and notifying you when network activity goes idle
23 | * in your current tab.
24 | * @param {*} fn - A valid function
25 | * @param {*} options - An options object
26 | */
27 | function networkIdleCallback(fn, options = {timeout: 0}) {
28 | // Call the function immediately if required features are absent
29 | if (
30 | !('MessageChannel' in window) ||
31 | !('serviceWorker' in navigator) ||
32 | !navigator.serviceWorker.controller
33 | ) {
34 | DOMContentLoad.then(() => fn({didTimeout: false}));
35 | return;
36 | }
37 |
38 | const messageChannel = new MessageChannel();
39 | navigator.serviceWorker.controller
40 | .postMessage(
41 | 'NETWORK_IDLE_ENQUIRY',
42 | [messageChannel.port2],
43 | );
44 |
45 | const timeoutId = setTimeout(() => {
46 | const cbToPop = networkIdleCallback.__callbacks__
47 | .find(cb => cb.id === timeoutId);
48 |
49 | networkIdleCallback.__popCallback__(cbToPop, true);
50 | }, options.timeout);
51 |
52 | networkIdleCallback.__callbacks__.push({
53 | id: timeoutId,
54 | fn,
55 | timeout: options.timeout,
56 | });
57 |
58 | messageChannel.port1.addEventListener('message', handleMessage);
59 | messageChannel.port1.start();
60 | }
61 |
62 | /*
63 | function cancelNetworkIdleCallback(callbackId) {
64 | clearTimeout(callbackId);
65 |
66 | networkIdleCallback.__callbacks__ = networkIdleCallback.__callbacks__
67 | .find(cb => cb.id === callbackId);
68 | }
69 | */
70 |
71 | networkIdleCallback.__popCallback__ = (callback, didTimeout) => {
72 | DOMContentLoad.then(() => {
73 | const cbToPop = networkIdleCallback.__callbacks__
74 | .find(cb => cb.id === callback.id);
75 |
76 | if (cbToPop) {
77 | cbToPop.fn({didTimeout});
78 | clearTimeout(cbToPop.id);
79 | networkIdleCallback.__callbacks__ = networkIdleCallback.__callbacks__.filter(
80 | cb => cb.id !== callback.id);
81 | }
82 | });
83 | };
84 |
85 | networkIdleCallback.__callbacks__ = [];
86 |
87 | if ('serviceWorker' in navigator) {
88 | navigator.serviceWorker.addEventListener('message', handleMessage);
89 | }
90 |
91 | /**
92 | * Handle message passing
93 | * @param {*} event - A valid event
94 | */
95 | function handleMessage(event) {
96 | if (!event.data) {
97 | return;
98 | }
99 |
100 | switch (event.data) {
101 | case 'NETWORK_IDLE_ENQUIRY_RESULT_IDLE':
102 | case 'NETWORK_IDLE_CALLBACK':
103 | networkIdleCallback.__callbacks__.forEach(callback => {
104 | networkIdleCallback.__popCallback__(callback, false);
105 | });
106 | break;
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/demos/news-workbox/README.md:
--------------------------------------------------------------------------------
1 | # Demo: Quicklink usage with workbox
2 |
3 | A demo showing how to use Quicklink with Workbox for offline caching and links in the visible viewport.
4 |
5 | ## Glitch Source
6 |
7 | - [Link to Glitch App](https://anton-karlovskiy-quicklink-news-workbox.glitch.me)
8 | - [Link to Project on Glitch](https://glitch.com/~anton-karlovskiy-quicklink-news-workbox)
9 |
10 | ## Installation
11 |
12 | ```sh
13 | git clone https://api.glitch.com/git/anton-karlovskiy-quicklink-news-workbox
14 | npm install
15 | npm start
16 | npm run build
17 | ```
18 |
--------------------------------------------------------------------------------
/demos/news/README.md:
--------------------------------------------------------------------------------
1 | # Demo: Quicklink basic usage
2 |
3 | A demo showing how to use Quicklink on a simple multi-page website.
4 |
5 | ## Glitch Source
6 |
7 | - [Link to Glitch App](https://anton-karlovskiy-quicklink-news.glitch.me)
8 | - [Link to Project on Glitch](https://glitch.com/~anton-karlovskiy-quicklink-news)
9 |
10 | ## Installation
11 |
12 | ```sh
13 | git clone https://api.glitch.com/git/anton-karlovskiy-quicklink-news
14 | npm install
15 | npm start
16 | npm run build
17 | ```
18 |
--------------------------------------------------------------------------------
/demos/spa/README.md:
--------------------------------------------------------------------------------
1 | # Demo: Quicklink integration for create-react-app
2 |
3 | A demo showing how to use Quicklink with in a create-react-app site.
4 | To integrate your React SPA with Quicklink, follow the steps [here](https://github.com/GoogleChromeLabs/quicklink#single-page-apps-react).
5 |
6 | ## Glitch Source
7 |
8 | - [Link to Glitch App](https://create-react-app-quicklink.glitch.me/)
9 | - [Link to Project on Glitch](https://glitch.com/~create-react-app-quicklink)
10 |
11 | ## Installation
12 |
13 | ```sh
14 | git clone https://api.glitch.com/git/create-react-app-quicklink
15 | npm install
16 | npm start
17 | npm run build
18 | ```
19 |
--------------------------------------------------------------------------------
/demos/sw.js:
--------------------------------------------------------------------------------
1 | /* eslint-env serviceworker */
2 |
3 | 'use strict';
4 |
5 | importScripts('https://unpkg.com/network-idle-callback@1.0.1/lib/request-monitor.js');
6 |
7 | self.addEventListener('install', event => {
8 | console.log('[ServiceWorker] Installed');
9 | });
10 |
11 | self.addEventListener('activate', event => {
12 | console.log('[ServiceWorker] Activated');
13 | });
14 |
15 | self.addEventListener('fetch', event => {
16 | console.log('[ServiceWorker] Fetch', event.request.url);
17 | self.requestMonitor.listen(event);
18 |
19 | const promise = fetch(event.request)
20 | .then(response => {
21 | console.log('done', event.clientId);
22 | self.requestMonitor.unlisten(event);
23 | return response;
24 | })
25 | .catch(error => {
26 | console.log('error');
27 | self.requestMonitor.unlisten(error);
28 | });
29 |
30 | event.respondWith(promise);
31 | });
32 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "quicklink",
3 | "version": "3.0.1",
4 | "description": "Faster subsequent page-loads by prefetching in-viewport links during idle time",
5 | "repository": {
6 | "type": "git",
7 | "url": "git+https://github.com/GoogleChromeLabs/quicklink.git"
8 | },
9 | "homepage": "https://getquick.link/",
10 | "bugs": {
11 | "url": "https://github.com/GoogleChromeLabs/quicklink/issues"
12 | },
13 | "author": "addyosmani ",
14 | "license": "Apache-2.0",
15 | "main": "dist/quicklink.js",
16 | "module": "dist/quicklink.mjs",
17 | "umd:main": "dist/quicklink.umd.js",
18 | "unpkg": "dist/quicklink.umd.js",
19 | "files": [
20 | "dist"
21 | ],
22 | "scripts": {
23 | "lint": "eslint --report-unused-disable-directives --ext .js,.mjs .",
24 | "lint-fix": "npm run lint -- --fix",
25 | "fix": "npm run lint -- --fix",
26 | "start": "sirv --dev --no-clear --no-logs --host 127.0.0.1 --port 8080",
27 | "uvu": "uvu test",
28 | "test": "npm-run-all build-all --parallel --race start uvu",
29 | "build": "microbundle src/index.mjs --no-sourcemap --external none",
30 | "build-plugin": "microbundle src/chunks.mjs --no-sourcemap --external none -o dist/react",
31 | "build-react-chunks": "babel src/react-chunks.js --out-file dist/react/hoc.js",
32 | "build-all": "npm-run-all --parallel build build-plugin build-react-chunks",
33 | "prepublishonly": "npm run build-all",
34 | "size": "size-limit",
35 | "changelog": "npm run conventional-changelog -i CHANGELOG.md -s -r 0",
36 | "release": "cross-env-shell \"npm run build-all && git commit -am $npm_package_version && git tag $npm_package_version && git push --follow-tags\""
37 | },
38 | "keywords": [
39 | "prefetch",
40 | "performance",
41 | "fetch",
42 | "intersectionobserver",
43 | "background",
44 | "speed"
45 | ],
46 | "dependencies": {
47 | "route-manifest": "^1.0.0",
48 | "throttles": "^1.0.1"
49 | },
50 | "peerDependencies": {
51 | "react": "^16.8.0 || ^17 || ^18 || ^19",
52 | "react-dom": "^16.8.0 || ^17 || ^18 || ^19"
53 | },
54 | "peerDependenciesMeta": {
55 | "react": {
56 | "optional": true
57 | },
58 | "react-dom": {
59 | "optional": true
60 | }
61 | },
62 | "devDependencies": {
63 | "@babel/cli": "^7.27.2",
64 | "@babel/core": "^7.27.4",
65 | "@babel/preset-env": "^7.27.2",
66 | "@babel/preset-react": "^7.27.1",
67 | "@size-limit/file": "^11.2.0",
68 | "conventional-changelog-cli": "^5.0.0",
69 | "cross-env": "^7.0.3",
70 | "eslint": "^8.57.1",
71 | "eslint-config-google": "^0.14.0",
72 | "eslint-plugin-react": "^7.37.5",
73 | "microbundle": "^0.15.1",
74 | "npm-run-all2": "^8.0.4",
75 | "puppeteer": "^24.10.0",
76 | "react": "^19.1.0",
77 | "react-dom": "^19.1.0",
78 | "sirv-cli": "^3.0.1",
79 | "size-limit": "^11.2.0",
80 | "uvu": "^0.5.6"
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/site/.browserslistrc:
--------------------------------------------------------------------------------
1 | # https://github.com/browserslist/browserslist#readme
2 |
3 | defaults
4 |
--------------------------------------------------------------------------------
/site/.config/configstore/update-notifier-pnpm.json:
--------------------------------------------------------------------------------
1 | {
2 | "optOut": false,
3 | "lastUpdateCheck": 1574808403652,
4 | "update": {
5 | "latest": "4.3.3",
6 | "current": "2.25.5",
7 | "type": "major",
8 | "name": "pnpm"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/site/.config/glitch-package-manager:
--------------------------------------------------------------------------------
1 | pnpm
2 |
--------------------------------------------------------------------------------
/site/.eleventy.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | /* eslint-disable new-cap */
4 |
5 | 'use strict';
6 |
7 | const {EleventyHtmlBasePlugin: htmlBasePlugin} = require('@11ty/eleventy');
8 | const navigationPlugin = require('@11ty/eleventy-navigation');
9 | const syntaxHighlight = require('@11ty/eleventy-plugin-syntaxhighlight');
10 | const autoprefixer = require('autoprefixer');
11 | const htmlminifier = require('html-minifier-terser');
12 | const markdownIt = require('markdown-it');
13 | const pluginRev = require('eleventy-plugin-rev');
14 | const postcss = require('postcss');
15 | const sass = require('eleventy-sass');
16 |
17 | const IS_PRODUCTION = process.env.NODE_ENV === 'production';
18 |
19 | const htmlminifierConfig = {
20 | collapseBooleanAttributes: true,
21 | collapseWhitespace: true,
22 | conservativeCollapse: false,
23 | decodeEntities: false,
24 | minifyCSS: true,
25 | minifyJS: true,
26 | minifyURLs: false,
27 | removeAttributeQuotes: true,
28 | removeComments: true,
29 | removeEmptyAttributes: false,
30 | removeOptionalAttributes: true,
31 | removeOptionalTags: true,
32 | removeRedundantAttributes: true,
33 | removeScriptTypeAttributes: true,
34 | removeStyleLinkTypeAttributes: true,
35 | removeTagWhitespace: false,
36 | sortAttributes: true,
37 | sortClassName: true,
38 | };
39 |
40 | module.exports = eleventyConfig => {
41 | eleventyConfig.addPlugin(htmlBasePlugin, {baseHref: '/'});
42 | eleventyConfig.addPlugin(navigationPlugin);
43 | eleventyConfig.addPlugin(syntaxHighlight);
44 | eleventyConfig.addPlugin(pluginRev);
45 | eleventyConfig.addPlugin(sass, [
46 | {
47 | postcss: postcss([autoprefixer]),
48 | sass: {
49 | style: 'expanded',
50 | sourceMap: true,
51 | },
52 | rev: false,
53 | },
54 | {
55 | sass: {
56 | style: 'compressed',
57 | sourceMap: false,
58 | },
59 | rev: true,
60 | when: [{NODE_ENV: 'production'}],
61 | },
62 | ]);
63 |
64 | eleventyConfig.addPassthroughCopy('src/assets/images');
65 | eleventyConfig.addPassthroughCopy('src/assets/js');
66 | eleventyConfig.addPassthroughCopy('src/site.webmanifest');
67 |
68 | eleventyConfig.addNunjucksFilter('markdown', string => {
69 | const md = new markdownIt();
70 | return md.render(string);
71 | });
72 |
73 | eleventyConfig.addPairedShortcode('markdownConvert', content => {
74 | const md = new markdownIt();
75 | return md.render(content);
76 | });
77 |
78 | eleventyConfig.addNunjucksShortcode('sectionTitle', title => {
79 | const md = new markdownIt();
80 | return md.render(`## ${title}`);
81 | });
82 |
83 | eleventyConfig.addTransform('htmlminifier', (content, outputPath) => {
84 | if (!outputPath.endsWith('.html')) return content;
85 | if (!IS_PRODUCTION) return content;
86 |
87 | return htmlminifier.minify(content, htmlminifierConfig);
88 | });
89 |
90 | return {
91 | dir: {
92 | input: 'src',
93 | output: 'build',
94 | },
95 | };
96 | };
97 |
--------------------------------------------------------------------------------
/site/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "quicklink-6a87b"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/site/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Compiled binary addons (https://nodejs.org/api/addons.html)
9 | build/
10 |
11 | # Dependency directories
12 | node_modules/
13 |
14 | /_datacache
15 |
--------------------------------------------------------------------------------
/site/.stylelintignore:
--------------------------------------------------------------------------------
1 | **/*.min.css
2 | **/*.min.scss
3 | **/dist/
4 | **/vendor/
5 | /build/
6 |
--------------------------------------------------------------------------------
/site/.stylelintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "stylelint-config-twbs-bootstrap"
4 | ],
5 | "reportInvalidScopeDisables": true,
6 | "reportNeedlessDisables": true,
7 | "rules": {
8 | "order/properties-order": null,
9 | "selector-class-pattern": null,
10 | "selector-no-qualifying-type": null
11 | },
12 | "overrides": [
13 | {
14 | "files": "**/*.scss",
15 | "rules": {
16 | "scss/selector-no-union-class-name": true
17 | }
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/site/README.md:
--------------------------------------------------------------------------------
1 | # eleventy-quicklink-website
2 |
3 | Our canonical site source for Quicklink. This project uses [Eleventy](https://www.11ty.io/) as a static site generator. Templating uses [Nunjucks](https://mozilla.github.io/nunjucks/).
4 |
5 | ## Installation
6 |
7 | ```sh
8 | git clone git@github.com:googlechromelabs/quicklink.git
9 | cd site
10 | npm install
11 | ```
12 |
13 | ## Commands
14 |
15 | | Command | Description |
16 | | --------------- | ------------------------------------------------------------- |
17 | | `npm start` | Start a development server and watch for updates |
18 | | `npm run build` | Build templates, data, CSS, and JS for production environment |
19 |
--------------------------------------------------------------------------------
/site/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "hosting": {
3 | "public": "build",
4 | "site": "getquicklink",
5 | "ignore": [
6 | "firebase.json",
7 | "**/.*",
8 | "**/node_modules/**"
9 | ],
10 | "headers": [
11 | {
12 | "source": "/service-worker.js",
13 | "headers": [
14 | {
15 | "key": "Cache-Control",
16 | "value": "no-cache"
17 | }
18 | ]
19 | },
20 | {
21 | "source": "**/*.@(js)",
22 | "headers": [
23 | {
24 | "key": "Cache-Control",
25 | "value": "max-age=31536000"
26 | }
27 | ]
28 | },
29 | {
30 | "source": "**/*.@(css)",
31 | "headers": [
32 | {
33 | "key": "Cache-Control",
34 | "value": "max-age=31536000"
35 | }
36 | ]
37 | },
38 | {
39 | "source": "**/*.@(eot|otf|ttf|ttc|woff)",
40 | "headers": [
41 | {
42 | "key": "Access-Control-Allow-Origin",
43 | "value": "*"
44 | }
45 | ]
46 | },
47 | {
48 | "source": "**/*.@(jpg|jpeg|gif|png|svg)",
49 | "headers": [
50 | {
51 | "key": "Cache-Control",
52 | "value": "max-age=604800"
53 | }
54 | ]
55 | },
56 | {
57 | "source": "404.html",
58 | "headers": [
59 | {
60 | "key": "Cache-Control",
61 | "value": "max-age=300"
62 | }
63 | ]
64 | }
65 | ]
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/site/index.njk:
--------------------------------------------------------------------------------
1 | ---
2 | title: Quicklink
3 | layout: layouts/base.njk
4 | description: Faster subsequent page-loads by prefetching in-viewport links during idle time.
5 | ---
6 |
7 | {% include "components/heading.njk" %}
8 |
9 | {% include "components/why-quicklink.njk" %}
10 |
11 | {% include "components/copy-snippet.njk" %}
12 |
13 | {% include "components/download.njk" %}
14 |
15 | {% include "components/trusted-by.njk" %}
16 |
17 | {% include "components/installation.njk" %}
18 |
19 | {% include "components/usage.njk" %}
20 |
21 | {% include "components/over-prefetching.njk" %}
22 |
23 | {% include "components/react.njk" %}
24 |
25 | {% include "components/chrome-extension.njk" %}
26 |
27 | {% include "components/why-prefetch.njk" %}
28 |
29 | {% include "components/use-with.njk" %}
30 |
--------------------------------------------------------------------------------
/site/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "eleventy-quicklink-website",
3 | "description": "",
4 | "version": "1.0.0",
5 | "private": true,
6 | "scripts": {
7 | "build": "rimraf build && cross-env NODE_ENV=production eleventy",
8 | "start": "rimraf build && cross-env NODE_ENV=development eleventy --serve",
9 | "deploy": "firebase deploy --project=quicklink-6a87b",
10 | "lint": "stylelint src/assets/styles",
11 | "test": "npm run lint && npm run build"
12 | },
13 | "license": "Apache-2.0",
14 | "devDependencies": {
15 | "@11ty/eleventy": "^2.0.1",
16 | "@11ty/eleventy-navigation": "^1.0.4",
17 | "@11ty/eleventy-plugin-syntaxhighlight": "^5.0.1",
18 | "autoprefixer": "^10.4.21",
19 | "cross-env": "^7.0.3",
20 | "eleventy-plugin-rev": "^2.0.0",
21 | "eleventy-sass": "^2.2.6",
22 | "html-minifier-terser": "^7.2.0",
23 | "markdown-it": "^14.1.0",
24 | "postcss": "^8.5.4",
25 | "rimraf": "^6.0.1",
26 | "stylelint": "^16.20.0",
27 | "stylelint-config-twbs-bootstrap": "^16.0.0"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/site/src/_data/site.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 |
3 | 'use strict';
4 |
5 | const process = require('node:process');
6 |
7 | const IS_NETLIFY = process.env.NETLIFY === 'true';
8 |
9 | module.exports = () => {
10 | return {
11 | title: 'Quicklink',
12 | subtitle: 'Instant next-page navigations',
13 | description: 'Faster subsequent page-loads by prefetching in-viewport links during idle time.',
14 | socialImage: '/assets/images/og-image.png',
15 | // If we are on Netlify, use the `DEPLOY_PRIME_URL` environment variable
16 | url: IS_NETLIFY ? process.env.DEPLOY_PRIME_URL : 'https://getquick.link',
17 | isNetlify: IS_NETLIFY,
18 | quicklinkGithubURL: 'https://github.com/GoogleChromeLabs/quicklink',
19 | quicklinkVersion: '3.0.1',
20 | quicklinkSizeLimit: '1KB',
21 | bottomResource: {
22 | caption: 'View source on GitHub',
23 | },
24 | useWithFrameworks: [
25 | {
26 | title: 'wordpress',
27 | logoFileName: 'wordpress.svg',
28 | url: 'https://wordpress.org/plugins/quicklink/',
29 | },
30 | {
31 | title: 'drupal',
32 | logoFileName: 'drupal.svg',
33 | url: 'https://www.drupal.org/project/quicklink/',
34 | },
35 | {
36 | title: 'magento',
37 | logoFileName: 'magento.svg',
38 | url: 'https://marketplace.magento.com/rafaelcg-magento2-quicklink.html',
39 | },
40 | {
41 | title: 'react',
42 | logoFileName: 'react.svg',
43 | url: 'https://github.com/HOUCe/react-quicklink-component/',
44 | },
45 | {
46 | title: 'angular',
47 | logoFileName: 'angular.svg',
48 | url: 'https://github.com/mgechev/ngx-quicklink/',
49 | },
50 | {
51 | title: 'vue',
52 | logoFileName: 'vue.svg',
53 | url: 'https://nuxtjs.org/api/components-nuxt-link/',
54 | },
55 | ],
56 | trustedByLogos: [
57 | {
58 | websiteUrl: 'https://www.ray-ban.com/',
59 | logoFileName: 'rayban.com.png',
60 | companyName: 'Ray-Ban',
61 | },
62 | {
63 | websiteUrl: 'https://www.oakley.com/',
64 | logoFileName: 'oakley.com.png',
65 | companyName: 'Oakley',
66 | },
67 | {
68 | websiteUrl: 'https://www.syfy.com/',
69 | logoFileName: 'syfy.com.png',
70 | companyName: 'SYFY WIRE',
71 | },
72 | {
73 | websiteUrl: 'https://www.newegg.com/',
74 | logoFileName: 'newegg.com.png',
75 | companyName: 'Newegg',
76 | },
77 | {
78 | websiteUrl: 'https://www.barefootwine.ca/',
79 | logoFileName: 'barefootwine.ca.png',
80 | companyName: 'BAREFOOT',
81 | },
82 | {
83 | websiteUrl: 'https://hashnode.com/',
84 | logoFileName: 'hashnode.com.png',
85 | companyName: 'Hashnode',
86 | },
87 | {
88 | websiteUrl: 'https://www.hartfordwines.com/',
89 | logoFileName: 'hartfordwines.com.png',
90 | companyName: 'HARTFORD',
91 | },
92 | {
93 | websiteUrl: 'https://vinyla.com/',
94 | logoFileName: 'vinyla.com.png',
95 | companyName: 'Vinyla',
96 | },
97 | {
98 | websiteUrl: 'https://www.matsuda.com/',
99 | logoFileName: 'matsuda.com.png',
100 | companyName: 'MATSUDA',
101 | },
102 | {
103 | websiteUrl: 'https://paulrand.design/',
104 | logoFileName: 'paulrand.design.png',
105 | companyName: 'Paul Rand',
106 | },
107 | {
108 | websiteUrl: 'http://www.week.co.jp/',
109 | logoFileName: 'week.co.jp.png',
110 | companyName: 'Komachi',
111 | },
112 | {
113 | websiteUrl: 'https://www.quiply.com/',
114 | logoFileName: 'quiply.com.png',
115 | companyName: 'Quiply',
116 | },
117 | {
118 | websiteUrl: 'https://saintagnes.org/',
119 | logoFileName: 'saintagnes.org.png',
120 | companyName: 'St Agnes',
121 | },
122 | ],
123 | };
124 | };
125 |
--------------------------------------------------------------------------------
/site/src/_includes/components/chrome-extension.njk:
--------------------------------------------------------------------------------
1 | {% extends "layouts/normal-section-wrapper.njk" %}
2 | {% block section %}
3 | {% sectionTitle "Chrome Extension" %}
4 | {{ "We've developed a [Chrome extension](https://chrome.google.com/webstore/detail/quicklink-chrome-extensio/epmplkdcjhgigmnjmjibilpmekhgkbeg) that injects and initializes `Quicklink` in every site you visit." | markdown | safe }}
5 | {{ "The extension can be used with the following purposes:" | markdown | safe }}
6 |
7 | {{ "* To navigate the web faster." | markdown | safe }}
8 | {{ "* To estimate the potential impact of `Quicklink` on a site, before implementing it (see [impact measurement guide](/measure))." | markdown | safe }}
9 |
10 | {{ "The extension comes with a default set of URL patterns [to ignore](https://github.com/GoogleChromeLabs/quicklink#optionsignores) (e.g. signin, logout, etc). You can add more patterns, by clicking on the extension icon, and picking 'Options' from the drop-down menu." | markdown | safe }}
11 | {{ "The code of the extension can be found at [this repository](https://github.com/demianrenzulli/quicklink-chrome-extension). Contributions are welcomed!" | markdown | safe }}
12 |
13 | {% endblock %}
--------------------------------------------------------------------------------
/site/src/_includes/components/copy-snippet.njk:
--------------------------------------------------------------------------------
1 | {% extends "layouts/normal-section-wrapper.njk" %}
2 | {% block section %}
3 |
27 | {% endblock %}
28 |
--------------------------------------------------------------------------------
/site/src/_includes/components/download.njk:
--------------------------------------------------------------------------------
1 | {% extends "layouts/normal-section-wrapper.njk" %}
2 | {% block section %}
3 | {% sectionTitle "Download" %}
4 |
14 | {% endblock %}
15 |
--------------------------------------------------------------------------------
/site/src/_includes/components/github-fork.njk:
--------------------------------------------------------------------------------
1 | Fork
2 |
--------------------------------------------------------------------------------
/site/src/_includes/components/github-star.njk:
--------------------------------------------------------------------------------
1 | Star
2 |
--------------------------------------------------------------------------------
/site/src/_includes/components/heading.njk:
--------------------------------------------------------------------------------
1 | {% extends "layouts/highlighted-section-wrapper.njk" %}
2 | {% block section %}
3 |
4 | "We implemented Quicklink and saw a 50% increase in conversions and 4x faster page transitions" - NewEgg
5 |
6 | {% endblock %}
7 |
--------------------------------------------------------------------------------
/site/src/_includes/components/installation.njk:
--------------------------------------------------------------------------------
1 | {% extends "layouts/normal-section-wrapper.njk" %}
2 | {% block section %}
3 | {% sectionTitle "Installation" %}
4 | {{ "For use with [Node.js](https://nodejs.org/) and [npm](https://www.npmjs.com/):" | markdown | safe }}
5 | {% highlight "bash" %}
6 | npm install quicklink
7 | {% endhighlight %}
8 | {{ "You can also grab `quicklink` from [unpkg.com/quicklink](https://unpkg.com/quicklink)." | markdown | safe }}
9 | {% endblock %}
10 |
--------------------------------------------------------------------------------
/site/src/_includes/components/over-prefetching.njk:
--------------------------------------------------------------------------------
1 | {% extends "layouts/normal-section-wrapper.njk" %}
2 | {% block section %}
3 | {% sectionTitle "Concerned about over-prefetching? We've got you covered" %}
4 | {{ "By default `quicklink` observes all in-viewport links in `document.body`. There are different ways of telling `quicklink` to limit the number of links to prefetch." | markdown | safe }}
5 |
6 | {{ "The most common approach is passing different `options` to configure prefetching when calling `quicklink.listen()`:" | markdown | safe }}
7 |
8 | {{ "* Indicating a specific DOM element to observe, with the `options.el` parameter:" | markdown | safe }}
9 |
10 | {% highlight "js" %}
11 | quicklink.listen({
12 | el: document.getElementById('content')
13 | });
14 | {% endhighlight %}
15 |
16 | {{ "* Passing an `options.limit` parameter, indicating the total number of requests that can be prefetched while observing the `options.el` container:" | markdown | safe }}
17 |
18 | {% highlight "js" %}
19 | quicklink.listen({
20 | limit: 5
21 | });
22 | {% endhighlight %}
23 |
24 | {{ "* Using `options.throttle`, to establish a concurrency limit for simultaneous requests while observing the `options.el` container:" | markdown | safe }}
25 |
26 | {% highlight "js" %}
27 | quicklink.listen({
28 | throttle: 2
29 | });
30 | {% endhighlight %}
31 |
32 | {{ "If none of these configuration options suits your needs, you can call `quicklink.prefetch()`, passing a single URL or an array of URLs to prefetch. Invoking `quicklink` this way, bypasses the `Intersection Observer` logic, giving you full control on the prefetch requests to be made:" | markdown | safe }}
33 |
34 | {% highlight "js" %}
35 | quicklink.prefetch(['2.html', '3.html', '4.js']);
36 | {% endhighlight %}
37 |
38 | {% endblock %}
--------------------------------------------------------------------------------
/site/src/_includes/components/react.njk:
--------------------------------------------------------------------------------
1 | {% extends "layouts/normal-section-wrapper.njk" %}
2 | {% block section %}
3 | {% sectionTitle "Single page apps (React)" %}
4 | {% markdownConvert %}
5 |
6 | ### Installation
7 |
8 | First, install the packages with [Node.js](https://nodejs.org/) and [npm](https://www.npmjs.com/):
9 | {% endmarkdownConvert %}
10 |
11 | {% highlight "bash" %}
12 | npm install quicklink webpack-route-manifest --save-dev
13 | {% endhighlight %}
14 |
15 | {% markdownConvert %}
16 | Then, configure Webpack route manifest into your project, as explained [here](https://github.com/lukeed/webpack-route-manifest).
17 | This will generate a map of routes and chunks called `rmanifest.json`. It can be obtained at:
18 |
19 | * URL: `site_url/rmanifest.json`
20 | * Window object: `window.__rmanifest`
21 |
22 | ### Usage
23 |
24 | Import `quicklink` React HOC where want to add prefetching functionality.
25 | Wrap your routes with the `withQuicklink()` HOC.
26 |
27 | Example:
28 | {% endmarkdownConvert %}
29 |
30 | {% highlight "jsx" %}
31 | import {withQuicklink} from 'quicklink/dist/react/hoc.js';
32 |
33 | const options = {
34 | origins: [],
35 | };
36 |
37 | Loading...}>
38 |
39 |
40 |
41 |
42 | ;
43 | {% endhighlight %}
44 |
45 | {% endblock %}
46 |
--------------------------------------------------------------------------------
/site/src/_includes/components/trusted-by.njk:
--------------------------------------------------------------------------------
1 | {% extends "layouts/normal-section-wrapper.njk" %}
2 | {% block section %}
3 | {% sectionTitle "Trusted by" %}
4 |
13 | {% endblock %}
14 |
--------------------------------------------------------------------------------
/site/src/_includes/components/usage.njk:
--------------------------------------------------------------------------------
1 | {% extends "layouts/normal-section-wrapper.njk" %}
2 | {% block section %}
3 | {% sectionTitle "Usage" %}
4 | {{ "Once initialized, `quicklink` will automatically prefetch URLs for links that are in-viewport during idle time." | markdown | safe }}
5 |
6 | {{ "Quickstart:" | markdown | safe }}
7 |
8 | {% highlight "html" %}
9 |
10 |
11 |
12 |
15 | {% endhighlight %}
16 |
17 | {{ "For example, you can initialize after the `load` event fires:" | markdown | safe }}
18 | {% highlight "html" %}
19 |
24 | {% endhighlight %}
25 |
26 | {{ "ES Module import:" | markdown | safe }}
27 | {% highlight "js" %}
28 | import {listen} from 'quicklink/dist/quicklink.mjs';
29 | listen();
30 | {% endhighlight %}
31 |
32 | {{ "The above options are best for multi-page sites. Single-page apps have a few options available for using quicklink with a router:" | markdown | safe }}
33 | {{ "* Call `quicklink.listen()` once a navigation to a new route has completed" | markdown | safe }}
34 | {{ "* Call `quicklink.listen()` against a specific DOM element / component" | markdown | safe }}
35 | {{ "* Call `quicklink.prefetch()` with a custom set of URLs to prefetch" | markdown | safe }}
36 | {% endblock %}
37 |
--------------------------------------------------------------------------------
/site/src/_includes/components/use-with.njk:
--------------------------------------------------------------------------------
1 | {% extends "layouts/normal-section-wrapper.njk" %}
2 | {% block section %}
3 | {% sectionTitle "Use with" %}
4 |
5 |
6 | {%- for framework in site.useWithFrameworks %}
7 |
8 |
9 |
10 | {%- endfor %}
11 |
12 |
13 | {% endblock %}
14 |
--------------------------------------------------------------------------------
/site/src/_includes/components/why-prefetch.njk:
--------------------------------------------------------------------------------
1 | {% extends "layouts/normal-section-wrapper.njk" %}
2 | {% block section %}
3 | {% sectionTitle "Why Quicklink\'s prefetch?" %}
4 |
5 |
6 |
7 |
8 | {% endblock %}
9 |
--------------------------------------------------------------------------------
/site/src/_includes/components/why-quicklink.njk:
--------------------------------------------------------------------------------
1 | {% extends "layouts/normal-section-wrapper.njk" %}
2 | {% block section %}
3 |
4 | Why Quicklink?
5 |
6 |
7 | {% set githubLarge = false %}
8 | {% include "components/github-star.njk" %}
9 | {% include "components/github-fork.njk" %}
10 |
11 |
12 |
13 | {% set githubLarge = true %}
14 | {% include "components/github-star.njk" %}
15 | {% include "components/github-fork.njk" %}
16 |
17 |
18 | {{ "This project aims to be a drop-in solution for sites to prefetch links based on what is in the user's viewport. It also aims to be small (**< 1KB minified/gzipped**)." | markdown | safe }}
19 | {% endblock %}
20 |
--------------------------------------------------------------------------------
/site/src/_includes/layouts/base.njk:
--------------------------------------------------------------------------------
1 |
2 |
3 | {% include "layouts/head.njk" %}
4 |
5 |
6 | {% include "layouts/header.njk" %}
7 |
8 | {{ content | safe }}
9 | {% include "layouts/footer.njk" %}
10 | ↑
11 |
12 |
13 |
14 |
19 |
20 |
21 |
22 |
23 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/site/src/_includes/layouts/favicons.njk:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/site/src/_includes/layouts/font-icons/chain-solid.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/site/src/_includes/layouts/font-icons/download-solid.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/site/src/_includes/layouts/footer.njk:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/site/src/_includes/layouts/head.njk:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ title + " | " + site.title if title else site.title }}
4 |
5 |
6 |
7 |
8 | {{ extra_head | safe }}
9 |
10 | {% include "layouts/favicons.njk" -%}
11 | {% include "layouts/social.njk" -%}
12 |
13 |
14 |
15 |
16 |
17 | {# TODO: opt for theme and tweak the background color by avoiding github markdown #}
18 |
19 |
--------------------------------------------------------------------------------
/site/src/_includes/layouts/header.njk:
--------------------------------------------------------------------------------
1 |
23 |
--------------------------------------------------------------------------------
/site/src/_includes/layouts/highlighted-section-wrapper.njk:
--------------------------------------------------------------------------------
1 |
2 | {% block section %}
3 | {% endblock %}
4 |
5 |
--------------------------------------------------------------------------------
/site/src/_includes/layouts/normal-section-wrapper.njk:
--------------------------------------------------------------------------------
1 |
2 | {% block section %}
3 | {% endblock %}
4 |
5 |
--------------------------------------------------------------------------------
/site/src/_includes/layouts/social.njk:
--------------------------------------------------------------------------------
1 | {% set socialImagePath = site.url + site.socialImage -%}
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/site/src/approach.njk:
--------------------------------------------------------------------------------
1 | ---
2 | title: Approach
3 | layout: layouts/base.njk
4 | description: Faster subsequent page-loads by prefetching in-viewport links during idle time.
5 | eleventyNavigation:
6 | key: Approach
7 | order: 2
8 | sections:
9 | howItWorks:
10 | title: "How it works"
11 | summary: "Quicklink attempts to make navigations to subsequent pages load faster. It:"
12 | ---
13 |
14 | {% extends "layouts/normal-section-wrapper.njk" %}
15 | {% block section %}
16 | {% markdownConvert %}
17 |
18 | ## How it works
19 |
20 | 
21 |
22 | Quicklink attempts to make navigations to subsequent pages load faster. It:
23 |
24 | * **Detects links within the viewport** (using [Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API))
25 | * **Waits until the browser is idle** (using [requestIdleCallback](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback))
26 | * **Checks if the user isn't on a slow connection** (using `navigator.connection.effectiveType`) or has data-saver enabled (using `navigator.connection.saveData`)
27 | * **Prefetches URLs to the links** (using [` `](https://www.w3.org/TR/resource-hints/#prefetch) or XHR). Provides some control over the request priority (can switch to `fetch()` if supported).
28 |
29 | 
30 |
31 | ## Future: Double-keyed HTTP Cache
32 |
33 | Most Quicklink users use the library to prefetch links on the same origin. Some users do however care about cross-origin prefetches and this section outlines some relevant notes on double-keyed caching that may impact you.
34 |
35 | Modern browsers are shifting towards a double-keyed HTTP cache. This partitions the HTTP cache roughly by the top-frame-origin that a request is made from. This breaks cross-origin resource prefetching, whose goal is to prefetch cross-origin resources and store them in the HTTP cache for later re-use. This [limits cache timing attacks](https://github.com/xsleaks/xsleaks/wiki/Browser-Side-Channels#cache-and-error-events) but does limit cross-origin prefetching from being effective.
36 |
37 | Chrome [will](https://groups.google.com/a/chromium.org/g/blink-dev/c/bSMOY-evrV4/m/qT0gCByxBAAJ) allow cross-origin prefetched resources in the HTTP cache to be re-used for top-level navigations, which should be safe because by navigating, the user is explicitly sharing their interest with the destination site.
38 |
39 | What this means for Quicklink is that the library will continue to work for same-origin navigations. If you choose to [control which origins can be prefetched](https://github.com/GoogleChromeLabs/quicklink#specify-a-custom-list-of-allowed-origins), these will be limited to prefetching same-origin use-cases only. As this is a browser-level limitation, it will impact any prefetching library.
40 |
41 | ## Session Stitching
42 |
43 | 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=<>×tamp=<>`), where this information is used to customize the experience or log information to analytics.
44 |
45 | 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.
46 |
47 | To workaround this problem, you can consider passing along session information via the [ping attribute](https://caniuse.com/ping) (separately) so the origin can stitch a session together asynchronously.
48 |
49 | ## Ad-related considerations
50 |
51 | Sites that rely on ads as a source of monetization should not prefetch ad-links, to avoid unintentionally counting clicks against those ad placements, which can lead to inflated Ad CTR (click-through-rate).
52 |
53 | Ads appear on sites mostly in two ways:
54 |
55 | - **Inside iframes:** By default, most ad-servers render ads within iframes. In these cases, those ad-links won't be prefetched by Quicklink, unless a developer explicitly passes in the URL of an ads iframe. The reason is that the library look-up for in-viewport elements is restricted to those of the top-level origin.
56 |
57 | - **Outside iframes:** In cases when the site shows same-origin ads, displayed in the top-level document (e.g. by hosting the ads themselves and by displaying the ads in the page directly), the developer needs to explicitly tell Quicklink to avoid prefetching these links. This can be achieved by passing the URL or subpath of the ad-link, or the element containing it to the [custom ignore patterns list](/#custom-ignore-patterns).
58 |
59 | {% endmarkdownConvert %}
60 | {% endblock %}
61 |
--------------------------------------------------------------------------------
/site/src/assets/images/icons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/quicklink/5f23ff26b445d66cf8b5b3672820e160f4869b41/site/src/assets/images/icons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/site/src/assets/images/icons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/quicklink/5f23ff26b445d66cf8b5b3672820e160f4869b41/site/src/assets/images/icons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/site/src/assets/images/icons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/quicklink/5f23ff26b445d66cf8b5b3672820e160f4869b41/site/src/assets/images/icons/apple-touch-icon.png
--------------------------------------------------------------------------------
/site/src/assets/images/icons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/quicklink/5f23ff26b445d66cf8b5b3672820e160f4869b41/site/src/assets/images/icons/favicon-16x16.png
--------------------------------------------------------------------------------
/site/src/assets/images/icons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/quicklink/5f23ff26b445d66cf8b5b3672820e160f4869b41/site/src/assets/images/icons/favicon-32x32.png
--------------------------------------------------------------------------------
/site/src/assets/images/icons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/quicklink/5f23ff26b445d66cf8b5b3672820e160f4869b41/site/src/assets/images/icons/favicon.ico
--------------------------------------------------------------------------------
/site/src/assets/images/icons/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/site/src/assets/images/logos/angular.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/site/src/assets/images/logos/drupal.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/site/src/assets/images/logos/magento.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/site/src/assets/images/logos/quicklink.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/site/src/assets/images/logos/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/site/src/assets/images/logos/vue.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/site/src/assets/images/logos/wordpress.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/site/src/assets/images/og-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/quicklink/5f23ff26b445d66cf8b5b3672820e160f4869b41/site/src/assets/images/og-image.png
--------------------------------------------------------------------------------
/site/src/assets/images/quicklink-used-logos/barefootwine.ca.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/quicklink/5f23ff26b445d66cf8b5b3672820e160f4869b41/site/src/assets/images/quicklink-used-logos/barefootwine.ca.png
--------------------------------------------------------------------------------
/site/src/assets/images/quicklink-used-logos/hartfordwines.com.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/quicklink/5f23ff26b445d66cf8b5b3672820e160f4869b41/site/src/assets/images/quicklink-used-logos/hartfordwines.com.png
--------------------------------------------------------------------------------
/site/src/assets/images/quicklink-used-logos/hashnode.com.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/quicklink/5f23ff26b445d66cf8b5b3672820e160f4869b41/site/src/assets/images/quicklink-used-logos/hashnode.com.png
--------------------------------------------------------------------------------
/site/src/assets/images/quicklink-used-logos/matsuda.com.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/quicklink/5f23ff26b445d66cf8b5b3672820e160f4869b41/site/src/assets/images/quicklink-used-logos/matsuda.com.png
--------------------------------------------------------------------------------
/site/src/assets/images/quicklink-used-logos/newegg.com.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/quicklink/5f23ff26b445d66cf8b5b3672820e160f4869b41/site/src/assets/images/quicklink-used-logos/newegg.com.png
--------------------------------------------------------------------------------
/site/src/assets/images/quicklink-used-logos/oakley.com.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/quicklink/5f23ff26b445d66cf8b5b3672820e160f4869b41/site/src/assets/images/quicklink-used-logos/oakley.com.png
--------------------------------------------------------------------------------
/site/src/assets/images/quicklink-used-logos/paulrand.design.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/quicklink/5f23ff26b445d66cf8b5b3672820e160f4869b41/site/src/assets/images/quicklink-used-logos/paulrand.design.png
--------------------------------------------------------------------------------
/site/src/assets/images/quicklink-used-logos/quiply.com.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/quicklink/5f23ff26b445d66cf8b5b3672820e160f4869b41/site/src/assets/images/quicklink-used-logos/quiply.com.png
--------------------------------------------------------------------------------
/site/src/assets/images/quicklink-used-logos/rayban.com.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/quicklink/5f23ff26b445d66cf8b5b3672820e160f4869b41/site/src/assets/images/quicklink-used-logos/rayban.com.png
--------------------------------------------------------------------------------
/site/src/assets/images/quicklink-used-logos/saintagnes.org.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/quicklink/5f23ff26b445d66cf8b5b3672820e160f4869b41/site/src/assets/images/quicklink-used-logos/saintagnes.org.png
--------------------------------------------------------------------------------
/site/src/assets/images/quicklink-used-logos/syfy.com.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/quicklink/5f23ff26b445d66cf8b5b3672820e160f4869b41/site/src/assets/images/quicklink-used-logos/syfy.com.png
--------------------------------------------------------------------------------
/site/src/assets/images/quicklink-used-logos/vinyla.com.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/quicklink/5f23ff26b445d66cf8b5b3672820e160f4869b41/site/src/assets/images/quicklink-used-logos/vinyla.com.png
--------------------------------------------------------------------------------
/site/src/assets/images/quicklink-used-logos/week.co.jp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/quicklink/5f23ff26b445d66cf8b5b3672820e160f4869b41/site/src/assets/images/quicklink-used-logos/week.co.jp.png
--------------------------------------------------------------------------------
/site/src/assets/images/screenshots/devtools-optimized-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/quicklink/5f23ff26b445d66cf8b5b3672820e160f4869b41/site/src/assets/images/screenshots/devtools-optimized-1.png
--------------------------------------------------------------------------------
/site/src/assets/images/screenshots/devtools-optimized-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/quicklink/5f23ff26b445d66cf8b5b3672820e160f4869b41/site/src/assets/images/screenshots/devtools-optimized-2.png
--------------------------------------------------------------------------------
/site/src/assets/images/screenshots/devtools-unoptimized.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/quicklink/5f23ff26b445d66cf8b5b3672820e160f4869b41/site/src/assets/images/screenshots/devtools-unoptimized.png
--------------------------------------------------------------------------------
/site/src/assets/images/screenshots/spa-devtools-optimized-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/quicklink/5f23ff26b445d66cf8b5b3672820e160f4869b41/site/src/assets/images/screenshots/spa-devtools-optimized-1.png
--------------------------------------------------------------------------------
/site/src/assets/images/screenshots/spa-devtools-optimized-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/quicklink/5f23ff26b445d66cf8b5b3672820e160f4869b41/site/src/assets/images/screenshots/spa-devtools-optimized-2.png
--------------------------------------------------------------------------------
/site/src/assets/images/screenshots/wpt-metrics-comparison.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/quicklink/5f23ff26b445d66cf8b5b3672820e160f4869b41/site/src/assets/images/screenshots/wpt-metrics-comparison.png
--------------------------------------------------------------------------------
/site/src/assets/images/screenshots/wpt-video-comparison.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/quicklink/5f23ff26b445d66cf8b5b3672820e160f4869b41/site/src/assets/images/screenshots/wpt-video-comparison.gif
--------------------------------------------------------------------------------
/site/src/assets/images/screenshots/wpt-visual-comparison.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/GoogleChromeLabs/quicklink/5f23ff26b445d66cf8b5b3672820e160f4869b41/site/src/assets/images/screenshots/wpt-visual-comparison.png
--------------------------------------------------------------------------------
/site/src/assets/js/script.js:
--------------------------------------------------------------------------------
1 | window.addEventListener('load', () => {
2 | 'use strict';
3 |
4 | function initGoToTopBtn() {
5 | const goTopBtn = document.querySelector('.back-to-top');
6 |
7 | function trackScroll() {
8 | const scrolled = window.pageYOffset;
9 | const threshold = 400;
10 |
11 | if (scrolled > threshold) {
12 | goTopBtn.classList.remove('hidden');
13 | }
14 | if (scrolled < threshold) {
15 | goTopBtn.classList.add('hidden');
16 | }
17 | }
18 |
19 | function scrollToTop() {
20 | const c = document.documentElement.scrollTop || document.body.scrollTop;
21 | if (c > 0) {
22 | window.requestAnimationFrame(scrollToTop);
23 | window.scrollTo(0, c - c / 8);
24 | }
25 | }
26 |
27 | function backToTop() {
28 | if (window.pageYOffset > 0) {
29 | scrollToTop();
30 | }
31 | }
32 |
33 | window.addEventListener('scroll', trackScroll, {passive: true});
34 | goTopBtn.addEventListener('click', backToTop);
35 | }
36 |
37 | initGoToTopBtn();
38 |
39 | const clipboard = new ClipboardJS('#copy-snippet-button', {
40 | text: trigger => trigger.parentNode.previousElementSibling.textContent.trim(),
41 | });
42 |
43 | clipboard.on('success', event => {
44 | event.clearSelection();
45 | event.trigger.blur();
46 | const notifyCopiedSnippet = document.querySelector('.notify-copied-snippet');
47 | notifyCopiedSnippet.classList.add('notify-copied-snippet--displayed');
48 | });
49 |
50 | clipboard.on('error', event => {
51 | console.error('[clipboard error] Action:', event.action);
52 | console.error('[clipboard error] Trigger:', event.trigger);
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/site/src/assets/styles/_copy-snippet.scss:
--------------------------------------------------------------------------------
1 | .copy-snippet-widget {
2 | background: linear-gradient(hsla(0deg, 0%, 100%, .775), hsla(0deg, 0%, 100%, .7));
3 | border-radius: 6px;
4 | padding: 10px 18px 17px;
5 | margin: 1.5em 0;
6 | box-shadow: inset 0 1px 0 hsla(0deg, 0%, 100%, 1), 0 3px 20px hsla(0deg, 0%, 0%, .1);
7 | }
8 |
9 | .snippet-for-copy {
10 | // border: 1px solid hsla(0, 0%, 0%, .15);
11 | border-radius: 1px;
12 | // background: hsla(0, 0%, 0%, .075);
13 | margin: 10px 0;
14 | font-size: .95em;
15 | }
16 |
17 | .copy-snippet-widget .snippet-for-copy pre {
18 | margin-bottom: 0;
19 | }
20 |
21 | .notify-copied-snippet {
22 | font-size: .8em;
23 | margin-top: .35em;
24 | margin-left: .35em;
25 | display: none;
26 | }
27 |
28 | .notify-copied-snippet--displayed {
29 | display: block;
30 | }
31 |
32 | .button {
33 | background: linear-gradient(hsla(0deg, 0%, 95%, 1), hsla(0deg, 0%, 90%, 1));
34 | border: 1px solid;
35 | --border-color-opacity: .25;
36 | --border-color: hsla(0deg, 0%, 25%, var(--border-color-opacity)) hsla(0deg, 0%, 10%, var(--border-color-opacity)) hsla(0deg, 0%, 0%, var(--border-color-opacity));
37 | border-color: var(--border-color);
38 | border-radius: 4px;
39 | padding: 4px 10px;
40 | --light-opacity: 1;
41 | --shadow-opacity: .05;
42 | --shadow-blur-radius: 1px;
43 | --box-shadow: inset 0 1px 0 hsla(0deg, 0%, 100%, var(--light-opacity)), 0 1px var(--shadow-blur-radius) hsla(0deg, 0%, 0%, var(--shadow-opacity)); // An intermediate variable is needed for Safari
44 | box-shadow: var(--box-shadow);
45 | cursor: pointer;
46 | font-size: 16px;
47 | }
48 |
49 | .button--copy-snippet {
50 | background: radial-gradient(ellipse at top, hsla(0deg, 0%, 100%, .25), transparent), linear-gradient(hsla(330deg, 73%, 49%, .65), hsla(330deg, 73%, 49%, 1));
51 | color: #fff;
52 | text-shadow: 0 1px 1px hsla(0deg, 0%, 0%, .25);
53 | --light-opacity: .2;
54 | --border-color-opacity: .25;
55 | vertical-align: top;
56 |
57 | font-size: .8em;
58 | padding: 7px 14px;
59 | border-radius: 7px;
60 | --shadow-opacity: .25;
61 | --shadow-blur-radius: 3px;
62 | font-weight: 700;
63 | letter-spacing: .01em;
64 | }
65 |
66 | @media (min-width: 600px) {
67 | .button--copy-snippet {
68 | font-size: 1em;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/site/src/assets/styles/github-markdown.scss:
--------------------------------------------------------------------------------
1 | // stylelint-disable declaration-no-important
2 |
3 | @use "vendor/github-markdown";
4 |
5 | .markdown-body {
6 | box-sizing: border-box;
7 | min-width: 200px;
8 | max-width: 980px;
9 | margin: 0 auto;
10 | padding: 45px;
11 | // TODO: !important use does not look pefect, the github font looks better
12 | font-family: Arial, Helvetica, sans-serif !important;
13 | // TODO: opt for font color
14 | color: hsl(226deg, 52%, 27%) !important;
15 | }
16 |
17 | .markdown-body h1,
18 | h2,
19 | h3,
20 | h4,
21 | h5 {
22 | // Google Sans is for headlines only per Google guidelines
23 | font-family: Arial, Helvetica, sans-serif !important;
24 | }
25 |
26 | @media (max-width: 767px) {
27 | .markdown-body {
28 | padding: 15px;
29 | }
30 |
31 | .markdown-body h2 {
32 | font-size: 1.2em !important;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/site/src/assets/styles/main.scss:
--------------------------------------------------------------------------------
1 | @use "vendor/prism";
2 | @use "copy-snippet";
3 |
4 | html {
5 | height: 100%;
6 | }
7 |
8 | html,
9 | body {
10 | min-height: 100%;
11 | }
12 |
13 | body {
14 | margin: 0;
15 | font-family: Arial, Helvetica, sans-serif;
16 | }
17 |
18 | p,
19 | ul {
20 | margin: 0 0 16px;
21 | font-style: normal;
22 | }
23 |
24 | .page-header {
25 | text-align: center;
26 | background: #fff;
27 | padding-top: 1rem;
28 | box-shadow: 0 0 6px rgba(57, 73, 76, .35);
29 | }
30 |
31 | .page-header__title {
32 | display: flex;
33 | justify-content: flex-start;
34 | align-items: center;
35 | padding: 7px;
36 | }
37 |
38 | .page-header__subtitle {
39 | font-size: 1em;
40 | margin: 0;
41 | }
42 |
43 | .page-header__logo-link {
44 | margin-right: 14px;
45 | border: none;
46 | border-right: 2px solid rgb(197, 197, 197);
47 | }
48 |
49 | .page-header__logo-image {
50 | width: 160px;
51 | height: auto;
52 | margin-right: 6px;
53 | }
54 |
55 | .page-header__navigation a {
56 | position: relative;
57 | display: inline-block;
58 | text-decoration: none;
59 | border: none;
60 | white-space: nowrap;
61 | padding: 8px 0;
62 | min-width: 40px;
63 | z-index: 1;
64 | }
65 |
66 | .page-header__navigation a::before {
67 | content: "";
68 | position: absolute;
69 | z-index: -1;
70 | top: 0;
71 | bottom: 0;
72 | left: -.5em;
73 | right: -.5em;
74 | background-color: #46aee8;
75 | transform-origin: 50% 100%;
76 | transform: scaleY(0);
77 | transition: transform .15s ease-in-out;
78 | }
79 |
80 | .page-header__navigation a:hover::before {
81 | transform: scaleY(1);
82 | transform-origin: 50% 100%;
83 | }
84 |
85 | .page-header__navigation a.active {
86 | pointer-events: none;
87 | }
88 |
89 | .page-header__navigation a.active::before {
90 | transform: scaleY(1);
91 | opacity: .5;
92 | transform-origin: 50% 100%;
93 | }
94 |
95 | .page-header__navigation ul {
96 | margin: 0;
97 | padding: .5rem .5rem 0;
98 | }
99 |
100 | .page-header__navigation li {
101 | display: inline;
102 | margin: 6px 0 0 15px;
103 | }
104 |
105 | .page-header__navigation li:last-child {
106 | margin-right: 0;
107 | }
108 |
109 | main.page-main {
110 | padding: 0;
111 | width: 96%;
112 | margin-top: 16px;
113 | margin-bottom: 16px;
114 | }
115 |
116 | .page-main > h1 {
117 | color: #fff;
118 | }
119 |
120 | .page-main > ul {
121 | margin: 0;
122 | padding: 0;
123 | }
124 |
125 | .hidden {
126 | display: none;
127 | }
128 |
129 | .text-center {
130 | text-align: center;
131 | }
132 |
133 | .center {
134 | display: flex;
135 | justify-content: center;
136 | }
137 |
138 | .back-to-top {
139 | opacity: 1;
140 | pointer-events: all;
141 | position: fixed;
142 | bottom: 3rem;
143 | right: 3rem;
144 | z-index: 9999;
145 | width: 30px;
146 | height: 30px;
147 | text-align: center;
148 | line-height: 30px;
149 | background: #fff;
150 | box-shadow: 0 2px 8px rgba(0, 0, 0, .35);
151 | cursor: pointer;
152 | border-radius: 50%;
153 | transform: translate3d(0, 0, 0);
154 | transition: transform .3s ease;
155 | }
156 |
157 | .back-to-top:hover {
158 | transform: translate3d(0, -2px, 0);
159 | }
160 |
161 | .back-to-top.hidden {
162 | opacity: 0;
163 | pointer-events: none;
164 | transform: translate3d(300%, 0, 0);
165 | transition: transform .3s ease, opacity .3s ease;
166 | }
167 |
168 | // no bullet list
169 | .page-main ul.no-bullet {
170 | list-style-type: none;
171 | padding-left: 0;
172 | }
173 |
174 | .list-icon svg {
175 | margin-right: 1rem;
176 | }
177 |
178 | // grid
179 | .flex-grid {
180 | display: flex;
181 | }
182 |
183 | .overflow-x-auto {
184 | overflow-x: auto;
185 | }
186 |
187 | .flex-grid .flex-grid__item {
188 | margin-left: 24px;
189 | }
190 |
191 | .flex-grid .flex-grid__item:last-child {
192 | margin-right: 16px;
193 | }
194 |
195 | .normal-section {
196 | padding: 0 8px;
197 | }
198 |
199 | .highlighted-section {
200 | padding: 16px 24px;
201 | margin: 16px 0;
202 | }
203 |
204 | .highlighted-section__text {
205 | // TODO: opt for bg color
206 | // background-color: #d8217d;
207 | background-color: #283646;
208 | color: #fff;
209 | }
210 |
211 | .highlighted-section__text p {
212 | color: #fff;
213 | }
214 |
215 | .highlighted-section.highlighted-section__text h2 {
216 | border: 0;
217 | }
218 |
219 | // heading
220 | .primary-font-color {
221 | // TODO: opt for font color
222 | color: hsl(226deg, 52%, 27%);
223 | }
224 |
225 | .secondary-font-color {
226 | color: #fe8ec6;
227 | }
228 |
229 | .tertiary-font-color {
230 | color: #d74b91;
231 | }
232 |
233 | main.page-main .heading {
234 | letter-spacing: -1px;
235 | font-size: 1.5em;
236 | font-weight: 900;
237 | margin: 0;
238 | border: 0;
239 | }
240 |
241 | main.page-main .heading em {
242 | font-style: normal;
243 | white-space: nowrap;
244 | }
245 |
246 | .flex-between-center {
247 | display: flex;
248 | justify-content: space-between;
249 | align-items: center;
250 | }
251 |
252 | .large-github {
253 | display: none;
254 | }
255 |
256 | .article-image {
257 | margin: 20px auto;
258 | display: block;
259 | max-width: 100%;
260 | height: auto;
261 | }
262 |
263 | @media (min-width: 600px) {
264 | main.page-main .heading {
265 | font-size: 2.1em;
266 | margin: 24px 0 18px;
267 | }
268 |
269 | .page-header__navigation a {
270 | padding: 16px 0;
271 | min-width: 50px;
272 | }
273 |
274 | .page-header__subtitle {
275 | margin: 1.7em 0;
276 | }
277 |
278 | .page-header__logo-image {
279 | width: 200px;
280 | }
281 |
282 | .small-github {
283 | display: none;
284 | }
285 |
286 | .large-github {
287 | display: block;
288 | }
289 | }
290 |
291 | @media (min-width: 992px) {
292 | body {
293 | background-color: #eee;
294 | }
295 |
296 | main.page-main {
297 | background-color: rgb(255, 255, 255);
298 | box-shadow: 0 1px 6px rgba(57, 73, 76, .35);
299 | margin-top: 3.2rem;
300 | margin-bottom: 3.2rem;
301 | padding-bottom: 1rem;
302 | }
303 |
304 | .page-header {
305 | padding-top: 0;
306 | }
307 |
308 | .page-header-content {
309 | display: flex;
310 | justify-content: space-between;
311 | align-items: center;
312 | min-width: 200px;
313 | max-width: 980px;
314 | margin: 0 auto;
315 | }
316 |
317 | .page-header__title {
318 | padding: 14px;
319 | }
320 |
321 | .page-header__logo-link {
322 | display: inline-block;
323 | }
324 |
325 | .page-header__logo-image {
326 | width: 222px;
327 | }
328 |
329 | .page-header nav {
330 | display: inline-block;
331 | }
332 |
333 | main.page-main .heading {
334 | margin: 24px 0;
335 | }
336 |
337 | .normal-section {
338 | padding: 0 24px;
339 | }
340 |
341 | .highlighted-section {
342 | padding: 16px 24px;
343 | margin: 16px 0;
344 | }
345 | }
346 |
347 | .site-footer {
348 | margin-top: 1rem;
349 | border-top: 1px dotted #cecece;
350 | }
351 |
352 | .site-footer p.text-center {
353 | margin-top: 16px;
354 | }
355 |
--------------------------------------------------------------------------------
/site/src/assets/styles/vendor/_prism.scss:
--------------------------------------------------------------------------------
1 | /**
2 | * prism.js tomorrow night eighties for JavaScript, CoffeeScript, CSS and HTML
3 | * Based on https://github.com/chriskempson/tomorrow-theme
4 | * @author Rose Pritchard
5 | */
6 |
7 | code[class*="language-"],
8 | pre[class*="language-"] {
9 | color: #ccc;
10 | background: none;
11 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
12 | font-size: 1em;
13 | text-align: left;
14 | white-space: pre;
15 | word-spacing: normal;
16 | word-break: normal;
17 | word-wrap: normal;
18 | line-height: 1.5;
19 |
20 | -moz-tab-size: 4;
21 | -o-tab-size: 4;
22 | tab-size: 4;
23 |
24 | -webkit-hyphens: none;
25 | -moz-hyphens: none;
26 | -ms-hyphens: none;
27 | hyphens: none;
28 | }
29 |
30 | /* Code blocks */
31 | pre[class*="language-"] {
32 | padding: 1em;
33 | margin: .5em 0;
34 | overflow: auto;
35 | }
36 |
37 | :not(pre) > code[class*="language-"],
38 | pre[class*="language-"] {
39 | background: #2d2d2d;
40 | }
41 |
42 | /* Inline code */
43 | :not(pre) > code[class*="language-"] {
44 | padding: .1em;
45 | border-radius: .3em;
46 | white-space: normal;
47 | }
48 |
49 | .token.comment,
50 | .token.block-comment,
51 | .token.prolog,
52 | .token.doctype,
53 | .token.cdata {
54 | color: #999;
55 | }
56 |
57 | .token.punctuation {
58 | color: #ccc;
59 | }
60 |
61 | .token.tag,
62 | .token.attr-name,
63 | .token.namespace,
64 | .token.deleted {
65 | color: #e2777a;
66 | }
67 |
68 | .token.function-name {
69 | color: #6196cc;
70 | }
71 |
72 | .token.boolean,
73 | .token.number,
74 | .token.function {
75 | color: #f08d49;
76 | }
77 |
78 | .token.property,
79 | .token.class-name,
80 | .token.constant,
81 | .token.symbol {
82 | color: #f8c555;
83 | }
84 |
85 | .token.selector,
86 | .token.important,
87 | .token.atrule,
88 | .token.keyword,
89 | .token.builtin {
90 | color: #cc99cd;
91 | }
92 |
93 | .token.string,
94 | .token.char,
95 | .token.attr-value,
96 | .token.regex,
97 | .token.variable {
98 | color: #7ec699;
99 | }
100 |
101 | .token.operator,
102 | .token.entity,
103 | .token.url {
104 | color: #67cdcc;
105 | }
106 |
107 | .token.important,
108 | .token.bold {
109 | font-weight: bold;
110 | }
111 | .token.italic {
112 | font-style: italic;
113 | }
114 |
115 | .token.entity {
116 | cursor: help;
117 | }
118 |
119 | .token.inserted {
120 | color: green;
121 | }
122 |
--------------------------------------------------------------------------------
/site/src/demo.njk:
--------------------------------------------------------------------------------
1 | ---
2 | title: Demo
3 | layout: layouts/base.njk
4 | description: Faster subsequent page-loads by prefetching in-viewport links during idle time.
5 | eleventyNavigation:
6 | key: Demo
7 | order: 3
8 | ---
9 |
10 | {% extends "layouts/normal-section-wrapper.njk" %}
11 | {% block section %}
12 | {% markdownConvert %}
13 |
14 | ## Demos
15 |
16 | This page contains some demo sites that use Quicklink to improve navigation, grouped by architecture: **Multi Page Apps** / **Single Page Apps**.
17 |
18 | If you like the library, and want to try them on your site, check out the **Installation** section of the [home page](/).
19 |
20 | #### Multi Page Apps
21 |
22 | In this demo you’ll compare an ecommerce site with and without Quicklink to see how navigation is improved thanks to the library.
23 |
24 | The following waterfall shows a typical navigation for a site without Quicklink (top) vs. the same site using the library (bottom):
25 |
26 | {% endmarkdownConvert %}
27 |
28 |
29 |
30 | {% markdownConvert %}
31 |
32 | To try the demo:
33 |
34 | 1. Open the [unoptimized site](https://mini-ecomm.glitch.me/) in Chrome.
35 | 1. Open **DevTools** and go to the **Network panel** to simulate a **Fast 3G** Connection.
36 | 1. Pick **Galaxy S5** as a simulated device.
37 | 1. Make sure **Disable cache** is not checked.
38 | 1. Reload the page.
39 |
40 | {% endmarkdownConvert %}
41 |
42 |
43 |
44 | {% markdownConvert %}
45 |
46 | Now, measure performance on the same site, that uses Quicklink:
47 |
48 | 1. Open the [optimized site](https://mini-ecomm-quicklink.glitch.me/) in Chrome.
49 | 1. Open **DevTools** and go to the **Network panel** to simulate a **Fast 3G** Connection.
50 | 1. Pick **Galaxy S5** as a simulated device.
51 | 1. Make sure **Disable cache** is not checked.
52 | 1. Reload the page.
53 |
54 | Prefetched links can be identified in the **Network** panel by having `quicklink` as the **Initiator** and **Lowest** as the **Priority**:
55 |
56 | {% endmarkdownConvert %}
57 |
58 |
59 |
60 | {% markdownConvert %}
61 |
62 | To measure the impact of `quicklink` on navigations:
63 |
64 | 1. Clear the **Network** trace.
65 | 1. Click on a list item.
66 | 1. Take a look at the **Network** panel.
67 |
68 | {% endmarkdownConvert %}
69 |
70 |
71 |
72 | {% markdownConvert %}
73 |
74 | In the **Size** column of the **Network** panel the trace shows that the product page was retrieved from the **prefetch cache** and now takes **3ms** to load: a **97% improvement** compared to the unoptimized version.
75 |
76 | Here is a comparison video:
77 |
78 | {% endmarkdownConvert %}
79 |
80 |
81 |
82 | {% markdownConvert %}
83 |
84 | ### Single Page Apps
85 |
86 | Quicklink 2.0 includes support for React-based single-page-apps. This has been covered to the detail in this [guide](https://web.dev/quicklink/).
87 |
88 | To try the demo:
89 |
90 | 1. Open the [optimized site](https://create-react-app-quicklink.glitch.me/) in Chrome.
91 | 1. Open DevTools and go to the **Network** panel to simulate a **Fast 3G** Connection.
92 | 1. Pick **Galaxy S5** as a simulated device.
93 | 1. Make sure **Disable cache** is not checked.
94 | 1. Reload the page.
95 |
96 | When the home page loads the chunks for that route are loaded. After that, `quicklink` prefetches the route's chunks for the in-viewport links:
97 |
98 | {% endmarkdownConvert %}
99 |
100 |
101 |
102 | {% markdownConvert %}
103 |
104 | Next:
105 |
106 | 1. Clear the **Network** log again.
107 | 1. Make sure **Disable** cache is not checked.
108 | 1. Click the Blog link to navigate to that page.
109 |
110 | {% endmarkdownConvert %}
111 |
112 |
113 |
114 | {% markdownConvert %}
115 |
116 | The Size column indicates that these chunks were retrieved from the "prefetch cache", instead of the network. Loading these chunks without a Quicklink takes approximately 580ms. Using the library it takes 2ms, which represents a 99% reduction!
117 |
118 | {% endmarkdownConvert %}
119 | {% endblock %}
120 |
--------------------------------------------------------------------------------
/site/src/index.njk:
--------------------------------------------------------------------------------
1 | ---
2 | title: Home
3 | layout: layouts/base.njk
4 | description: Faster subsequent page-loads by prefetching in-viewport links during idle time.
5 | eleventyNavigation:
6 | key: Home
7 | order: 0
8 | extra_head:
9 | ---
10 |
11 | {% include "components/heading.njk" %}
12 |
13 | {% include "components/why-quicklink.njk" %}
14 |
15 | {% include "components/copy-snippet.njk" %}
16 |
17 | {% include "components/download.njk" %}
18 |
19 | {% include "components/trusted-by.njk" %}
20 |
21 | {% include "components/installation.njk" %}
22 |
23 | {% include "components/usage.njk" %}
24 |
25 | {% include "components/over-prefetching.njk" %}
26 |
27 | {% include "components/react.njk" %}
28 |
29 | {% include "components/chrome-extension.njk" %}
30 |
31 | {% include "components/why-prefetch.njk" %}
32 |
33 | {% include "components/use-with.njk" %}
34 |
--------------------------------------------------------------------------------
/site/src/measure.njk:
--------------------------------------------------------------------------------
1 | ---
2 | title: Measure
3 | layout: layouts/base.njk
4 | description: Faster subsequent page-loads by prefetching in-viewport links during idle time.
5 | eleventyNavigation:
6 | key: Measure
7 | order: 4
8 | ---
9 |
10 | {% extends "layouts/normal-section-wrapper.njk" %}
11 | {% block section %}
12 | {% markdownConvert %}
13 | ## Measuring impact of Quicklink in sites
14 |
15 | Implementing Quicklink in sites can speed up navigations, by automatically prefetching in-viewport links during idle time.
16 | Different metrics can be improved as a result of this, the most common ones being [Start Render](https://sites.google.com/a/webpagetest.org/docs/using-webpagetest/quick-start-quide#TOC-Start-Render:) and [First Contentful Paint](https://developers.google.com/web/tools/lighthouse/audits/first-contentful-paint).
17 |
18 | In this section, we explore different ways of measuring the impact of Quicklink in sites. To showcase that, we’ll use the following sites:
19 | - An [unoptimized e-commerce demo](https://mini-ecomm.glitch.me/), consisting of a listing and a product page. The demo introduces a 1s delay before the product page response, to similate the backend processing time of a real e-commerce site. You can see the code, and make changes by remixing the [Glitch project](https://glitch.com/edit/#!/mini-ecomm?path=server.js:11:58).
20 | - An [optimized version of the site](https://mini-ecomm-quicklink.glitch.me/), which is a copy of the original version, but this time, using Quicklink in the listing page, to prefetch links that come to the view. You can view and edit the code, in the [Glitch project](https://glitch.com/edit/#!/mini-ecomm-quicklink?path=server.js:1:0).
21 |
22 | If you take a look at the code of the listing page, in the optimized version of the site, you'll find the code to initialize Quicklink:
23 | {% endmarkdownConvert %}
24 | {% highlight "js" %}
25 |
26 |
31 | {% endhighlight %}
32 | {% markdownConvert %}
33 | ## Using Chrome DevTools
34 |
35 | The first tool you’ll use is [Chrome DevTools](https://developers.google.com/web/tools/chrome-devtools), which is useful both for local development, and also for production URLs.
36 |
37 | First, measure performance before implementing Quicklink:
38 |
39 | - Open the [unoptimized demo](https://mini-ecomm.glitch.me/) in Chrome.
40 | - Open the **Network** panel and simulate a **Fast 3G** Connection.
41 | - Pick **Galaxy S5** as simulated device.
42 | - Make sure **Disable cache** is not checked.
43 | - Reload the page.
44 | - Click on the first product in the listing.
45 |
46 | Take a look at the **Time** column: the product page takes approximately **2.5s** to load:
47 |
48 | {% endmarkdownConvert %}
49 |
50 |
51 |
52 | {% markdownConvert %}
53 |
54 | Now, measure performance after implementing Quicklink:
55 |
56 | - Open the [optimized demo](https://mini-ecomm-quicklink.glitch.me/) in Chrome.
57 | - Open the **Network** panel and simulate a Fast 3G Connection.
58 | - Pick **Galaxy S5** as simulated device.
59 | - Make sure **Disable cache** is not checked.
60 | - Reload the page.
61 |
62 | Prefetched links can be identified in the Network panel by having Quicklink as the **Initiator** and **Lowest** as the Priority:
63 |
64 | {% endmarkdownConvert %}
65 |
66 |
67 |
68 | {% markdownConvert %}
69 |
70 | To measure the impact of Quicklink on navigations:
71 |
72 | - Click on a list item.
73 | - Take a look at the **Network** panel.
74 |
75 | {% endmarkdownConvert %}
76 |
77 |
78 |
79 | {% markdownConvert %}
80 |
81 | In the Size column of the **Network** panel the trace shows that the product page was retrieved from the **prefetch cache** and now takes **3ms** to load: a **97% improvement** compared to the unoptimized version.
82 |
83 | ## Using Webpagetest
84 |
85 | Webpagetest can be used to measure impact on real devices and different connection types. You'll use [WPT Scripting](https://sites.google.com/a/webpagetest.org/docs/using-webpagetest/scripting) to simulate a user arriving at the home page and clicking one of the product items.
86 |
87 | Open to webpagetest.org.
88 | Pick **Nexus 5** as Test Location.
89 | In the Advanced Settings tab, pick **3GFast** in the connection type.
90 | In the Script tab, place the following script:
91 |
92 | ```
93 | logData 0
94 | navigate https://mini-ecomm.glitch.me/
95 | logData 1
96 | execAndWait document.querySelector('a').click()
97 | ```
98 |
99 | The script instructs WPT to open the [unoptimized demo](https://mini-ecomm.glitch.me/) and simulate a click on the first product of the listing. Metrics are captured only for the product page. Here is the [resultiing test](https://www.webpagetest.org/result/191103_TM_e68d81788d8744762301b44c6e3e72d2/).
100 |
101 | Repeat the process on [the demo](https://mini-ecomm-quicklink.glitch.me/) that uses Quicklink. The script looks like:
102 |
103 | ```
104 | logData 0
105 | navigate https://mini-ecomm.glitch.me/
106 | logData 1
107 | execAndWait document.querySelector('a').click()
108 | ```
109 |
110 | Here is the [resultiing test](https://www.webpagetest.org/result/191103_E3_f8217e45ad837ac084868d4f3b9a4a73/).
111 |
112 | The following table compares the main metrics obtained for each of the sites:
113 |
114 | {% endmarkdownConvert %}
115 |
116 |
117 |
118 | {% markdownConvert %}
119 |
120 | Next, create a comparison between the tests.
121 |
122 | - Open the [WPT test](https://www.webpagetest.org/result/191103_TM_e68d81788d8744762301b44c6e3e72d2/) for the unoptimized site.
123 | - Click on the median run, that appears in the "First View" cell of the report.
124 | - Repeat the same process in the [optimized test](https://www.webpagetest.org/result/191103_E3_f8217e45ad837ac084868d4f3b9a4a73/).
125 |
126 | To create a comparison test, you need to append the IDs from the previous links as comma separated valuees, and send them as query params to `https://www.webpagetest.org/video/compare.php`:
127 |
128 | ```
129 | https://www.webpagetest.org/video/compare.php?tests=test_id_1,test_id_2
130 | ```
131 |
132 | The resulting comparison of the test ran previously can be found [here](https://www.webpagetest.org/video/compare.php?tests=191103_TM_e68d81788d8744762301b44c6e3e72d2-r%3A8-c%3A0%2C191103_E3_f8217e45ad837ac084868d4f3b9a4a73-r%3A7-c%3A0&thumbSize=200&ival=500&end=visual).
133 |
134 | ### Visual Comparison
135 |
136 | The unoptimized site starts rendering approximately at **2.5s**, the demo that uses Quicklink, starts at **1.2s**.
137 |
138 | {% endmarkdownConvert %}
139 |
140 |
141 |
142 | {% markdownConvert %}
143 |
144 | ### Video
145 |
146 | A video can be generated from the [comparison page](https://www.webpagetest.org/video/compare.php?tests=191103_TM_e68d81788d8744762301b44c6e3e72d2-r%3A8-c%3A0%2C191103_E3_f8217e45ad837ac084868d4f3b9a4a73-r%3A7-c%3A0&thumbSize=200&ival=500&end=visual), by clicking on **Create Video**.
147 |
148 | {% endmarkdownConvert %}
149 |
150 |
151 |
152 | {% markdownConvert %}
153 |
154 | ### Using RUM (Real user monitoring) tools
155 |
156 | RUM tools, let you visualize how different metrics evolve in time for real users. If prefetching affects a large amount of pages, you might be able to see more page loads being loaded faster after implementing it, which can be reflected in metrics [First Contentful Paint](https://developers.google.com/web/tools/lighthouse/audits/first-contentful-paint).
157 |
158 | For example, the [Chrome User Experience Report](https://developers.google.com/web/tools/chrome-user-experience-report/) provides performance metrics for how real-world Chrome users experience popular destinations on the web.
159 | CrUX data is available in [PageSpeed Insights](https://developers.google.com/speed/pagespeed/insights/) and also in [BigQuery](https://bigquery.cloud.google.com/dataset/chrome-ux-report:all?pli=1), but you can obtain a quick visualization of the evolution of your metrics using the [CrUX dashboard](https://g.co/chromeuxdash) (refer to [this guide](https://web.dev/chrome-ux-report-data-studio-dashboard/) for more details).
160 | The report contains a section for First Contentful Paint. If a large number of page views are prefetched as a result of implementing Quicklink, this graph could show a positive evolution in time.
161 |
162 | **Note:** Even when checking the performance for real users is a general performance best practice, it’s usually hard to correlate the overall performance improvement of a site with a single optimization like this one. With that said, the best way to make sure you’re measuring exactly this change is to perform a before / after test with laboratory tools as explained in previous sections.
163 |
164 | ### Using Quicklink Chrome extension
165 |
166 | [Quicklink Chrome Extension](https://chrome.google.com/webstore/detail/quicklink-chrome-extensio/epmplkdcjhgigmnjmjibilpmekhgkbeg) injects Quicklink in every site a user visits. You can use it to measure the potential impact of implementing the library on a site, before doing it.
167 | Since the extension will simulate how the library would work when implemented, you can install the extension and then run the tests with DevTools, as described in the previous section.
168 |
169 | ### Conclusion
170 |
171 | Quicklink can highly improve navigations by automatically prefetching in-viewport links, . We’ve explored different tools to measure the impact of implementing it in your site.
172 | Metrics like [Start Render](https://sites.google.com/a/webpagetest.org/docs/using-webpagetest/quick-start-quide#TOC-Start-Render:) and [First Contentful Paint](https://developers.google.com/web/tools/lighthouse/audits/first-contentful-paint) can be directly impacted by this change, but other metrics can be also improved as a result of this, as seen in the tests performed in this guide.
173 | Laboratory testing tools, like Chrome DevTools and Webpagetest can help you have an accurate idea of the impact of this change, by running a before / after comparison. Also, If the number of pages affected by this change is large enough, you might be able to visualize the impact of the implementation on real user monitoring (RUM) tools as well.
174 |
175 | {% endmarkdownConvert %}
176 | {% endblock %}
177 |
--------------------------------------------------------------------------------
/site/src/robots.njk:
--------------------------------------------------------------------------------
1 | ---
2 | permalink: /robots.txt
3 | layout: null
4 | eleventyExcludeFromCollections: true
5 | ---
6 | # www.robotstxt.org
7 |
8 | User-agent: *
9 | Disallow:{% if site.isNetlify %} /{% endif %}
10 | Sitemap: {{ site.url + "/sitemap.xml" }}
11 |
--------------------------------------------------------------------------------
/site/src/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Quicklink",
3 | "short_name": "Quicklink",
4 | "icons": [
5 | {
6 | "src": "/assets/images/icons/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/assets/images/icons/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "start_url": "/",
17 | "theme_color": "#fff",
18 | "background_color": "#fff",
19 | "display": "standalone"
20 | }
21 |
--------------------------------------------------------------------------------
/site/src/sitemap.njk:
--------------------------------------------------------------------------------
1 | ---
2 | permalink: /sitemap.xml
3 | layout: null
4 | eleventyExcludeFromCollections: true
5 | ---
6 |
7 |
8 | {% for page in collections.all -%}
9 |
10 | {{ site.url + page.url }}
11 | {{ page.date.toISOString() }}
12 |
13 | {%- endfor %}
14 |
15 |
--------------------------------------------------------------------------------
/site/watch.json:
--------------------------------------------------------------------------------
1 | {
2 | "install": {
3 | "include": [
4 | "^package\\.json$",
5 | "^\\.env$"
6 | ]
7 | },
8 | "restart": {
9 | "exclude": [
10 | "^src/",
11 | "^dist/"
12 | ],
13 | "include": [
14 | "watch.json$",
15 | ".eleventy.js$"
16 | ]
17 | },
18 | "throttle": 1000
19 | }
--------------------------------------------------------------------------------
/src/chunks.mjs:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | **/
16 |
17 | import throttle from 'throttles';
18 | import {viaFetch, supported} from './prefetch.mjs';
19 | import requestIdleCallback from './request-idle-callback.mjs';
20 |
21 | // Cache of URLs we've prefetched
22 | // Its `size` is compared against `opts.limit` value.
23 | const toPrefetch = new Set();
24 |
25 | /**
26 | * Determine if the anchor tag should be prefetched.
27 | * A filter can be a RegExp, Function, or Array of both.
28 | * - Function receives `node.href, node` arguments
29 | * - RegExp receives `node.href` only (the full URL)
30 | * @param {Element} node The anchor () tag.
31 | * @param {Mixed} filter The custom filter(s)
32 | * @return {Boolean} If true, then it should be ignored
33 | */
34 | function isIgnored(node, filter) {
35 | return Array.isArray(filter) ?
36 | filter.some(x => isIgnored(node, x)) :
37 | (filter.test || filter).call(filter, node.href, node);
38 | }
39 |
40 | /**
41 | * Prefetch an array of URLs if the user's effective
42 | * connection type and data-saver preferences suggests
43 | * it would be useful. By default, looks at in-viewport
44 | * links for `document`. Can also work off a supplied
45 | * DOM element or static array of URLs.
46 | * @param {Object} options - Configuration options for quicklink
47 | * @param {Object} [options.el] - DOM element to prefetch in-viewport links of
48 | * @param {Boolean} [options.priority] - Attempt higher priority fetch (low or high)
49 | * @param {Array} [options.origins] - Allowed origins to prefetch (empty allows all)
50 | * @param {Array|RegExp|Function} [options.ignores] - Custom filter(s) that run after origin checks
51 | * @param {Number} [options.timeout] - Timeout after which prefetching will occur
52 | * @param {Number} [options.throttle] - The concurrency limit for prefetching
53 | * @param {Number} [options.limit] - The total number of prefetches to allow
54 | * @param {Function} [options.timeoutFn] - Custom timeout function
55 | * @param {Function} [options.onError] - Error handler for failed `prefetch` requests
56 | * @param {Function} [options.prefetchChunks] - Function to prefetch chunks for route URLs (with route manifest for URL mapping)
57 | * @return {undefined}
58 | */
59 | export function listen(options = {}) {
60 | if (!window.IntersectionObserver) return;
61 |
62 | const [toAdd, isDone] = throttle(options.throttle || 1 / 0);
63 | const limit = options.limit || 1 / 0;
64 |
65 | const allowed = options.origins || [location.hostname];
66 | const ignores = options.ignores || [];
67 |
68 | const timeoutFn = options.timeoutFn || requestIdleCallback;
69 |
70 | const {prefetchChunks} = options;
71 |
72 | const prefetchHandler = urls => {
73 | prefetch(urls, options.priority)
74 | .then(isDone)
75 | .catch(error => {
76 | isDone();
77 | if (options.onError) options.onError(error);
78 | });
79 | };
80 |
81 | const observer = new IntersectionObserver(entries => {
82 | entries.forEach(entry => {
83 | if (entry.isIntersecting) {
84 | observer.unobserve(entry = entry.target);
85 | // Do not prefetch if will match/exceed limit
86 | if (toPrefetch.size < limit) {
87 | toAdd(() => {
88 | prefetchChunks ?
89 | prefetchChunks(entry, prefetchHandler) :
90 | prefetchHandler(entry.href);
91 | });
92 | }
93 | }
94 | });
95 | });
96 |
97 | timeoutFn(() => {
98 | // Find all links & Connect them to IO if allowed
99 | (options.el || document).querySelectorAll('a').forEach(link => {
100 | // If the anchor matches a permitted origin
101 | // ~> A `[]` or `true` means everything is allowed
102 | if (!allowed.length || allowed.includes(link.hostname)) {
103 | // If there are any filters, the link must not match any of them
104 | if (!isIgnored(link, ignores)) observer.observe(link);
105 | }
106 | });
107 | }, {
108 | timeout: options.timeout || 2000,
109 | });
110 |
111 | return function () {
112 | // wipe url list
113 | toPrefetch.clear();
114 | // detach IO entries
115 | observer.disconnect();
116 | };
117 | }
118 |
119 | /**
120 | * Prefetch a given URL with an optional preferred fetch priority
121 | * @param {String} url - the URL to fetch
122 | * @param {Boolean} [isPriority] - if is "high" priority
123 | * @return {Object} a Promise
124 | */
125 | export function prefetch(url, isPriority) {
126 | const {connection} = navigator;
127 |
128 | if (connection) {
129 | // Don't prefetch if using 2G or if Save-Data is enabled.
130 | if (connection.saveData) {
131 | return Promise.reject(new Error('Cannot prefetch, Save-Data is enabled'));
132 | }
133 |
134 | if (/2g/.test(connection.effectiveType)) {
135 | return Promise.reject(new Error('Cannot prefetch, network conditions are poor'));
136 | }
137 | }
138 |
139 | // Dev must supply own catch()
140 | return Promise.all(
141 | [].concat(url).map(str => {
142 | if (toPrefetch.has(str)) return [];
143 |
144 | // Add it now, regardless of its success
145 | // ~> so that we don't repeat broken links
146 | toPrefetch.add(str);
147 |
148 | return (isPriority ? viaFetch : supported)(
149 | new URL(str, location.href).toString(),
150 | );
151 | }),
152 | );
153 | }
154 |
--------------------------------------------------------------------------------
/src/index.mjs:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | **/
16 |
17 | import throttle from 'throttles';
18 | import {prefetchOnHover, supported, viaFetch} from './prefetch.mjs';
19 | import requestIdleCallback from './request-idle-callback.mjs';
20 | import {addSpeculationRules, hasSpecRulesSupport} from './prerender.mjs';
21 |
22 | // Cache of URLs we've prefetched
23 | // Its `size` is compared against `opts.limit` value.
24 | const toPrefetch = new Set();
25 |
26 | // Cache of URLs we've prerendered
27 | const toPrerender = new Set();
28 | // global var to keep prerenderAndPrefer option
29 | let shouldPrerenderAndPrefetch = false;
30 |
31 | /**
32 | * Determine if the anchor tag should be prefetched.
33 | * A filter can be a RegExp, Function, or Array of both.
34 | * - Function receives `node.href, node` arguments
35 | * - RegExp receives `node.href` only (the full URL)
36 | * @param {Element} node The anchor ( ) tag.
37 | * @param {Mixed} filter The custom filter(s)
38 | * @return {Boolean} If true, then it should be ignored
39 | */
40 | function isIgnored(node, filter) {
41 | return Array.isArray(filter) ?
42 | filter.some(x => isIgnored(node, x)) :
43 | (filter.test || filter).call(filter, node.href, node);
44 | }
45 |
46 | /**
47 | * Checks network conditions
48 | * @param {NetworkInformation} conn The connection information to be checked
49 | * @return {Boolean|Object} Error Object if the constrainsts are met or boolean otherwise
50 | */
51 | function checkConnection(conn) {
52 | if (conn) {
53 | // Don't pre* if using 2G or if Save-Data is enabled.
54 | if (conn.saveData) {
55 | return new Error('Save-Data is enabled');
56 | }
57 |
58 | if (/2g/.test(conn.effectiveType)) {
59 | return new Error('network conditions are poor');
60 | }
61 | }
62 |
63 | return true;
64 | }
65 |
66 | /**
67 | * Prefetch an array of URLs if the user's effective
68 | * connection type and data-saver preferences suggests
69 | * it would be useful. By default, looks at in-viewport
70 | * links for `document`. Can also work off a supplied
71 | * DOM element or static array of URLs.
72 | * @param {Object} options - Configuration options for quicklink
73 | * @param {Object|Array} [options.el] - DOM element(s) to prefetch in-viewport links of
74 | * @param {Boolean} [options.priority] - Attempt higher priority fetch (low or high)
75 | * @param {Boolean} [options.checkAccessControlAllowOrigin] - Check Access-Control-Allow-Origin response header
76 | * @param {Boolean} [options.checkAccessControlAllowCredentials] - Check the Access-Control-Allow-Credentials response header
77 | * @param {Boolean} [options.onlyOnMouseover] - Enable the prefetch only on mouseover event
78 | * @param {Array} [options.origins] - Allowed origins to prefetch (empty allows all)
79 | * @param {Array|RegExp|Function} [options.ignores] - Custom filter(s) that run after origin checks
80 | * @param {Number} [options.timeout] - Timeout after which prefetching will occur
81 | * @param {Number} [options.throttle] - The concurrency limit for prefetching
82 | * @param {Number} [options.threshold] - The area percentage of each link that must have entered the viewport to be fetched
83 | * @param {Number} [options.limit] - The total number of prefetches to allow
84 | * @param {Number} [options.delay] - Time each link needs to stay inside viewport before prefetching (milliseconds)
85 | * @param {Function} [options.timeoutFn] - Custom timeout function
86 | * @param {Function} [options.onError] - Error handler for failed `prefetch` requests
87 | * @param {Function} [options.hrefFn] - Function to use to build the URL to prefetch.
88 | * If it's not a valid function, then it will use the entry href.
89 | * @param {Boolean} [options.prerender] - Option to switch from prefetching and use prerendering only
90 | * @param {String} [options.eagerness] - Prerender eagerness mode - default immediate
91 | * @param {Boolean} [options.prerenderAndPrefetch] - Option to use both prerendering and prefetching
92 | * @return {Function}
93 | */
94 | export function listen(options = {}) {
95 | if (!window.IntersectionObserver || !('isIntersecting' in IntersectionObserverEntry.prototype)) return;
96 |
97 | const [toAdd, isDone] = throttle(options.throttle || 1 / 0);
98 | const limit = options.limit || 1 / 0;
99 | const threshold = options.threshold || 0;
100 |
101 | const allowed = options.origins || [location.hostname];
102 | const ignores = options.ignores || [];
103 | const delay = options.delay || 0;
104 | const hrefsInViewport = [];
105 |
106 | const timeoutFn = options.timeoutFn || requestIdleCallback;
107 | const hrefFn = typeof options.hrefFn === 'function' && options.hrefFn;
108 |
109 | const shouldOnlyPrerender = options.prerender || false;
110 | shouldPrerenderAndPrefetch = options.prerenderAndPrefetch || false;
111 |
112 | const setTimeoutIfDelay = (callback, delay) => {
113 | if (!delay) {
114 | callback();
115 | return;
116 | }
117 |
118 | setTimeout(callback, delay);
119 | };
120 |
121 | const observer = new IntersectionObserver(entries => {
122 | entries.forEach(entry => {
123 | // On enter
124 | if (entry.isIntersecting) {
125 | entry = entry.target;
126 | // Adding href to array of hrefsInViewport
127 | hrefsInViewport.push(entry.href);
128 |
129 | // Setting timeout
130 | setTimeoutIfDelay(() => {
131 | // Do not prefetch if not found in viewport
132 | if (!hrefsInViewport.includes(entry.href)) return;
133 |
134 | observer.unobserve(entry);
135 |
136 | // prerender, if..
137 | // either it's the prerender + prefetch mode or it's prerender *only* mode
138 | // Prerendering limit is following options.limit. UA may impose arbitraty numeric limit
139 | if ((shouldPrerenderAndPrefetch || shouldOnlyPrerender) && toPrerender.size < limit) {
140 | prerender(hrefFn ? hrefFn(entry) : entry.href, options.eagerness).catch(error => {
141 | if (options.onError) {
142 | options.onError(error);
143 | } else {
144 | throw error;
145 | }
146 | });
147 |
148 | return;
149 | }
150 |
151 | // Do not prefetch if will match/exceed limit and user has not switched to shouldOnlyPrerender mode
152 | if (toPrefetch.size < limit && !shouldOnlyPrerender) {
153 | toAdd(() => {
154 | prefetch(hrefFn ? hrefFn(entry) : entry.href, options.priority,
155 | options.checkAccessControlAllowOrigin, options.checkAccessControlAllowCredentials, options.onlyOnMouseover)
156 | .then(isDone)
157 | .catch(error => {
158 | isDone();
159 | if (options.onError) options.onError(error);
160 | });
161 | });
162 | }
163 | }, delay);
164 | // On exit
165 | } else {
166 | entry = entry.target;
167 | const index = hrefsInViewport.indexOf(entry.href);
168 | if (index > -1) {
169 | hrefsInViewport.splice(index);
170 | }
171 | }
172 | });
173 | }, {
174 | threshold,
175 | });
176 |
177 | timeoutFn(() => {
178 | // Find all links & Connect them to IO if allowed
179 | const elementsToListen = options.el &&
180 | options.el.length &&
181 | options.el.length > 0 &&
182 | options.el[0].nodeName === 'A' ?
183 | options.el :
184 | (options.el || document).querySelectorAll('a');
185 |
186 | elementsToListen.forEach(link => {
187 | // If the anchor matches a permitted origin
188 | // ~> A `[]` or `true` means everything is allowed
189 | if (!allowed.length || allowed.includes(link.hostname)) {
190 | // If there are any filters, the link must not match any of them
191 | if (!isIgnored(link, ignores)) observer.observe(link);
192 | }
193 | });
194 | }, {
195 | timeout: options.timeout || 2000,
196 | });
197 |
198 | return function () {
199 | // wipe url list
200 | toPrefetch.clear();
201 | // detach IO entries
202 | observer.disconnect();
203 | };
204 | }
205 |
206 | /**
207 | * Prefetch a given URL with an optional preferred fetch priority
208 | * @param {String | String[]} urls - the URLs to fetch
209 | * @param {Boolean} isPriority - if is "high" priority
210 | * @param {Boolean} checkAccessControlAllowOrigin - true to set crossorigin="anonymous" for DOM prefetch
211 | * and mode:'cors' for API fetch
212 | * @param {Boolean} checkAccessControlAllowCredentials - true to set credentials:'include' for API fetch
213 | * @param {Boolean} onlyOnMouseover - true to enable prefetch only on mouseover event
214 | * @return {Object} a Promise
215 | */
216 | export function prefetch(urls, isPriority, checkAccessControlAllowOrigin, checkAccessControlAllowCredentials, onlyOnMouseover) {
217 | const chkConn = checkConnection(navigator.connection);
218 | if (chkConn instanceof Error) {
219 | return Promise.reject(new Error(`Cannot prefetch, ${chkConn.message}`));
220 | }
221 |
222 | if (toPrerender.size > 0 && !shouldPrerenderAndPrefetch) {
223 | console.warn('[Warning] You are using both prefetching and prerendering on the same document');
224 | }
225 |
226 | // Dev must supply own catch()
227 | return Promise.all(
228 | [].concat(urls).map(str => {
229 | if (toPrefetch.has(str)) return [];
230 |
231 | // Add it now, regardless of its success
232 | // ~> so that we don't repeat broken links
233 | toPrefetch.add(str);
234 |
235 | return prefetchOnHover((isPriority ? viaFetch : supported), new URL(str, location.href).toString(), onlyOnMouseover,
236 | checkAccessControlAllowOrigin, checkAccessControlAllowCredentials, isPriority);
237 | }),
238 | );
239 | }
240 |
241 | /**
242 | * Prerender a given URL
243 | * @param {String | String[]} urls - the URLs to fetch
244 | * @param {String} eagerness - prerender eagerness mode - default immediate
245 | * @return {Object} a Promise
246 | */
247 | export function prerender(urls, eagerness = 'immediate') {
248 | const chkConn = checkConnection(navigator.connection);
249 | if (chkConn instanceof Error) {
250 | return Promise.reject(new Error(`Cannot prerender, ${chkConn.message}`));
251 | }
252 |
253 | // prerendering preconditions:
254 | // 1) whether UA supports spec rules.. If not, fallback to prefetch
255 | // Note: Prerendering supports same-site cross origin with opt-in header
256 | if (!hasSpecRulesSupport()) {
257 | prefetch(urls, true, false, false, eagerness === 'moderate' || eagerness === 'conservative');
258 | return Promise.reject(new Error('This browser does not support the speculation rules API. Falling back to prefetch.'));
259 | }
260 |
261 | for (const url of [].concat(urls)) {
262 | toPrerender.add(url);
263 | }
264 |
265 | // check if both prerender and prefetch exists.. throw a warning but still proceed
266 | if (toPrefetch.size > 0 && !shouldPrerenderAndPrefetch) {
267 | console.warn('[Warning] You are using both prefetching and prerendering on the same document');
268 | }
269 |
270 | const addSpecRules = addSpeculationRules(toPrerender, eagerness);
271 | return addSpecRules === true ? Promise.resolve() : Promise.reject(addSpecRules);
272 | }
273 |
--------------------------------------------------------------------------------
/src/prefetch.mjs:
--------------------------------------------------------------------------------
1 | /**
2 | * Portions copyright 2018 Google Inc.
3 | * Inspired by Gatsby's prefetching logic, with those portions
4 | * remaining MIT. Additions include support for Fetch API,
5 | * XHR switching, SaveData and Effective Connection Type checking.
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | **/
19 |
20 | /**
21 | * Checks if a feature on `link` is natively supported.
22 | * Examples of features include `prefetch` and `preload`.
23 | * @param {Object} link - Link object.
24 | * @return {Boolean} whether the feature is supported
25 | */
26 | function hasPrefetch(link) {
27 | link = document.createElement('link');
28 | return link.relList && link.relList.supports && link.relList.supports('prefetch');
29 | }
30 |
31 | /**
32 | * Fetches a given URL using ` `
33 | * @param {string} url - the URL to fetch
34 | * @param {Boolean} hasCrossorigin - true to set crossorigin="anonymous"
35 | * @return {Object} a Promise
36 | */
37 | function viaDOM(url, hasCrossorigin) {
38 | return new Promise((resolve, reject, link) => {
39 | link = document.createElement('link');
40 | link.rel = 'prefetch';
41 | link.href = url;
42 | if (hasCrossorigin) {
43 | link.setAttribute('crossorigin', 'anonymous');
44 | }
45 |
46 | link.onload = resolve;
47 | link.onerror = reject;
48 |
49 | document.head.appendChild(link);
50 | });
51 | }
52 |
53 | /**
54 | * Fetches a given URL using XMLHttpRequest
55 | * @param {string} url - the URL to fetch
56 | * @param {Boolean} hasCredentials - true to set withCredentials:true
57 | * @return {Object} a Promise
58 | */
59 | function viaXHR(url, hasCredentials) {
60 | return new Promise((resolve, reject, request) => {
61 | request = new XMLHttpRequest();
62 |
63 | request.open('GET', url, request.withCredentials = hasCredentials);
64 |
65 | request.setRequestHeader('Accept', '*/*');
66 |
67 | request.onload = () => {
68 | if (request.status === 200) {
69 | resolve();
70 | } else {
71 | // eslint-disable-next-line prefer-promise-reject-errors
72 | reject();
73 | }
74 | };
75 |
76 | request.send();
77 | });
78 | }
79 |
80 | /**
81 | * Fetches a given URL using the Fetch API. Falls back
82 | * to XMLHttpRequest if the API is not supported.
83 | * @param {string} url - the URL to fetch
84 | * @param {Boolean} hasModeCors - true to set mode:'cors'
85 | * @param {Boolean} hasCredentials - true to set credentials:'include'
86 | * @param {Boolean} isPriority - true to set priority:'high'
87 | * @return {Object} a Promise
88 | */
89 | export function viaFetch(url, hasModeCors, hasCredentials, isPriority) {
90 | // TODO: Investigate using preload for high-priority
91 | // fetches. May have to sniff file-extension to provide
92 | // valid 'as' values. In the future, we may be able to
93 | // use Priority Hints here.
94 | //
95 | // As of 2018, fetch() is high-priority in Chrome
96 | // and medium-priority in Safari.
97 | const options = {headers: {accept: '*/*'}};
98 | if (!hasModeCors) options.mode = 'no-cors';
99 | if (hasCredentials) options.credentials = 'include';
100 | isPriority ? options.priority = 'high' : options.priority = 'low';
101 | return window.fetch ? fetch(url, options) : viaXHR(url, hasCredentials);
102 | }
103 |
104 | /**
105 | * Calls the prefetch function immediately
106 | * or only on the mouseover event.
107 | * @param {Function} callback - original prefetch function
108 | * @param {String} url - url to prefetch
109 | * @param {Boolean} onlyOnMouseover - true to add the mouseover listener
110 | * @return {Object} a Promise
111 | */
112 | export function prefetchOnHover(callback, url, onlyOnMouseover, ...args) {
113 | if (!onlyOnMouseover) return callback(url, ...args);
114 |
115 | const elements = document.querySelectorAll(`a[href="${url}"]`);
116 | const timerMap = new Map();
117 |
118 | for (const el of elements) {
119 | const mouseenterListener = _ => {
120 | const timer = setTimeout(() => {
121 | el.removeEventListener('mouseenter', mouseenterListener);
122 | el.removeEventListener('mouseleave', mouseleaveListener);
123 | return callback(url, ...args);
124 | }, 200);
125 | timerMap.set(el, timer);
126 | };
127 |
128 | const mouseleaveListener = _ => {
129 | const timer = timerMap.get(el);
130 | if (timer) {
131 | clearTimeout(timer);
132 | timerMap.delete(el);
133 | }
134 | };
135 |
136 | el.addEventListener('mouseenter', mouseenterListener);
137 | el.addEventListener('mouseleave', mouseleaveListener);
138 | }
139 | }
140 |
141 | export const supported = hasPrefetch() ? viaDOM : viaFetch;
142 |
--------------------------------------------------------------------------------
/src/prerender.mjs:
--------------------------------------------------------------------------------
1 | /**
2 | * Portions copyright 2018 Google Inc.
3 | * Inspired by Gatsby's prefetching logic, with those portions
4 | * remaining MIT. Additions include support for Fetch API,
5 | * XHR switching, SaveData and Effective Connection Type checking.
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | **/
19 |
20 | /**
21 | * Add a given set of urls to the speculation rules
22 | * @param {Set} urlsToPrerender - the URLs to add to speculation rules
23 | * @param {String} eagerness - prerender eagerness mode
24 | * @return {Boolean|Object} boolean or Error Object
25 | */
26 | export function addSpeculationRules(urlsToPrerender, eagerness) {
27 | const specScript = document.createElement('script');
28 | specScript.type = 'speculationrules';
29 | specScript.text = `{"prerender":[{"source": "list",
30 | "urls": ["${Array.from(urlsToPrerender).join('","')}"],
31 | "eagerness": "${eagerness}"}]}`;
32 | try {
33 | document.head.appendChild(specScript);
34 | } catch (error) {
35 | return error;
36 | }
37 |
38 | return true;
39 | }
40 |
41 | /**
42 | * Check whether UA supports Speculation Rules API
43 | * @return {Boolean} whether UA has support for Speculation Rules API
44 | */
45 | export function hasSpecRulesSupport() {
46 | return HTMLScriptElement.supports('speculationrules');
47 | }
48 |
--------------------------------------------------------------------------------
/src/react-chunks.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2019-2020 Google LLC
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | **/
16 |
17 | import React, {useEffect, useRef, useState} from 'react';
18 | import rmanifest from 'route-manifest';
19 | import {listen} from './quicklink.js';
20 |
21 | const useIntersect = ({root = null, rootMargin, threshold = 0} = {}) => {
22 | const [entry, updateEntry] = useState({});
23 | const [node, setNode] = useState(null);
24 | const observer = useRef(null);
25 |
26 | useEffect(() => {
27 | if (observer.current) observer.current.disconnect();
28 | observer.current = new window.IntersectionObserver(
29 | ([entry]) => updateEntry(entry),
30 | {
31 | root,
32 | rootMargin,
33 | threshold,
34 | },
35 | );
36 |
37 | const {current: currentObserver} = observer;
38 | if (node) currentObserver.observe(node);
39 |
40 | return () => currentObserver.disconnect();
41 | }, [node, root, rootMargin, threshold]);
42 |
43 | return [setNode, entry];
44 | };
45 |
46 | const __defaultAccessor = mix => {
47 | return (mix && mix.href) || mix || '';
48 | };
49 |
50 | const prefetchChunks = (entry, prefetchHandler, accessor = __defaultAccessor) => {
51 | const {files} = rmanifest(window.__rmanifest, entry.pathname);
52 | const chunkURLs = files.map(accessor).filter(Boolean);
53 | if (chunkURLs.length) {
54 | prefetchHandler(chunkURLs);
55 | } else {
56 | // also prefetch regular links in-viewport
57 | prefetchHandler(entry.href);
58 | }
59 | };
60 |
61 | const withQuicklink = (Component, options = {}) => {
62 | // eslint-disable-next-line react/display-name
63 | return props => {
64 | const [ref, entry] = useIntersect({root: document.body.parentElement});
65 | const {intersectionRatio} = entry;
66 |
67 | useEffect(() => {
68 | options.prefetchChunks = prefetchChunks;
69 |
70 | if (intersectionRatio > 0) {
71 | listen(options);
72 | }
73 | }, [intersectionRatio]);
74 |
75 | return (
76 |
77 |
78 |
79 | );
80 | };
81 | };
82 |
83 | export {
84 | withQuicklink,
85 | };
86 |
--------------------------------------------------------------------------------
/src/request-idle-callback.mjs:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018 Google Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | **/
16 |
17 | // RIC and shim for browsers setTimeout() without it
18 | const requestIdleCallback = window.requestIdleCallback ||
19 | function (cb) {
20 | const start = Date.now();
21 | return setTimeout(() => {
22 | cb({
23 | didTimeout: false,
24 | timeRemaining() {
25 | return Math.max(0, 50 - (Date.now() - start));
26 | },
27 | });
28 | }, 1);
29 | };
30 |
31 | export default requestIdleCallback;
32 |
--------------------------------------------------------------------------------
/test/fixtures/1.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Prefetch experiments
7 |
8 |
9 |
10 |
11 |
12 | Page 1
13 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Soluta est sint assumenda corrupti minima aut, magnam
14 | totam beatae ullam ea iste voluptatum iusto expedita animi rem vitae rerum atque nemo!
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/test/fixtures/2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Prefetch experiments
7 |
8 |
9 |
10 |
11 |
12 | Page 2
13 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Soluta est sint assumenda corrupti minima aut, magnam
14 | totam beatae ullam ea iste voluptatum iusto expedita animi rem vitae rerum atque nemo!
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/test/fixtures/3.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Prefetch experiments
7 |
8 |
9 |
10 |
11 |
12 | Page 3
13 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Soluta est sint assumenda corrupti minima aut, magnam
14 | totam beatae ullam ea iste voluptatum iusto expedita animi rem vitae rerum atque nemo!
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/test/fixtures/4.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Prefetch experiments
7 |
8 |
9 |
10 |
11 |
12 | Page 4
13 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Soluta est sint assumenda corrupti minima aut, magnam
14 | totam beatae ullam ea iste voluptatum iusto expedita animi rem vitae rerum atque nemo!
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/test/fixtures/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Prefetch experiments
6 |
7 |
8 |
9 |
10 | Link 1
11 | Link 2
12 | Link 3
13 |
16 | Link 4
17 |
18 |
22 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/test/fixtures/main.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: Roboto, Arial, sans-serif;
3 | }
4 |
5 | .screen {
6 | height: 100vh;
7 | }
8 |
--------------------------------------------------------------------------------
/test/fixtures/rmanifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "/about": [
3 | {
4 | "type": "style",
5 | "href": "/test/static/css/about.00ec0d84.chunk.css"
6 | },
7 | {
8 | "type": "script",
9 | "href": "/test/static/js/about.921ebc84.chunk.js"
10 | }
11 | ],
12 | "/blog": [
13 | {
14 | "type": "style",
15 | "href": "/test/static/css/blog.2a8b6ab6.chunk.css"
16 | },
17 | {
18 | "type": "script",
19 | "href": "/test/static/js/blog.1dcce8a6.chunk.js"
20 | }
21 | ],
22 | "/": [
23 | {
24 | "type": "style",
25 | "href": "/test/static/css/home.6d953f22.chunk.css"
26 | },
27 | {
28 | "type": "script",
29 | "href": "/test/static/js/home.14835906.chunk.js"
30 | },
31 | {
32 | "type": "image",
33 | "href": "/test/static/media/video.b9b6e9e1.svg"
34 | }
35 | ],
36 | "/blog/:title": [
37 | {
38 | "type": "style",
39 | "href": "/test/static/css/article.cb6f97df.chunk.css"
40 | },
41 | {
42 | "type": "script",
43 | "href": "/test/static/js/article.cb6f97df.chunk.js"
44 | }
45 | ],
46 | "*": [
47 | {
48 | "type": "script",
49 | "href": "/test/static/js/6.7f61b1a1.chunk.js"
50 | }
51 | ]
52 | }
53 |
--------------------------------------------------------------------------------
/test/fixtures/test-allow-origin-all.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Prefetch: Allow All Origins
7 |
8 |
9 |
10 |
11 |
12 | Link 1
13 | Link 2
14 | Link 3
15 | Spinner
16 |
19 | Link 4
20 |
21 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/test/fixtures/test-allow-origin.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Prefetch: Allowed Origins
7 |
8 |
9 |
10 |
11 |
12 | Link 1
13 | Link 2
14 | Spinner
15 |
18 | Link 4
19 |
20 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/test/fixtures/test-basic-usage.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Prefetch: Basic Usage
7 |
8 |
9 |
10 |
11 |
12 | Link 1
13 | Link 2
14 | Link 3
15 |
18 | Link 4
19 |
20 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/test/fixtures/test-custom-dom-source.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Prefetch: Custom DOM source
7 |
8 |
9 |
10 |
11 |
12 | Link 1
13 | Link 2
14 | Link 3
15 |
18 | Link 4
19 |
20 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/test/fixtures/test-custom-href-function.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Prefetch: Custom function to build the URL.
7 |
8 |
9 |
10 |
11 |
12 | Link 1
13 | Link 2
14 | Link 3
15 | Link 4
16 |
17 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/test/fixtures/test-delay.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Prefetch: Basic Usage
7 |
8 |
9 |
10 |
11 |
12 | Link 1
13 | Link 2
14 | Link 3
15 |
18 | Link 4
19 |
20 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/test/fixtures/test-es-modules.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Prefetch: ES Modules
7 |
8 |
9 |
10 |
11 |
12 | Link 1
13 | Link 2
14 | Link 3
15 |
18 | Link 4
19 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/test/fixtures/test-ignore-basic.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Prefetch: Ignore Basic
7 |
8 |
9 |
10 |
11 |
12 | Link 1
13 | Link 2
14 | Link 3
15 | Spinner
16 |
19 | Link 4
20 |
21 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/test/fixtures/test-ignore-multiple.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Prefetch: Ignore Multiple
7 |
8 |
9 |
10 |
11 |
12 | Link 1
13 | Link 2
14 | Link 3
15 | Spinner
16 |
19 | Link 4
20 |
21 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/test/fixtures/test-limit.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Prefetch: Basic Usage
7 |
8 |
9 |
10 |
11 |
12 | Link 1
13 | Link 2
14 | Link 3
15 | Link 4
16 |
17 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/test/fixtures/test-node-list.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Prefetch: NodeList case
7 |
8 |
9 |
10 |
11 |
12 | Link 1
13 |
16 |
19 | Link 4
20 |
21 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/test/fixtures/test-prefetch-chunks.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Prefetch: Chunk URL list
7 |
8 |
9 |
10 |
11 |
12 | Home
13 | Blog
14 | About
15 |
18 | Link 4
19 |
20 |
21 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/test/fixtures/test-prefetch-duplicate-shared.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Prefetch: Static URL list
7 |
8 |
9 |
10 |
11 |
12 | Link 2
13 |
16 | Link 4
17 |
18 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/test/fixtures/test-prefetch-duplicate.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Prefetch: Static URL list
7 |
8 |
9 |
10 |
11 |
12 | Link 1
13 | Link 2
14 | Link 3
15 |
18 | Link 4
19 |
20 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/test/fixtures/test-prefetch-multiple.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Prefetch: Static URL list
7 |
8 |
9 |
10 |
11 |
12 | Link 1
13 | Link 2
14 | Link 3
15 |
18 | Link 4
19 |
20 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/test/fixtures/test-prefetch-single.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Prefetch: Static URL list
7 |
8 |
9 |
10 |
11 |
12 | Link 1
13 | Link 2
14 | Link 3
15 |
18 | Link 4
19 |
20 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/test/fixtures/test-prerender-andPrefetch.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Prefetch: Basic Usage
7 |
8 |
9 |
10 |
11 |
12 | Link 1
13 | Link 2
14 | Link 3
15 |
18 | Link 4
19 |
20 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/test/fixtures/test-prerender-only.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Prefetch: Basic Usage
7 |
8 |
9 |
10 |
11 |
12 | Link 1
13 | Link 2
14 | Link 3
15 |
18 | Link 4
19 |
20 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/test/fixtures/test-prerender-wrapper-multiple.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Prefetch: Basic Usage
7 |
8 |
9 |
10 |
11 |
12 | Link 1
13 | Link 2
14 | Link 3
15 |
18 | Link 4
19 |
20 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/test/fixtures/test-prerender-wrapper-single.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Prefetch: Basic Usage
7 |
8 |
9 |
10 |
11 |
12 | Link 1
13 | Link 2
14 | Link 3
15 |
18 | Link 4
19 |
20 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/test/fixtures/test-same-origin.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Prefetch: Same Origin
7 |
8 |
9 |
10 |
11 |
12 | Link 1
13 | Link 2
14 | Spinner
15 |
18 | Link 4
19 |
20 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/test/fixtures/test-threshold.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Prefetch: Basic Usage
7 |
8 |
9 |
29 |
30 |
31 |
32 |
46 |
47 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/test/fixtures/test-throttle.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Prefetch: Basic Usage
7 |
8 |
9 |
10 |
11 |
12 | Link 1
13 | Link 2
14 | Link 3
15 | Link 4
16 |
17 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/translations/zh-cn/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | # quicklink
16 |
17 | > 可以在空闲时间预获取页面可视区域(以下简称视区)内的链接,加快后续加载速度。
18 |
19 | ## 工作原理
20 |
21 | Quicklink 通过以下方式加快后续页面的加载速度:
22 |
23 | - **检测视区中的链接**(使用 [Intersection Observer](https://developer.mozilla.org/zh-CN/docs/Web/API/Intersection_Observer_API))。
24 | - **等待浏览器空闲**(使用 [requestIdleCallback](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback))。
25 | - **确认用户并未处于慢速连接**(使用 `navigator.connection.effectiveType`)或启用省流模式(使用 `navigator.connection.saveData`)。
26 | - **预获取视区内的 URL**(使用 [` `](https://www.w3.org/TR/resource-hints/#prefetch) 或 XHR)。可根据请求优先级进行控制(若支持 fetch() 可进行切换)。
27 |
28 | ## 开发原因
29 |
30 | 该项目旨在为网站提供一套解决方案,预获取处于用户视区中的链接,同时保持极小的体积(**minify/gzip 后 <1KB**)。
31 |
32 | ## 安装方法
33 |
34 | [node](http://nodejs.org) 或 [npm](https://npmjs.com) 用户:
35 |
36 | ```sh
37 | npm install --save quicklink
38 | ```
39 |
40 | 或者从 [unpkg.com/quicklink](https://unpkg.com/quicklink) 获取 `quicklink`。
41 |
42 | ## 用法
43 |
44 | 初始化后,`quicklink` 将自动在闲时预获取视区内的链接 URL。
45 |
46 | 快速上手:
47 |
48 | ```html
49 |
50 |
51 |
52 |
55 | ```
56 |
57 | 举个例子,你可以在 `load` 方法触发之后进行初始化:
58 |
59 | ```html
60 |
65 | ```
66 |
67 | 或者导入 ES 模块:
68 |
69 | ```js
70 | import quicklink from "dist/quicklink.mjs";
71 | quicklink();
72 | ```
73 |
74 | 以上方法适用于多页网站。单页应用可以搭配 router 使用 quicklink:
75 |
76 | - 进入新路由地址后,调用 `quicklink()`。
77 | - 针对特定 DOM 元素/组件调用 `quicklink()`。
78 | - 调用 `quicklink({urls:[...]})`,传入自定义 URL 集合进行预获取。
79 |
80 | ## API
81 |
82 | `quicklink` 接受带有以下参数的 option 对象(可选):
83 |
84 | - `el`:指定需要预获取的 DOM 元素视区。
85 | - `urls`:预获取的静态 URL 数组(若此参数非空,则不会检测视区中 `document` 或 DOM 元素的链接)。
86 | - `timeout`:整型数,为 requestIdleCallback 设置超时。浏览器必须在此之前进行预获取(以毫秒为单位), 默认取 2 秒。
87 | - `timeoutFn`:指定超时处理函数。默认为 requestIdleCallback。也可以替换为 [networkIdleCallback](https://github.com/pastelsky/network-idle-callback)(详见 demo)等自定义函数。
88 | - `priority`:布尔值,指定 fetch 的优先级。默认为 `false`。若配置为 `true` 将会尝试使用 `fetch()` API(而非 rel=prefetch)。
89 | - `origins`: 静态字符串数组,包含允许进行预获取操作的 URL 主机名。默认为同域请求源,可阻止跨域请求。
90 | - `ignores`: RegExp(正则表达式),Function(函数)或者 Array(数组),用于进一步确定某 URL 是否可被预获取。会在匹配请求源之后执行。
91 |
92 | 待探索:
93 |
94 | - 支持资源扩展名检测及使用 [rel=preload](https://w3c.github.io/preload/) 获取高优资源。
95 | - 使用 [Priority Hints](https://github.com/WICG/priority-hints) 进行重要性提示。
96 |
97 | ## Polyfills
98 |
99 | `quicklink`:
100 |
101 | - [requestIdleCallback](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback) 的一个非常小的回退。
102 | - Requires `IntersectionObserver` to be supported. This is [supported in all modern browsers](https://caniuse.com/intersectionobserver), however you can use the [Intersection Observer polyfill](https://github.com/GoogleChromeLabs/intersection-observer) to support legacy browsers if needed.
103 |
104 | ## 方法
105 |
106 | ### 为预获取操作自定义超时时间
107 |
108 | 默认超时时间为 2 秒(通过 `requestIdleCallback`),这里我们重写为 4 秒:
109 |
110 | ```js
111 | quicklink({
112 | timeout: 4000,
113 | });
114 | ```
115 |
116 | ### 设置用于检测链接的 DOM 元素
117 |
118 | 默认值为 `document`。
119 |
120 | ```js
121 | const elem = document.getElementById('carousel');
122 | quicklink({
123 | el: elem,
124 | });
125 | ```
126 |
127 | ### 自定义预获取 URL 数组
128 |
129 | 如果你想指定用于预获取的静态 URL 列表,而不是视区内的链接,你可以使用自定义 URL。
130 |
131 | ```js
132 | quicklink({
133 | urls: ['2.html', '3.html', '4.js'],
134 | });
135 | ```
136 |
137 | ### 为预获取设置请求优先级
138 |
139 | 默认为低优先级(`rel=prefetch` 或 XHR)。对于高优先级(`priority: true`)的操作,尝试使用 `fetch()` 或退阶使用 XHR。
140 |
141 | ```js
142 | quicklink({ priority: true });
143 | ```
144 |
145 | ### 自定义受允许请求源列表
146 |
147 | 指定可被预获取的主机名列表,默认情况下仅允许同源主机名。
148 |
149 | > **划重点**:你还得加上自己的主机名!
150 |
151 | ```js
152 | quicklink({
153 | origins: [
154 | // 添加我自己的
155 | 'my-website.com',
156 | 'api.my-website.com',
157 | // 添加第三方的
158 | 'other-website.com',
159 | 'example.com',
160 | // ...
161 | ],
162 | });
163 | ```
164 |
165 | ### 允许所有源
166 |
167 | 允许所有跨域请求。
168 |
169 | > **注意**:可能会导致 [CORB](https://chromium.googlesource.com/chromium/src/+/main/services/network/cross_origin_read_blocking_explainer.md) 以及 [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) 问题!
170 |
171 | ```js
172 | quicklink({
173 | origins: true,
174 | // 或者
175 | origins: [],
176 | });
177 | ```
178 |
179 | ### 自定义忽略模式
180 |
181 | 以下过滤器会在匹配 `origin` 之后执行。Ignores 在避免大型文件下载或 DOM 属性动态响应时十分有用!
182 |
183 | ```js
184 | // 默认启用同源限制。
185 | //
186 | // 这个示例会忽略所有对以下模式的请求:
187 | // - 所有 "/api/*" 路径名
188 | // - 所有 ".zip" 扩展名
189 | // - 所有带有 noprefetch 扩展名的 标签
190 | //
191 | quicklink({
192 | ignores: [
193 | /\/api\/?/,
194 | uri => uri.includes('.zip'),
195 | (uri, elem) => elem.hasAttribute('noprefetch'),
196 | ],
197 | });
198 | ```
199 |
200 | 也许你还想忽略那些包含 URL fragment 的 URL(比如 `index.html#top`),不对它们进行预获取。那么对于在页面中使用了锚点标题,或者在单页应用设置了 URL fragment 的情况,这个功能可以避免对类似的 URL 进行预获取。
201 |
202 | 使用 `ignores` 来实现:
203 |
204 | ```js
205 | quicklink({
206 | ignores: [
207 | uri => uri.includes('#'),
208 | // 或者使用正则表达式: /#(.+)/
209 | // 或者使用元素匹配: (uri, elem) => !!elem.hash
210 | ],
211 | });
212 | ```
213 |
214 | ## 浏览器支持
215 |
216 | quicklink 提供的预获取是[渐进增强](https://www.smashingmagazine.com/2009/04/progressive-enhancement-what-it-is-and-how-to-use-it/)的,跨浏览器支持如下:
217 |
218 | - 不使用 polyfills:Chrome,Firefox,Edge,Opera,Android Browser,Samsung Internet 支持。
219 | - 使用 [Intersection Observer polyfill](https://github.com/GoogleChromeLabs/intersection-observer)(gzipped/minified 后大约 6KB):Safari,IE9+ 支持。
220 |
221 | 部分功能支持分层实现:
222 |
223 | - 用于检查用户是否处于低速联网状态(通过`navigator.connection.effectiveType`)的 [Network Information API](https://wicg.github.io/netinfo/) 仅适用于 [Chrome 61+ 和 Opera 57+](https://caniuse.com/#feat=netinfo)。
224 |
225 | - 如果 `{priority:true}` 和 [fetch()](https://fetch.spec.whatwg.org/) 均不可用,则将使用 XHR。
226 |
227 | ## 直接使用预获取器
228 |
229 | `quicklink` 包含一个预获取器,可以单独导入其他项目中。方法是先将 `quicklink` 作为依赖项安装,然后按如下方式使用:
230 |
231 | ```html
232 |
239 | ```
240 |
241 | ## Demo
242 |
243 | 这个 [WebPageTest demo](https://www.webpagetest.org/video/view.php?id=181212_4c294265117680f2636676721cc886613fe2eede&data=1) 演示了 quicklink 的预获取功能,它将页面加载性能提高了 4 秒! [这个 Youtube 视频](https://youtu.be/rQ75YEbJicw) 对使用预获取之前和之后进行了对比。
244 |
245 | 为了做演示,我们在 Firebase 上部署了一个 [Google Blog](https://blog.google/),接着部署了另一个在主页添加了 quicklink 的版本,测试从主页导航到一个自动预获取的文章所用时间。结果表明预获取版本加载速度更快。
246 |
247 | 请注意:这绝不是对这项技术优缺点的详尽测试,只是演示了该方法可能带来的潜在改进。你自己的实现可能不尽相同。
248 |
249 | ## 相关项目
250 |
251 | - 在用 [Gatsby](https://gatsbyjs.org/) 吗? 现在可以免费下载它了。它使用 `Intersection Observer` 预获取视图中的所有链接,本项目灵感亦来源于此。
252 | - 想要更加数据驱动的方案吗? 参见 [Guess.js](https://guess-js.github.io/)。它根据用户上网方式,使用数据分析和机器学习来预获取资源。它还有 [Webpack](https://www.npmjs.com/package/guess-webpack) 和 [Gatsby](https://www.gatsbyjs.org/docs/optimize-prefetching-with-guessjs/) 的插件。
253 |
254 | ## 许可证
255 |
256 | 本项目已获得 Apache-2.0 许可。
257 |
--------------------------------------------------------------------------------