├── .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 |
27 | CSS 28 |
29 |
30 |
31 | Link 4 32 |
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 |
29 | CSS 30 |
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 |
4 |
5 | Place this snippet in your head or just before your close body tag: 6 |
7 | 8 |
9 | {% highlight "html" %} 10 | 11 | 16 | {% endhighlight %} 17 |
18 | 19 |
20 | 21 |
22 | 23 |
24 | Copied. Now place it just before </body> on your pages. 25 |
26 |
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 |
5 | {%- for trustedByLogo in site.trustedByLogos %} 6 | 7 | {# TODO: responsive sizing like "used-with" logos #} 8 | {{ trustedByLogo.companyName }} 9 | {{ trustedByLogo.companyName }} 10 | 11 | {%- endfor %} 12 |
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 | {{ framework.title }} 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 | Prefetch pages a user may need in the future to improve subsequent page loads 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 | 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 | ![](/assets/images/graphs/what-is-prefetch-svgomg.svg) 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 | ![](/assets/images/graphs/prefetch-quicklink-svgomg.svg) 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 | Image shows that use of quicklink improves navigation for a site by 1 second 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 | Image shows chrome dev tools inspection for site not using quicklink 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 | Image shows chrome dev tools inspection for site using quicklink 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 | Image shows the site using quicklink along with prefetch cache improves data fetching time by 97 percent 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 | The gif shows comparison between same site being loaded with quicklink takes 1 second less than unoptimised version 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 | Image shows that data for loading links available on the page being viewed are loaded beforehand by quicklink 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 | Image shows that quicklink takes only 2 milliseconds to load a site using prefetch cache 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 | Image shows chrome dev tools inspection for site not using quicklink 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 | Image shows chrome dev tools inspection for site using quicklink 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 | Image shows the site using quicklink along with prefetch cache improves data fetching time by 97 percent 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 | Image shows WPT results for site loaded with quicklink are much faster than without quicklink 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 | Image shows that use of quicklink improves load time for a site by 1.3 seconds 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 | The gif shows comparison between same site being loaded with quicklink takes 1 second less than the unoptimised version 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 |
14 | CSS 15 |
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 |
17 | CSS 18 |
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 |
16 | CSS 17 |
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 |
16 | CSS 17 |
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 |
16 | CSS 17 |
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 |
16 | CSS 17 |
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 |
16 | CSS 17 |
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 |
17 | CSS 18 |
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 |
17 | CSS 18 |
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 |
14 | Link 2 15 |
16 |
17 | Link 3 18 |
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 |
16 | CSS 17 |
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 |
14 | CSS 15 |
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 |
16 | CSS 17 |
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 |
16 | CSS 17 |
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 |
16 | CSS 17 |
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 |
16 | CSS 17 |
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 |
16 | CSS 17 |
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 |
16 | CSS 17 |
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 |
16 | CSS 17 |
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 |
16 | CSS 17 |
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 | npm 6 | 7 | 8 | gzip size 9 | 10 | 11 | ci 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 | --------------------------------------------------------------------------------