Repository: GoogleChromeLabs/quicklink Branch: main Commit: 7b4198bb9dee Files: 119 Total size: 246.7 KB Directory structure: gitextract_udyormir/ ├── .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 ├── demos/ │ ├── basic.html │ ├── hrefFn/ │ │ ├── 2.html │ │ ├── 2.json │ │ └── hrefFn_demo.html │ ├── network-idle.html │ ├── network-idle.js │ ├── news/ │ │ └── README.md │ ├── news-workbox/ │ │ └── README.md │ ├── spa/ │ │ └── README.md │ └── sw.js ├── 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.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 │ │ │ ├── footer.njk │ │ │ ├── head.njk │ │ │ ├── header.njk │ │ │ ├── highlighted-section-wrapper.njk │ │ │ ├── normal-section-wrapper.njk │ │ │ └── social.njk │ │ ├── api.njk │ │ ├── approach.njk │ │ ├── assets/ │ │ │ ├── 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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc.js ================================================ 'use strict'; module.exports = { presets: [ '@babel/preset-react', [ '@babel/preset-env', { loose: true, bugfixes: true, }, ], ], }; ================================================ FILE: .editorconfig ================================================ # https://editorconfig.org/ root = true [*] charset = utf-8 end_of_line = lf indent_size = 2 indent_style = space insert_final_newline = true trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = false ================================================ FILE: .eslintignore ================================================ **/*.min.js **/dist/ **/build/ **/vendor/ # explicitly include dot js files !.*.js ================================================ FILE: .eslintrc.js ================================================ 'use strict'; module.exports = { env: { browser: true, es6: true, node: true, }, parserOptions: { sourceType: 'script', ecmaVersion: 2017, }, extends: [ 'google', 'plugin:react/recommended', ], settings: { react: { version: 'detect', }, }, rules: { 'max-len': [ 'warn', { // 130 on GitHub, 80 on npmjs.org for README.md code blocks code: 130, }, ], 'arrow-parens': [ 'error', 'as-needed', ], 'space-before-function-paren': [ 'error', { anonymous: 'always', named: 'never', }, ], 'no-negated-condition': 'warn', 'no-const-assign': 'error', 'prefer-destructuring': [ 'off', { object: true, array: false, }, ], 'prefer-template': 'error', 'strict': 'error', 'spaced-comment': [ 'error', 'always', { exceptions: [ '/', ], }, ], }, overrides: [ { files: [ 'src/**', ], parserOptions: { sourceType: 'module', }, }, { files: [ 'site/**', ], env: { node: false, }, parserOptions: { sourceType: 'script', }, rules: { 'require-jsdoc': 'off', 'strict': 'error', }, }, ], }; ================================================ FILE: .gitattributes ================================================ # Enforce Unix newlines * text=auto eol=lf *.njk linguist-language=js ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Something is not working as expected labels: --- **Before you start** 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: **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior. **Expected behavior** A clear and concise description of what you expected to happen. **Version:** * OS w/ version: [e.g. iOS 12] * Browser w/ version [e.g. Chrome 75] **Additional context, screenshots, screencasts** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project labels: --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: monthly groups: github-actions: patterns: - "*" ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: - main pull_request: workflow_dispatch: env: FORCE_COLOR: 2 NODE: 24 permissions: contents: read jobs: run: runs-on: ubuntu-latest steps: - name: Clone repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Node.js uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: ${{ env.NODE }} cache: npm - name: Disable AppArmor run: echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns - name: Install dependencies run: npm ci - name: Run tests run: npm test ================================================ FILE: .github/workflows/codeql.yml ================================================ name: "CodeQL" on: push: branches: - main pull_request: branches: - main schedule: - cron: "0 0 * * 0" workflow_dispatch: jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Initialize CodeQL uses: github/codeql-action/init@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6 with: languages: "javascript" queries: +security-and-quality - name: Autobuild uses: github/codeql-action/autobuild@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6 with: category: "/language:javascript" ================================================ FILE: .github/workflows/lint.yml ================================================ name: Lint on: push: branches: - main pull_request: workflow_dispatch: env: FORCE_COLOR: 2 NODE: 24 permissions: contents: read jobs: lint: runs-on: ubuntu-latest steps: - name: Clone repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Node.js uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: "${{ env.NODE }}" cache: npm - name: Install dependencies run: npm ci - name: Lint run: npm run lint ================================================ FILE: .github/workflows/site.yml ================================================ name: Site on: push: branches: - main pull_request: workflow_dispatch: env: FORCE_COLOR: 2 NODE: 24 permissions: contents: read defaults: run: working-directory: site jobs: build: runs-on: ubuntu-latest steps: - name: Clone repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Node.js uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: ${{ env.NODE }} cache: npm - name: Install dependencies run: npm ci - name: Run tests run: npm test ================================================ FILE: .github/workflows/size-limit.yml ================================================ name: Size limit on: push: branches: - main pull_request: workflow_dispatch: env: FORCE_COLOR: 2 NODE: 24 permissions: contents: read jobs: size-limit: runs-on: ubuntu-latest steps: - name: Clone repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Set up Node.js uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: ${{ env.NODE }} cache: npm - name: Install dependencies run: npm ci - name: Build run: npm run build - name: Run size-limit run: npm run size ================================================ FILE: .gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* dist build .DS_Store # Runtime data pids *.pid *.seed *.pid.lock # Coverage directory used by tools like istanbul coverage # Dependency directories node_modules/ # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env # Local Netlify folder .netlify ================================================ FILE: .size-limit.json ================================================ [ { "path": "dist/quicklink.js", "limit": "2.5 kB", "gzip": true }, { "path": "dist/quicklink.mjs", "limit": "2.5 kB", "gzip": true }, { "path": "dist/quicklink.modern.mjs", "limit": "2 kB", "gzip": true }, { "path": "dist/quicklink.umd.js", "limit": "2.55 kB", "gzip": true } ] ================================================ FILE: CHANGELOG.md ================================================ ## 2.3.0 (2022-08-05) * 2.3.0 ([6189deb](https://github.com/GoogleChromeLabs/quicklink/commit/6189deb)) * Add support for same-site prerendering with Speculation Rules API (#258) ([3d26f40](https://github.com/GoogleChromeLabs/quicklink/commit/3d26f40)), closes [#258](https://github.com/GoogleChromeLabs/quicklink/issues/258) * Create node.js.yml ([2f69b42](https://github.com/GoogleChromeLabs/quicklink/commit/2f69b42)) * Fix Markdown (#250) ([13e2f82](https://github.com/GoogleChromeLabs/quicklink/commit/13e2f82)), closes [#250](https://github.com/GoogleChromeLabs/quicklink/issues/250) * Update node.js.yml ([312dbbb](https://github.com/GoogleChromeLabs/quicklink/commit/312dbbb)) * chore: Modify repository default languages (#264) ([5a0d396](https://github.com/GoogleChromeLabs/quicklink/commit/5a0d396)), closes [#264](https://github.com/GoogleChromeLabs/quicklink/issues/264) ## 2.2.0 (2021-06-18) * [release] additions to changelog ([6ac410c](https://github.com/GoogleChromeLabs/quicklink/commit/6ac410c)) * [release] bump core to 2.2.0 ([418eb50](https://github.com/GoogleChromeLabs/quicklink/commit/418eb50)) * [release] bump site to 2.2.0 ([22a055c](https://github.com/GoogleChromeLabs/quicklink/commit/22a055c)) * [release] update changelog for 2.2.0 ([bb1a648](https://github.com/GoogleChromeLabs/quicklink/commit/bb1a648)) * [site] reorder logos ([09907bb](https://github.com/GoogleChromeLabs/quicklink/commit/09907bb)) * [site] update version ([7babbda](https://github.com/GoogleChromeLabs/quicklink/commit/7babbda)) * Add Magento Quicklink module (Readme + Site) (#216) ([c77e057](https://github.com/GoogleChromeLabs/quicklink/commit/c77e057)), closes [#216](https://github.com/GoogleChromeLabs/quicklink/issues/216) * Added `threshold` option to allow users select the % of link areas that entered the viewport before ([d3746e1](https://github.com/GoogleChromeLabs/quicklink/commit/d3746e1)), closes [#214](https://github.com/GoogleChromeLabs/quicklink/issues/214) * Instructions to debug Quicklink ([2b3dc21](https://github.com/GoogleChromeLabs/quicklink/commit/2b3dc21)) ## 2.1.0 (2021-02-07) * [docs] Add React SPA demos to repo and site (#179) ([179cb56](https://github.com/GoogleChromeLabs/quicklink/commit/179cb56)), closes [#179](https://github.com/GoogleChromeLabs/quicklink/issues/179) * [docs] drop highlightjs reference and link up prism styles ([b91872b](https://github.com/GoogleChromeLabs/quicklink/commit/b91872b)) * [docs] Fix quicklink logo ([25829de](https://github.com/GoogleChromeLabs/quicklink/commit/25829de)) * [docs] minor tweak to header text ([cd31382](https://github.com/GoogleChromeLabs/quicklink/commit/cd31382)) * [docs] refactor api docs syntax highlighting ([59f7eca](https://github.com/GoogleChromeLabs/quicklink/commit/59f7eca)) * [docs] refactor copy-snippets syntax highlighting ([5445a8b](https://github.com/GoogleChromeLabs/quicklink/commit/5445a8b)) * [docs] refactor measure docs syntax highlighting ([eaa87ac](https://github.com/GoogleChromeLabs/quicklink/commit/eaa87ac)) * [docs] refactor over-prefetching docs syntax highlighting ([6ec1287](https://github.com/GoogleChromeLabs/quicklink/commit/6ec1287)) * [docs] refactor react docs syntax highlighting ([ba5984d](https://github.com/GoogleChromeLabs/quicklink/commit/ba5984d)) * [docs] refactor usage docs syntax highlighting ([25367f4](https://github.com/GoogleChromeLabs/quicklink/commit/25367f4)) * [docs] remainder of syntax highlighting fixes ([cf32a85](https://github.com/GoogleChromeLabs/quicklink/commit/cf32a85)) * [docs] remove highlightjs ([7ff8c8f](https://github.com/GoogleChromeLabs/quicklink/commit/7ff8c8f)) * [docs] Update CHANGELOG ([c4a4726](https://github.com/GoogleChromeLabs/quicklink/commit/c4a4726)) * [docs] various style theme improvements ([06786fb](https://github.com/GoogleChromeLabs/quicklink/commit/06786fb)) * [feat] (options): Add a `hrefFn` option to build the URL to prefetch. (#201) ([ee072d4](https://github.com/GoogleChromeLabs/quicklink/commit/ee072d4)), closes [#201](https://github.com/GoogleChromeLabs/quicklink/issues/201) * [feat] `delay` option to reduce impact on CDNs and servers [alternative without data-attributes] (#2 ([5cdf569](https://github.com/GoogleChromeLabs/quicklink/commit/5cdf569)), closes [#217](https://github.com/GoogleChromeLabs/quicklink/issues/217) * [infra] add eleventy syntax highlighting ([b48f80d](https://github.com/GoogleChromeLabs/quicklink/commit/b48f80d)) * [infra] Add site to firebase hosting config ([fe43486](https://github.com/GoogleChromeLabs/quicklink/commit/fe43486)) * [infra] bump version to 2.1.0 ([81232e8](https://github.com/GoogleChromeLabs/quicklink/commit/81232e8)) * Added Ray-Ban and Oakley from Luxxotica to trustedByLogos section (#202) ([b4494b0](https://github.com/GoogleChromeLabs/quicklink/commit/b4494b0)), closes [#202](https://github.com/GoogleChromeLabs/quicklink/issues/202) * Correct typo, duplicate "passing" (#185) ([932f655](https://github.com/GoogleChromeLabs/quicklink/commit/932f655)), closes [#185](https://github.com/GoogleChromeLabs/quicklink/issues/185) * Fix issues typo in 'network-idle.js' (#218) ([534e7b3](https://github.com/GoogleChromeLabs/quicklink/commit/534e7b3)), closes [#218](https://github.com/GoogleChromeLabs/quicklink/issues/218) * New demo page (#205) ([5205d62](https://github.com/GoogleChromeLabs/quicklink/commit/5205d62)), closes [#205](https://github.com/GoogleChromeLabs/quicklink/issues/205) * update homepage url in package.json (#184) ([172275b](https://github.com/GoogleChromeLabs/quicklink/commit/172275b)), closes [#184](https://github.com/GoogleChromeLabs/quicklink/issues/184) * Updating broken link ([224df77](https://github.com/GoogleChromeLabs/quicklink/commit/224df77)) * Fix: Cannot read property 'then' of undefined (#188) ([a8872b8](https://github.com/GoogleChromeLabs/quicklink/commit/a8872b8)), closes [#188](https://github.com/GoogleChromeLabs/quicklink/issues/188) * chore(deps): bump http-proxy from 1.18.0 to 1.18.1 (#200) ([0aa5157](https://github.com/GoogleChromeLabs/quicklink/commit/0aa5157)), closes [#200](https://github.com/GoogleChromeLabs/quicklink/issues/200) ## 2.0.0 (2020-05-07) * [infra] Add site updates for 2.0.0 (#178) ([8aa512b](https://github.com/GoogleChromeLabs/quicklink/commit/8aa512b)), closes [#178](https://github.com/GoogleChromeLabs/quicklink/issues/178) * [infra] Bump versions to 2.0.0 ([08d9a39](https://github.com/GoogleChromeLabs/quicklink/commit/08d9a39)) * 2.0.0 ([735caf6](https://github.com/GoogleChromeLabs/quicklink/commit/735caf6)) ## 2.0.0-beta (2020-04-24) * [core] Adds withQuicklink HOC (#172) ([89cd6a9](https://github.com/GoogleChromeLabs/quicklink/commit/89cd6a9)), closes [#172](https://github.com/GoogleChromeLabs/quicklink/issues/172) [#175](https://github.com/GoogleChromeLabs/quicklink/issues/175) [#176](https://github.com/GoogleChromeLabs/quicklink/issues/176) [#177](https://github.com/GoogleChromeLabs/quicklink/issues/177) * [core] Introduce prefetch chunks build (#171) ([301aedb](https://github.com/GoogleChromeLabs/quicklink/commit/301aedb)), closes [#171](https://github.com/GoogleChromeLabs/quicklink/issues/171) [#168](https://github.com/GoogleChromeLabs/quicklink/issues/168) [#169](https://github.com/GoogleChromeLabs/quicklink/issues/169) * [docs] Add initial site ([e934a2b](https://github.com/GoogleChromeLabs/quicklink/commit/e934a2b)) * [docs] Add notes on double-keyed caching ([03d3c97](https://github.com/GoogleChromeLabs/quicklink/commit/03d3c97)) * [docs] Added Newegg to trustedByLogos section (#153) ([453a661](https://github.com/GoogleChromeLabs/quicklink/commit/453a661)), closes [#153](https://github.com/GoogleChromeLabs/quicklink/issues/153) * [docs] Bugfix/syntax highlighting site (#147) ([0f644e7](https://github.com/GoogleChromeLabs/quicklink/commit/0f644e7)), closes [#147](https://github.com/GoogleChromeLabs/quicklink/issues/147) * [docs] Compress site resources w/ImageOptim ([179e18e](https://github.com/GoogleChromeLabs/quicklink/commit/179e18e)) * [docs] Measuring impact of QuickLink in sites guide (#146) ([2ce99e3](https://github.com/GoogleChromeLabs/quicklink/commit/2ce99e3)), closes [#146](https://github.com/GoogleChromeLabs/quicklink/issues/146) * [docs] New section "Quicklink extension" for home page. (#150) ([468c231](https://github.com/GoogleChromeLabs/quicklink/commit/468c231)), closes [#150](https://github.com/GoogleChromeLabs/quicklink/issues/150) * [docs] Over-prefetching section for home page (#148) ([75aa643](https://github.com/GoogleChromeLabs/quicklink/commit/75aa643)), closes [#148](https://github.com/GoogleChromeLabs/quicklink/issues/148) * [docs] Update the Angular logo (#158) ([836f170](https://github.com/GoogleChromeLabs/quicklink/commit/836f170)), closes [#158](https://github.com/GoogleChromeLabs/quicklink/issues/158) * [infra] Add firebase deployment ([89ab866](https://github.com/GoogleChromeLabs/quicklink/commit/89ab866)) * [infra] Fix tests (#142) ([6644860](https://github.com/GoogleChromeLabs/quicklink/commit/6644860)), closes [#142](https://github.com/GoogleChromeLabs/quicklink/issues/142) * [infra] Publish dist directory (#98) ([9cdf06f](https://github.com/GoogleChromeLabs/quicklink/commit/9cdf06f)), closes [#98](https://github.com/GoogleChromeLabs/quicklink/issues/98) * 2.0.0-beta ([185d1e8](https://github.com/GoogleChromeLabs/quicklink/commit/185d1e8)) * Add support for Quicklink 2.0.0-alpha ([7c7c917](https://github.com/GoogleChromeLabs/quicklink/commit/7c7c917)) * Add twitter metadata ([74d3224](https://github.com/GoogleChromeLabs/quicklink/commit/74d3224)) * Adding SPA section to README.md ([ea3229a](https://github.com/GoogleChromeLabs/quicklink/commit/ea3229a)) * Fix typo on README (#127) ([5acb27e](https://github.com/GoogleChromeLabs/quicklink/commit/5acb27e)), closes [#127](https://github.com/GoogleChromeLabs/quicklink/issues/127) * package.json: bump version to 2.0.0-alpha ([d5d5ca5](https://github.com/GoogleChromeLabs/quicklink/commit/d5d5ca5)) * Updates to initial site (#144) ([3f796f6](https://github.com/GoogleChromeLabs/quicklink/commit/3f796f6)), closes [#144](https://github.com/GoogleChromeLabs/quicklink/issues/144) * chore(deps): bump acorn from 6.4.0 to 6.4.1 (#167) ([8b62949](https://github.com/GoogleChromeLabs/quicklink/commit/8b62949)), closes [#167](https://github.com/GoogleChromeLabs/quicklink/issues/167) ## 2.0.0-alpha (2019-09-25) * (docs) update to remove TODOs from README ([8cd1183](https://github.com/GoogleChromeLabs/quicklink/commit/8cd1183)) * Update docs with ad-related considerations (#122) ([7ac672f](https://github.com/GoogleChromeLabs/quicklink/commit/7ac672f)), closes [#122](https://github.com/GoogleChromeLabs/quicklink/issues/122) * Major: Rework exports; Add `throttle` and `limit` options (#120) ([4044de0](https://github.com/GoogleChromeLabs/quicklink/commit/4044de0)), closes [#120](https://github.com/GoogleChromeLabs/quicklink/issues/120) [#1](https://github.com/GoogleChromeLabs/quicklink/issues/1) ## 1.0.1 (2019-08-17) * (demo) Introduce new demos for basic + workbox usage ([9eb7fa0](https://github.com/GoogleChromeLabs/quicklink/commit/9eb7fa0)) * (demos) Add new demos to README ([85729aa](https://github.com/GoogleChromeLabs/quicklink/commit/85729aa)) * (docs) Update README: note on session stitching ([ba9795c](https://github.com/GoogleChromeLabs/quicklink/commit/ba9795c)) * (infra) Bump version to 1.0.1 ([d75188d](https://github.com/GoogleChromeLabs/quicklink/commit/d75188d)) * A few quick size optimizations ([201c217](https://github.com/GoogleChromeLabs/quicklink/commit/201c217)) * Add homepage and bugs links to package.json (#116) ([002645b](https://github.com/GoogleChromeLabs/quicklink/commit/002645b)), closes [#116](https://github.com/GoogleChromeLabs/quicklink/issues/116) * Add note to README about Drupal module. ([d94ff80](https://github.com/GoogleChromeLabs/quicklink/commit/d94ff80)) * Check if `requestIdleCallback` exists in `window` (#112) ([089da91](https://github.com/GoogleChromeLabs/quicklink/commit/089da91)), closes [#112](https://github.com/GoogleChromeLabs/quicklink/issues/112) * Create .editorconfig (#61) ([beae09b](https://github.com/GoogleChromeLabs/quicklink/commit/beae09b)), closes [#61](https://github.com/GoogleChromeLabs/quicklink/issues/61) * Fail silently, don’t throw an error, when IntersectionObserver isn’t available (#113) ([32e5b61](https://github.com/GoogleChromeLabs/quicklink/commit/32e5b61)), closes [#113](https://github.com/GoogleChromeLabs/quicklink/issues/113) * Fix ES Module import syntax ([a2b90ff](https://github.com/GoogleChromeLabs/quicklink/commit/a2b90ff)) * GitHub Issue Templates (#109) ([2e6401e](https://github.com/GoogleChromeLabs/quicklink/commit/2e6401e)), closes [#109](https://github.com/GoogleChromeLabs/quicklink/issues/109) * HTML formatting tidy for Tests & Demos (#114) ([f4aef2e](https://github.com/GoogleChromeLabs/quicklink/commit/f4aef2e)), closes [#114](https://github.com/GoogleChromeLabs/quicklink/issues/114) * HTTPS link to gruntjs.com (#100) ([47f49e7](https://github.com/GoogleChromeLabs/quicklink/commit/47f49e7)), closes [#100](https://github.com/GoogleChromeLabs/quicklink/issues/100) * HTTPS link to nodejs.org (#110) ([cf9551e](https://github.com/GoogleChromeLabs/quicklink/commit/cf9551e)), closes [#110](https://github.com/GoogleChromeLabs/quicklink/issues/110) * Mention instant.page as a related project ([0bc8aec](https://github.com/GoogleChromeLabs/quicklink/commit/0bc8aec)) * Mention Safari ≥ 12.1 working without polyfills (#111) ([b01e5bb](https://github.com/GoogleChromeLabs/quicklink/commit/b01e5bb)), closes [#111](https://github.com/GoogleChromeLabs/quicklink/issues/111) * remove extraneous full stops / periods from comment (#105) ([23737af](https://github.com/GoogleChromeLabs/quicklink/commit/23737af)), closes [#105](https://github.com/GoogleChromeLabs/quicklink/issues/105) * remove unneeded type="text/css" from demo (#106) ([4dc74f1](https://github.com/GoogleChromeLabs/quicklink/commit/4dc74f1)), closes [#106](https://github.com/GoogleChromeLabs/quicklink/issues/106) * remove unneeded type="text/css" from demo page (#104) ([24919bd](https://github.com/GoogleChromeLabs/quicklink/commit/24919bd)), closes [#104](https://github.com/GoogleChromeLabs/quicklink/issues/104) * Update link to Gatsby with Guess.js (#108) ([dc02d33](https://github.com/GoogleChromeLabs/quicklink/commit/dc02d33)), closes [#108](https://github.com/GoogleChromeLabs/quicklink/issues/108) * Update microbundle to fix "missing JSX plugin" issue ([8f5cf22](https://github.com/GoogleChromeLabs/quicklink/commit/8f5cf22)) * Update repo path in package.json ([45e9bbd](https://github.com/GoogleChromeLabs/quicklink/commit/45e9bbd)) * Update the Readme and add a mention of the WordPress plugin ([0f15f45](https://github.com/GoogleChromeLabs/quicklink/commit/0f15f45)) * Use latest version of polyfill.io JS (#92) ([b15c8ba](https://github.com/GoogleChromeLabs/quicklink/commit/b15c8ba)), closes [#92](https://github.com/GoogleChromeLabs/quicklink/issues/92) * fix: Attempt to address build issues (Travis) ([9755280](https://github.com/GoogleChromeLabs/quicklink/commit/9755280)) * fix: stop observing links once prefetched; ([ce0011c](https://github.com/GoogleChromeLabs/quicklink/commit/ce0011c)) * fix(README): use UMD file for ` ``` 2. Add the following snippet in its place, to import the module from its source file: ```js ``` 3. Open [src/index.mjs](src/index.mjs) for edit and replace the following line: ```js import throttle from 'throttles'; ``` By: ```js import throttle from '../node_modules/throttles/dist/index.mjs' ``` 4. Build the project: `npm run build`. 5. Start a local server: `npm start`. By default, this will start the local server at `https://localhost:8080`. 6. Open the file where the modifications where made: `http://localhost:8080/test/fixtures/test-basic-usage.html`. 7. Open Chrome DevTools and go the **Sources** tab. 8. Under `localhost:8080/src` you can find the unminified versions of the `Quicklink` files. Now you can use breakpoints and inspect variables to debug the library. ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================


npm gzip size ci

# quicklink > Faster subsequent page-loads by prefetching or prerendering in-viewport links during idle time ## How it works Quicklink attempts to make navigations to subsequent pages load faster. It: - **Detects links within the viewport** (using [Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)) - **Waits until the browser is idle** (using [requestIdleCallback](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback)) - **Checks if the user isn't on a slow connection** (using `navigator.connection.effectiveType`) or has data-saver enabled (using `navigator.connection.saveData`) - **Prefetches** (using [``](https://www.w3.org/TR/resource-hints/#prefetch) or XHR) or **prerenders** (using [Speculation Rules API](https://github.com/WICG/nav-speculation/blob/main/triggers.md)) URLs to the links. Provides some control over the request priority (can switch to `fetch()` if supported). ## Why This project aims to be a drop-in solution for sites to prefetch or prerender links based on what is in the user's viewport. It also aims to be small (**< 2KB minified/gzipped**). ## Multi page apps ### Installation For use with [Node.js](https://nodejs.org/) and [npm](https://www.npmjs.com/): ```sh npm install quicklink ``` You can also grab `quicklink` from [unpkg.com/quicklink](https://unpkg.com/quicklink). ### Usage Once initialized, `quicklink` will automatically prefetch URLs for links that are in-viewport during idle time. Quickstart: ```html ``` For example, you can initialize after the `load` event fires: ```html ``` ES Module import: ```js import {listen, prefetch} from 'quicklink'; ``` ## Single page apps (React) ### Installation First, install the packages with [Node.js](https://nodejs.org/) and [npm](https://www.npmjs.com/): ```sh npm install quicklink webpack-route-manifest --save-dev ``` Then, configure Webpack route manifest into your project, as explained [here](https://github.com/lukeed/webpack-route-manifest). This will generate a map of routes and chunks called `rmanifest.json`. It can be obtained at: - URL: `site_url/rmanifest.json` - Window object: `window.__rmanifest` ### Usage Import `quicklink` React HOC where want to add prefetching functionality. Wrap your routes with the `withQuicklink()` HOC. Example: ```jsx import {withQuicklink} from 'quicklink/dist/react/hoc.js'; const options = { origins: [], }; Loading...}> ; ``` ## API ### quicklink.listen(options) Returns: `Function` A "reset" function is returned, which will empty the active `IntersectionObserver` and the cache of URLs that have already been prefetched or prerendered. This can be used between page navigations and/or when significant DOM changes have occurred. #### options.prerender - Type: `Boolean` - Default: `false` Whether to switch from the default prefetching mode to the prerendering mode for the links inside the viewport. > **Note:** The prerendering mode (when this option is set to true) will fallback to the prefetching mode if the browser does not support prerender. > Once the element exits the viewport, the `speculationrules` script is removed from the DOM. This approach makes it possible to exceed the limit of 10 prerenders imposed for the 'immediate' and 'eager' settings for eagerness. #### options.eagerness - Type: `String` - Default: `immediate` Determines the mode to be used for prerendering specified within the speculation rules. #### options.prerenderAndPrefetch * Type: `Boolean` * Default: `false` Whether to activate both the prefetching and prerendering mode at the same time. #### options.delay - Type: `Number` - Default: `0` The _amount of time_ each link needs to stay inside the viewport before being prefetched, in milliseconds. #### options.el - Type: `HTMLElement|NodeList` - Default: `document.body` The DOM element to observe for in-viewport links to prefetch or the NodeList of Anchor Elements. #### options.limit - Type: `Number` - Default: `Infinity` The _total_ requests that can be prefetched or prerendered while observing the `options.el` container. #### options.threshold - Type: `Number` - Default: `0` The _area percentage_ of each link that must have entered the viewport to be fetched, in its decimal form (e.g. 0.25 = 25%). #### options.throttle - Type: `Number` - Default: `Infinity` The _concurrency limit_ for simultaneous requests while observing the `options.el` container. #### options.timeout - Type: `Number` - Default: `2000` The `requestIdleCallback` timeout, in milliseconds. > **Note:** The browser must be idle for the configured duration before prefetching. #### options.timeoutFn - Type: `Function` - Default: `requestIdleCallback` A function used for specifying a `timeout` delay. This can be swapped out for a custom function like [networkIdleCallback](https://github.com/pastelsky/network-idle-callback) (see demos). By default, this uses [`requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback) or the embedded polyfill. #### options.priority - Type: `Boolean` - Default: `false` Whether or not the URLs within the `options.el` container should be treated as high priority. When `true`, quicklink will attempt to use the `fetch()` API if supported (rather than `link[rel=prefetch]`). #### options.origins - Type: `Array` - Default: `[location.hostname]` A static array of URL hostnames that are allowed to be prefetched. Defaults to the same domain origin, which prevents _any_ cross-origin requests. **Important:** An empty array (`[]`) allows **_all origins_** to be prefetched. #### options.ignores - Type: `RegExp` or `Function` or `Array` - Default: `[]` Determine if a URL should be prefetched. When a `RegExp` tests positive, a `Function` returns `true`, or an `Array` contains the string, then the URL is _not_ prefetched. > **Note:** An `Array` may contain `String`, `RegExp`, or `Function` values. > **Important:** This logic is executed _after_ origin matching! #### options.onError - Type: `Function` - Default: None An optional error handler that will receive any errors from prefetched requests. By default, these errors are silently ignored. #### options.hrefFn - Type: `Function` - Default: None An optional function to generate the URL to prefetch. It receives an [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) as the argument. ### quicklink.prefetch(urls, isPriority) Returns: `Promise` The `urls` provided are always passed through `Promise.all`, which means the result will always resolve to an Array. > **Important:** You much `catch` you own request error(s). #### urls - Type: `String` or `Array` - Required: `true` One or many URLs to be prefetched. > **Note:** Each `url` value is resolved from the current location. #### isPriority - Type: `Boolean` - Default: `false` Whether or not the URL(s) should be treated as "high priority" targets. By default, calls to `prefetch()` are low priority. > **Note:** This behaves identically to `listen()`'s `priority` option. ### quicklink.prerender(urls, eagerness) Returns: `Promise` > **Important:** You much `catch` you own request error(s). #### urls - Type: `String` or `Array` - Required: `true` One or many URLs to be prerendered. > **Note:** Speculative Rules API supports same-site cross origin Prerendering with [opt-in header](https://bit.ly/ss-cross-origin-pre). #### eagerness - Type: `String` - Default: `immediate` Determines the mode to be used for prerendering specified within the speculation rules. ## Polyfills `quicklink`: - Includes a very small fallback for [requestIdleCallback](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback) - 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. ## Recipes ### Set a custom timeout for prefetching resources Defaults to 2 seconds (via `requestIdleCallback`). Here we override it to 4 seconds: ```js quicklink.listen({ timeout: 4000, }); ``` ### Set a specific Anchor Elements NodeList to observe for in-viewport links Defaults to `document` otherwise. ```js quicklink.listen({ el: document.querySelectorAll('a.linksToPrefetch'), }); ``` ### Set the DOM element to observe for in-viewport links Defaults to `document` otherwise. ```js quicklink.listen({ el: document.getElementById('carousel'), }); ``` ### Programmatically `prefetch()` URLs If you would prefer to provide a static list of URLs to be prefetched, instead of detecting those in-viewport, customizing URLs is supported. ```js // Single URL quicklink.prefetch('2.html'); // Multiple URLs quicklink.prefetch(['2.html', '3.html', '4.js']); // Multiple URLs, with high priority // Note: Can also be use with single URL! quicklink.prefetch(['2.html', '3.html', '4.js'], true); ``` ### Programmatically `prerender()` URLs If you would prefer to provide a static list of URLs to be prerendered, instead of detecting those in-viewport, customizing URLs is supported. ```js // Single URL quicklink.prerender('2.html'); // Multiple URLs quicklink.prerender(['2.html', '3.html', '4.js']); ``` ### Set the request priority for prefetches while scrolling Defaults to low-priority (`rel=prefetch` or XHR). For high-priority (`priority: true`), attempts to use `fetch()` or falls back to XHR. > **Note:** This runs `prefetch(..., true)` with URLs found within the `options.el` container. ```js quicklink.listen({priority: true}); ``` ### Specify a custom list of allowed origins Provide a list of hostnames that should be prefetch-able. Only the same origin is allowed by default. > **Important:** You must also include your own hostname! ```js quicklink.listen({ origins: [ // add mine 'my-website.com', 'api.my-website.com', // add third-parties 'other-website.com', 'example.com', // ... ], }); ``` ### Allow all origins Enables all cross-origin requests to be made. > **Note:** You may run into [CORB](https://chromium.googlesource.com/chromium/src/+/main/services/network/cross_origin_read_blocking_explainer.md) and [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) issues! ```js quicklink.listen({ origins: true, // or origins: [], }); ``` ### Custom Ignore Patterns These filters run _after_ the `origins` matching has run. Ignores can be useful for avoiding large file downloads or for responding to DOM attributes dynamically. ```js // Same-origin restraint is enabled by default. // // This example will ignore all requests to: // - all "/api/*" pathnames // - all ".zip" extensions // - all tags with "noprefetch" attribute // quicklink.listen({ ignores: [ /\/api\/?/, uri => uri.includes('.zip'), (uri, elem) => elem.hasAttribute('noprefetch'), ], }); ``` You may also wish to ignore prefetches to URLs which contain a URL fragment (e.g. `index.html#top`). This can be useful if you (1) are using anchors to headings in a page or (2) have URL fragments setup for a single-page application, and which to avoid firing prefetches for similar URLs. Using `ignores` this can be achieved as follows: ```js quicklink.listen({ ignores: [ uri => uri.includes('#'), // or RegExp: /#(.+)/ // or element matching: (uri, elem) => !!elem.hash ], }); ``` ### Custom URL to prefetch via hrefFn callback The hrefFn method allows to build the URL to prefetch (e.g. API endpoint) on the fly instead of the prefetching the `href` attribute URL. ```js quicklink.listen({ hrefFn(element) { return element.href.replace('html', 'json'); }, }); ``` ## Browser Support The prefetching provided by `quicklink` can be viewed as a [progressive enhancement](https://www.smashingmagazine.com/2009/04/progressive-enhancement-what-it-is-and-how-to-use-it/). Cross-browser support is as follows: - Without polyfills: Chrome, Safari ≥ 12.1, Firefox, Edge, Opera, Android Browser, Samsung Internet. - With [Intersection Observer polyfill](https://github.com/GoogleChromeLabs/intersection-observer) ~6KB gzipped/minified: Safari ≤ 12.0, IE11 - With the above and a [Set()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) and [Array.from](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from) polyfill: IE9 and IE10. [Core.js](https://github.com/zloirock/core-js) provides both `Set()` and `Array.from()` shims. Projects like [es6-shim](https://github.com/paulmillr/es6-shim/blob/master/README.md) are an alternative you can consider. Certain features have layered support: - The [Network Information API](https://wicg.github.io/netinfo/), which is used to check if the user has a slow effective connection type (via `navigator.connection.effectiveType`) is only available in [Chrome 61+ and Opera 57+](https://caniuse.com/netinfo) - If opting for `{priority: true}` and the [Fetch API](https://fetch.spec.whatwg.org/) isn't available, XHR will be used instead. ## Using the prefetcher directly A `prefetch` method can be individually imported for use in other projects. This method includes the logic to respect Data Saver and 2G connections. It also issues requests thru `fetch()`, XHRs, or `link[rel=prefetch]` depending on (a) the `isPriority` value and (b) the current browser's support. After installing `quicklink` as a dependency, you can use it as follows: ```html ``` ## Demo ### Glitch demos - [Using Quicklink in a multi-page site](https://github.com/GoogleChromeLabs/quicklink/tree/main/demos/news) - [Using Quicklink with Service Workers (via Workbox)](https://github.com/GoogleChromeLabs/quicklink/tree/main/demos/news-workbox) - [Using Quicklink to prefetch API calls instead of `href` attribute](https://github.com/GoogleChromeLabs/quicklink/tree/main/demos/hrefFn) - [Using Quicklink to prerender a specific page](https://uskay-prerender2.glitch.me/next.html) ### Research Here's a [WebPageTest run](https://www.webpagetest.org/video/view.php?id=181212_4c294265117680f2636676721cc886613fe2eede&data=1) for our [demo](https://keyword-2-ecd7b.firebaseapp.com/) improving page-load performance by up to 4 seconds via quicklink's prefetching. A [video](https://youtu.be/rQ75YEbJicw) comparison of the before/after prefetching is on YouTube. For demo purposes, we deployed a version of the [Google Blog](https://blog.google) on Firebase hosting. We then deployed another version of it, adding quicklink to the homepage and benchmarked navigating from the homepage to an article that was automatically prefetched. The prefetched version loaded faster. Please note: this is by no means an exhaustive benchmark of the pros and cons of in-viewport link prefetching. Just a demo of the potential improvements the approach can offer. Your own mileage may heavily vary. ## Additional notes ### Session Stitching Cross-origin prefetching (e.g `a.com/foo.html` prefetches `b.com/bar.html`) has a number of limitations. One such limitation is with session-stitching. `b.com` may expect `a.com`'s navigation requests to include session information (e.g a temporary ID - e.g `b.com/bar.html?hash=<>×tamp=<>`), where this information is used to customize the experience or log information to analytics. If session-stitching requires a timestamp in the URL, what is prefetched and stored in the HTTP cache may not be the same as the one the user ultimately navigates to. This introduces a challenge as it can result in double prefetches. To workaround this problem, you can consider passing along session information via the [ping attribute](https://caniuse.com/ping) (separately) so the origin can stitch a session together asynchronously. ### Ad-related considerations 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). Ads appear on sites mostly in two ways: - **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. - **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). ## Related projects - Using [Gatsby](https://gatsbyjs.org)? You already get most of this for free baked in. It uses `Intersection Observer` to prefetch all of the links that are in view and provided heavy inspiration for this project. - Want a more data-driven approach? See [Guess.js](https://guess-js.github.io). It uses analytics and machine-learning to prefetch resources based on how users navigate your site. It also has plugins for [Webpack](https://www.npmjs.com/package/guess-webpack) and [Gatsby](https://www.gatsbyjs.org/docs/optimizing-site-performance-with-guessjs/). - WordPress users can now get quicklink as a [WordPress Plugin from the plugin repository](https://wordpress.org/plugins/quicklink/). - Drupal users can install the [Quicklink Drupal module](https://www.drupal.org/project/quicklink). - Magento 2 users can install the [rafaelcg-magento2-quicklink](https://marketplace.magento.com/rafaelcg-magento2-quicklink.html) or [rangerz/magento2-module-quicklink](https://github.com/rangerz/magento2-module-quicklink). - Want less aggressive prefetching? [instant.page](https://instant.page/) prefetches on mouseover and touchstart, right before a click. ## License Licensed under the [Apache-2.0 license](LICENSE). ================================================ FILE: demos/basic.html ================================================ Basic demo

Basic demo

Link 1 Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Link 2
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Eos, quos? Link 3
CSS
Link 4
================================================ FILE: demos/hrefFn/2.html ================================================ Prefetch experiments

Page 2

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!

================================================ FILE: demos/hrefFn/2.json ================================================ { "title": "API Target to Prefetch Example", "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!" } ================================================ FILE: demos/hrefFn/hrefFn_demo.html ================================================ Basic demo

Basic demo

Link 1 Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

================================================ FILE: demos/network-idle.html ================================================ network-idle-callback demo

This demo uses network-idle-callback to only prefetch when network activity goes idle in the current tab.

Link 1 Link 2 Link 3
CSS
Link 4 ================================================ FILE: demos/network-idle.js ================================================ // This script is a localized version of the upstream // https://github.com/pastelsky/network-idle-callback // which fixes issues with browser importing of the // above dependency. It is hopefully temporary. 'use strict'; const DOMContentLoad = new Promise(resolve => { document.addEventListener('DOMContentLoaded', resolve); }); navigator.serviceWorker.getRegistration() .then(registration => { if (!registration) { console.warn('`networkIdleCallback` was called before a service worker was registered.'); console.warn('`networkIdleCallback` is ineffective without a working service worker'); } }); /** * networkIdleCallback works similar to requestIdleCallback, * detecting and notifying you when network activity goes idle * in your current tab. * @param {*} fn - A valid function * @param {*} options - An options object */ function networkIdleCallback(fn, options = {timeout: 0}) { // Call the function immediately if required features are absent if ( !('MessageChannel' in window) || !('serviceWorker' in navigator) || !navigator.serviceWorker.controller ) { DOMContentLoad.then(() => fn({didTimeout: false})); return; } const messageChannel = new MessageChannel(); navigator.serviceWorker.controller .postMessage( 'NETWORK_IDLE_ENQUIRY', [messageChannel.port2], ); const timeoutId = setTimeout(() => { const cbToPop = networkIdleCallback.__callbacks__ .find(cb => cb.id === timeoutId); networkIdleCallback.__popCallback__(cbToPop, true); }, options.timeout); networkIdleCallback.__callbacks__.push({ id: timeoutId, fn, timeout: options.timeout, }); messageChannel.port1.addEventListener('message', handleMessage); messageChannel.port1.start(); } /* function cancelNetworkIdleCallback(callbackId) { clearTimeout(callbackId); networkIdleCallback.__callbacks__ = networkIdleCallback.__callbacks__ .find(cb => cb.id === callbackId); } */ networkIdleCallback.__popCallback__ = (callback, didTimeout) => { DOMContentLoad.then(() => { const cbToPop = networkIdleCallback.__callbacks__ .find(cb => cb.id === callback.id); if (cbToPop) { cbToPop.fn({didTimeout}); clearTimeout(cbToPop.id); networkIdleCallback.__callbacks__ = networkIdleCallback.__callbacks__.filter( cb => cb.id !== callback.id); } }); }; networkIdleCallback.__callbacks__ = []; if ('serviceWorker' in navigator) { navigator.serviceWorker.addEventListener('message', handleMessage); } /** * Handle message passing * @param {*} event - A valid event */ function handleMessage(event) { if (!event.data) { return; } switch (event.data) { case 'NETWORK_IDLE_ENQUIRY_RESULT_IDLE': case 'NETWORK_IDLE_CALLBACK': networkIdleCallback.__callbacks__.forEach(callback => { networkIdleCallback.__popCallback__(callback, false); }); break; } } ================================================ FILE: demos/news/README.md ================================================ # Demo: Quicklink basic usage A demo showing how to use Quicklink on a simple multi-page website. ## Glitch Source - [Link to Glitch App](https://anton-karlovskiy-quicklink-news.glitch.me) - [Link to Project on Glitch](https://glitch.com/~anton-karlovskiy-quicklink-news) ## Installation ```sh git clone https://api.glitch.com/git/anton-karlovskiy-quicklink-news npm install npm start npm run build ``` ================================================ FILE: demos/news-workbox/README.md ================================================ # Demo: Quicklink usage with workbox A demo showing how to use Quicklink with Workbox for offline caching and links in the visible viewport. ## Glitch Source - [Link to Glitch App](https://anton-karlovskiy-quicklink-news-workbox.glitch.me) - [Link to Project on Glitch](https://glitch.com/~anton-karlovskiy-quicklink-news-workbox) ## Installation ```sh git clone https://api.glitch.com/git/anton-karlovskiy-quicklink-news-workbox npm install npm start npm run build ``` ================================================ FILE: demos/spa/README.md ================================================ # Demo: Quicklink integration for create-react-app A demo showing how to use Quicklink with in a create-react-app site. To integrate your React SPA with Quicklink, follow the steps [here](https://github.com/GoogleChromeLabs/quicklink#single-page-apps-react). ## Glitch Source - [Link to Glitch App](https://create-react-app-quicklink.glitch.me/) - [Link to Project on Glitch](https://glitch.com/~create-react-app-quicklink) ## Installation ```sh git clone https://api.glitch.com/git/create-react-app-quicklink npm install npm start npm run build ``` ================================================ FILE: demos/sw.js ================================================ /* eslint-env serviceworker */ 'use strict'; importScripts('https://unpkg.com/network-idle-callback@1.0.1/lib/request-monitor.js'); self.addEventListener('install', event => { console.log('[ServiceWorker] Installed'); }); self.addEventListener('activate', event => { console.log('[ServiceWorker] Activated'); }); self.addEventListener('fetch', event => { console.log('[ServiceWorker] Fetch', event.request.url); self.requestMonitor.listen(event); const promise = fetch(event.request) .then(response => { console.log('done', event.clientId); self.requestMonitor.unlisten(event); return response; }) .catch(error => { console.log('error'); self.requestMonitor.unlisten(error); }); event.respondWith(promise); }); ================================================ FILE: package.json ================================================ { "name": "quicklink", "version": "3.0.1", "description": "Faster subsequent page-loads by prefetching in-viewport links during idle time", "repository": { "type": "git", "url": "git+https://github.com/GoogleChromeLabs/quicklink.git" }, "homepage": "https://getquick.link/", "bugs": { "url": "https://github.com/GoogleChromeLabs/quicklink/issues" }, "author": "addyosmani ", "license": "Apache-2.0", "main": "dist/quicklink.js", "module": "dist/quicklink.mjs", "umd:main": "dist/quicklink.umd.js", "unpkg": "dist/quicklink.umd.js", "files": [ "dist" ], "scripts": { "lint": "eslint --report-unused-disable-directives --ext .js,.mjs .", "lint-fix": "npm run lint -- --fix", "fix": "npm run lint -- --fix", "start": "sirv --dev --no-clear --no-logs --host 127.0.0.1 --port 8080", "uvu": "uvu test", "test": "npm-run-all build-all --parallel --race start uvu", "build": "microbundle src/index.mjs --no-sourcemap --external none", "build-plugin": "microbundle src/chunks.mjs --no-sourcemap --external none -o dist/react", "build-react-chunks": "babel src/react-chunks.js --out-file dist/react/hoc.js", "build-all": "npm-run-all --parallel build build-plugin build-react-chunks", "prepublishonly": "npm run build-all", "size": "size-limit", "changelog": "npm run conventional-changelog -i CHANGELOG.md -s -r 0", "release": "cross-env-shell \"npm run build-all && git commit -am $npm_package_version && git tag $npm_package_version && git push --follow-tags\"" }, "keywords": [ "prefetch", "performance", "fetch", "intersectionobserver", "background", "speed" ], "dependencies": { "route-manifest": "^1.0.0", "throttles": "^1.0.1" }, "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19", "react-dom": "^16.8.0 || ^17 || ^18 || ^19" }, "peerDependenciesMeta": { "react": { "optional": true }, "react-dom": { "optional": true } }, "devDependencies": { "@babel/cli": "^7.28.6", "@babel/core": "^7.29.0", "@babel/preset-env": "^7.29.0", "@babel/preset-react": "^7.28.5", "@size-limit/file": "^12.0.0", "conventional-changelog": "^7.1.1", "cross-env": "^10.1.0", "eslint": "^8.57.1", "eslint-config-google": "^0.14.0", "eslint-plugin-react": "^7.37.5", "microbundle": "^0.15.1", "npm-run-all2": "^8.0.4", "puppeteer": "^24.37.2", "react": "^19.2.4", "react-dom": "^19.2.4", "sirv-cli": "^3.0.1", "size-limit": "^12.0.0", "uvu": "^0.5.6" } } ================================================ FILE: site/.browserslistrc ================================================ # https://github.com/browserslist/browserslist#readme defaults ================================================ FILE: site/.config/configstore/update-notifier-pnpm.json ================================================ { "optOut": false, "lastUpdateCheck": 1574808403652, "update": { "latest": "4.3.3", "current": "2.25.5", "type": "major", "name": "pnpm" } } ================================================ FILE: site/.config/glitch-package-manager ================================================ pnpm ================================================ FILE: site/.eleventy.js ================================================ /* eslint-env node */ /* eslint-disable new-cap */ 'use strict'; const {EleventyHtmlBasePlugin: htmlBasePlugin} = require('@11ty/eleventy'); const navigationPlugin = require('@11ty/eleventy-navigation'); const syntaxHighlight = require('@11ty/eleventy-plugin-syntaxhighlight'); const autoprefixer = require('autoprefixer'); const htmlminifier = require('html-minifier-terser'); const markdownIt = require('markdown-it'); const pluginRev = require('eleventy-plugin-rev'); const postcss = require('postcss'); const sass = require('eleventy-sass'); const IS_PRODUCTION = process.env.NODE_ENV === 'production'; const htmlminifierConfig = { collapseBooleanAttributes: true, collapseWhitespace: true, conservativeCollapse: false, decodeEntities: false, minifyCSS: true, minifyJS: true, minifyURLs: false, removeAttributeQuotes: true, removeComments: true, removeEmptyAttributes: false, removeOptionalAttributes: true, removeOptionalTags: true, removeRedundantAttributes: true, removeScriptTypeAttributes: true, removeStyleLinkTypeAttributes: true, removeTagWhitespace: false, sortAttributes: true, sortClassName: true, }; module.exports = eleventyConfig => { eleventyConfig.addPlugin(htmlBasePlugin, {baseHref: '/'}); eleventyConfig.addPlugin(navigationPlugin); eleventyConfig.addPlugin(syntaxHighlight); eleventyConfig.addPlugin(pluginRev); eleventyConfig.addPlugin(sass, [ { postcss: postcss([autoprefixer]), sass: { style: 'expanded', sourceMap: true, }, rev: false, }, { sass: { style: 'compressed', sourceMap: false, }, rev: true, when: [{NODE_ENV: 'production'}], }, ]); eleventyConfig.addPassthroughCopy('src/assets/images'); eleventyConfig.addPassthroughCopy('src/assets/js'); eleventyConfig.addPassthroughCopy('src/site.webmanifest'); eleventyConfig.addNunjucksFilter('markdown', string => { const md = new markdownIt(); return md.render(string); }); eleventyConfig.addPairedShortcode('markdownConvert', content => { const md = new markdownIt(); return md.render(content); }); eleventyConfig.addNunjucksShortcode('sectionTitle', title => { const md = new markdownIt(); return md.render(`## ${title}`); }); eleventyConfig.addTransform('htmlminifier', (content, outputPath) => { if (!outputPath.endsWith('.html')) return content; if (!IS_PRODUCTION) return content; return htmlminifier.minify(content, htmlminifierConfig); }); return { dir: { input: 'src', output: 'build', }, }; }; ================================================ FILE: site/.firebaserc ================================================ { "projects": { "default": "quicklink-6a87b" } } ================================================ FILE: site/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Compiled binary addons (https://nodejs.org/api/addons.html) build/ # Dependency directories node_modules/ /_datacache ================================================ FILE: site/.stylelintignore ================================================ **/*.min.css **/*.min.scss **/dist/ **/vendor/ /build/ ================================================ FILE: site/.stylelintrc.json ================================================ { "extends": [ "stylelint-config-twbs-bootstrap" ], "reportInvalidScopeDisables": true, "reportNeedlessDisables": true, "rules": { "order/properties-order": null, "selector-class-pattern": null, "selector-no-qualifying-type": null }, "overrides": [ { "files": "**/*.scss", "rules": { "scss/selector-no-union-class-name": true } } ] } ================================================ FILE: site/LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: site/README.md ================================================ # eleventy-quicklink-website 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/). ## Installation ```sh git clone git@github.com:googlechromelabs/quicklink.git cd site npm install ``` ## Commands | Command | Description | | --------------- | ------------------------------------------------------------- | | `npm start` | Start a development server and watch for updates | | `npm run build` | Build templates, data, CSS, and JS for production environment | ================================================ FILE: site/firebase.json ================================================ { "hosting": { "public": "build", "site": "getquicklink", "ignore": [ "firebase.json", "**/.*", "**/node_modules/**" ], "headers": [ { "source": "/service-worker.js", "headers": [ { "key": "Cache-Control", "value": "no-cache" } ] }, { "source": "**/*.@(js)", "headers": [ { "key": "Cache-Control", "value": "max-age=31536000" } ] }, { "source": "**/*.@(css)", "headers": [ { "key": "Cache-Control", "value": "max-age=31536000" } ] }, { "source": "**/*.@(eot|otf|ttf|ttc|woff)", "headers": [ { "key": "Access-Control-Allow-Origin", "value": "*" } ] }, { "source": "**/*.@(jpg|jpeg|gif|png|svg)", "headers": [ { "key": "Cache-Control", "value": "max-age=604800" } ] }, { "source": "404.html", "headers": [ { "key": "Cache-Control", "value": "max-age=300" } ] } ] } } ================================================ FILE: site/index.njk ================================================ --- title: Quicklink layout: layouts/base.njk description: Faster subsequent page-loads by prefetching in-viewport links during idle time. --- {% include "components/heading.njk" %} {% include "components/why-quicklink.njk" %} {% include "components/copy-snippet.njk" %} {% include "components/download.njk" %} {% include "components/trusted-by.njk" %} {% include "components/installation.njk" %} {% include "components/usage.njk" %} {% include "components/over-prefetching.njk" %} {% include "components/react.njk" %} {% include "components/chrome-extension.njk" %} {% include "components/why-prefetch.njk" %} {% include "components/use-with.njk" %} ================================================ FILE: site/package.json ================================================ { "name": "eleventy-quicklink-website", "description": "", "version": "1.0.0", "private": true, "scripts": { "build": "rimraf build && cross-env NODE_ENV=production eleventy", "start": "rimraf build && cross-env NODE_ENV=development eleventy --serve", "deploy": "firebase deploy --project=quicklink-6a87b", "lint": "stylelint src/assets/styles", "test": "npm run lint && npm run build" }, "license": "Apache-2.0", "devDependencies": { "@11ty/eleventy": "^2.0.1", "@11ty/eleventy-navigation": "^1.0.5", "@11ty/eleventy-plugin-syntaxhighlight": "^5.0.2", "autoprefixer": "^10.4.24", "cross-env": "^10.1.0", "eleventy-plugin-rev": "^2.0.0", "eleventy-sass": "^2.2.6", "html-minifier-terser": "^7.2.0", "markdown-it": "^14.1.0", "postcss": "^8.5.6", "rimraf": "^6.1.2", "stylelint": "^16.26.1", "stylelint-config-twbs-bootstrap": "^16.1.0" } } ================================================ FILE: site/src/_data/site.js ================================================ /* eslint-env node */ 'use strict'; const path = require('node:path'); const process = require('node:process'); const IS_NETLIFY = process.env.NETLIFY === 'true'; const {version: quicklinkVersion} = require(path.join(__dirname, '../../../package.json')); module.exports = () => { return { title: 'Quicklink', subtitle: 'Instant next-page navigations', description: 'Faster subsequent page-loads by prefetching in-viewport links during idle time.', socialImage: '/assets/images/og-image.png', // If we are on Netlify, use the `DEPLOY_PRIME_URL` environment variable url: IS_NETLIFY ? process.env.DEPLOY_PRIME_URL : 'https://getquick.link', isNetlify: IS_NETLIFY, quicklinkGithubURL: 'https://github.com/GoogleChromeLabs/quicklink', quicklinkVersion, quicklinkSizeLimit: '1KB', bottomResource: { caption: 'View source on GitHub', }, useWithFrameworks: [ { title: 'wordpress', logoFileName: 'wordpress.svg', url: 'https://wordpress.org/plugins/quicklink/', }, { title: 'drupal', logoFileName: 'drupal.svg', url: 'https://www.drupal.org/project/quicklink/', }, { title: 'magento', logoFileName: 'magento.svg', url: 'https://marketplace.magento.com/rafaelcg-magento2-quicklink.html', }, { title: 'react', logoFileName: 'react.svg', url: 'https://github.com/HOUCe/react-quicklink-component/', }, { title: 'angular', logoFileName: 'angular.svg', url: 'https://github.com/mgechev/ngx-quicklink/', }, { title: 'vue', logoFileName: 'vue.svg', url: 'https://nuxtjs.org/api/components-nuxt-link/', }, ], trustedByLogos: [ { websiteUrl: 'https://www.ray-ban.com/', logoFileName: 'rayban.com.png', companyName: 'Ray-Ban', }, { websiteUrl: 'https://www.oakley.com/', logoFileName: 'oakley.com.png', companyName: 'Oakley', }, { websiteUrl: 'https://www.syfy.com/', logoFileName: 'syfy.com.png', companyName: 'SYFY WIRE', }, { websiteUrl: 'https://www.newegg.com/', logoFileName: 'newegg.com.png', companyName: 'Newegg', }, { websiteUrl: 'https://www.barefootwine.ca/', logoFileName: 'barefootwine.ca.png', companyName: 'BAREFOOT', }, { websiteUrl: 'https://hashnode.com/', logoFileName: 'hashnode.com.png', companyName: 'Hashnode', }, { websiteUrl: 'https://www.hartfordwines.com/', logoFileName: 'hartfordwines.com.png', companyName: 'HARTFORD', }, { websiteUrl: 'https://vinyla.com/', logoFileName: 'vinyla.com.png', companyName: 'Vinyla', }, { websiteUrl: 'https://www.matsuda.com/', logoFileName: 'matsuda.com.png', companyName: 'MATSUDA', }, { websiteUrl: 'https://paulrand.design/', logoFileName: 'paulrand.design.png', companyName: 'Paul Rand', }, { websiteUrl: 'http://www.week.co.jp/', logoFileName: 'week.co.jp.png', companyName: 'Komachi', }, { websiteUrl: 'https://www.quiply.com/', logoFileName: 'quiply.com.png', companyName: 'Quiply', }, { websiteUrl: 'https://saintagnes.org/', logoFileName: 'saintagnes.org.png', companyName: 'St Agnes', }, ], }; }; ================================================ FILE: site/src/_includes/components/chrome-extension.njk ================================================ {% extends "layouts/normal-section-wrapper.njk" %} {% block section %} {% sectionTitle "Chrome Extension" %} {{ "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 }} {{ "The extension can be used with the following purposes:" | markdown | safe }} {{ "* To navigate the web faster." | markdown | safe }} {{ "* To estimate the potential impact of `Quicklink` on a site, before implementing it (see [impact measurement guide](/measure))." | markdown | safe }} {{ "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 }} {{ "The code of the extension can be found at [this repository](https://github.com/demianrenzulli/quicklink-chrome-extension). Contributions are welcomed!" | markdown | safe }} {% endblock %} ================================================ FILE: site/src/_includes/components/copy-snippet.njk ================================================ {% extends "layouts/normal-section-wrapper.njk" %} {% block section %}
Place this snippet in your head or just before your close body tag:
{% highlight "html" %} {% endhighlight %}
Copied. Now place it just before </body> on your pages.
{% endblock %} ================================================ FILE: site/src/_includes/components/download.njk ================================================ {% extends "layouts/normal-section-wrapper.njk" %} {% block section %} {% sectionTitle "Download" %} {% endblock %} ================================================ FILE: site/src/_includes/components/github-fork.njk ================================================ Fork ================================================ FILE: site/src/_includes/components/github-star.njk ================================================ Star ================================================ FILE: site/src/_includes/components/heading.njk ================================================ {% extends "layouts/highlighted-section-wrapper.njk" %} {% block section %}

"We implemented Quicklink and saw a 50% increase in conversions and 4x faster page transitions" - NewEgg

{% endblock %} ================================================ FILE: site/src/_includes/components/installation.njk ================================================ {% extends "layouts/normal-section-wrapper.njk" %} {% block section %} {% sectionTitle "Installation" %} {{ "For use with [Node.js](https://nodejs.org/) and [npm](https://www.npmjs.com/):" | markdown | safe }} {% highlight "bash" %} npm install quicklink {% endhighlight %} {{ "You can also grab `quicklink` from [unpkg.com/quicklink](https://unpkg.com/quicklink)." | markdown | safe }} {% endblock %} ================================================ FILE: site/src/_includes/components/over-prefetching.njk ================================================ {% extends "layouts/normal-section-wrapper.njk" %} {% block section %} {% sectionTitle "Concerned about over-prefetching? We've got you covered" %} {{ "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 }} {{ "The most common approach is passing different `options` to configure prefetching when calling `quicklink.listen()`:" | markdown | safe }} {{ "* Indicating a specific DOM element to observe, with the `options.el` parameter:" | markdown | safe }} {% highlight "js" %} quicklink.listen({ el: document.getElementById('content') }); {% endhighlight %} {{ "* Passing an `options.limit` parameter, indicating the total number of requests that can be prefetched while observing the `options.el` container:" | markdown | safe }} {% highlight "js" %} quicklink.listen({ limit: 5 }); {% endhighlight %} {{ "* Using `options.throttle`, to establish a concurrency limit for simultaneous requests while observing the `options.el` container:" | markdown | safe }} {% highlight "js" %} quicklink.listen({ throttle: 2 }); {% endhighlight %} {{ "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 }} {% highlight "js" %} quicklink.prefetch(['2.html', '3.html', '4.js']); {% endhighlight %} {% endblock %} ================================================ FILE: site/src/_includes/components/react.njk ================================================ {% extends "layouts/normal-section-wrapper.njk" %} {% block section %} {% sectionTitle "Single page apps (React)" %} {% markdownConvert %} ### Installation First, install the packages with [Node.js](https://nodejs.org/) and [npm](https://www.npmjs.com/): {% endmarkdownConvert %} {% highlight "bash" %} npm install quicklink webpack-route-manifest --save-dev {% endhighlight %} {% markdownConvert %} Then, configure Webpack route manifest into your project, as explained [here](https://github.com/lukeed/webpack-route-manifest). This will generate a map of routes and chunks called `rmanifest.json`. It can be obtained at: * URL: `site_url/rmanifest.json` * Window object: `window.__rmanifest` ### Usage Import `quicklink` React HOC where want to add prefetching functionality. Wrap your routes with the `withQuicklink()` HOC. Example: {% endmarkdownConvert %} {% highlight "jsx" %} import {withQuicklink} from 'quicklink/dist/react/hoc.js'; const options = { origins: [], }; Loading...}> ; {% endhighlight %} {% endblock %} ================================================ FILE: site/src/_includes/components/trusted-by.njk ================================================ {% extends "layouts/normal-section-wrapper.njk" %} {% block section %} {% sectionTitle "Trusted by" %}
{%- for trustedByLogo in site.trustedByLogos %} {# TODO: responsive sizing like "used-with" logos #} {{ trustedByLogo.companyName }} {%- endfor %}
{% endblock %} ================================================ FILE: site/src/_includes/components/usage.njk ================================================ {% extends "layouts/normal-section-wrapper.njk" %} {% block section %} {% sectionTitle "Usage" %} {{ "Once initialized, `quicklink` will automatically prefetch URLs for links that are in-viewport during idle time." | markdown | safe }} {{ "Quickstart:" | markdown | safe }} {% highlight "html" %} {% endhighlight %} {{ "For example, you can initialize after the `load` event fires:" | markdown | safe }} {% highlight "html" %} {% endhighlight %} {{ "ES Module import:" | markdown | safe }} {% highlight "js" %} import {listen} from 'quicklink/dist/quicklink.mjs'; listen(); {% endhighlight %} {{ "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 }} {{ "* Call `quicklink.listen()` once a navigation to a new route has completed" | markdown | safe }} {{ "* Call `quicklink.listen()` against a specific DOM element / component" | markdown | safe }} {{ "* Call `quicklink.prefetch()` with a custom set of URLs to prefetch" | markdown | safe }} {% endblock %} ================================================ FILE: site/src/_includes/components/use-with.njk ================================================ {% extends "layouts/normal-section-wrapper.njk" %} {% block section %} {% sectionTitle "Use with" %}
{%- for framework in site.useWithFrameworks %} {{ framework.title }} {%- endfor %}
{% endblock %} ================================================ FILE: site/src/_includes/components/why-prefetch.njk ================================================ {% extends "layouts/normal-section-wrapper.njk" %} {% block section %} {% sectionTitle "Why Quicklink\'s prefetch?" %}
Prefetch pages a user may need in the future to improve subsequent page loads
{% endblock %} ================================================ FILE: site/src/_includes/components/why-quicklink.njk ================================================ {% extends "layouts/normal-section-wrapper.njk" %} {% block section %}

Why Quicklink? {% set githubLarge = false %} {% include "components/github-star.njk" %} {% include "components/github-fork.njk" %} {% set githubLarge = true %} {% include "components/github-star.njk" %} {% include "components/github-fork.njk" %}

{{ "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 }} {% endblock %} ================================================ FILE: site/src/_includes/layouts/base.njk ================================================ {% include "layouts/head.njk" %} {% include "layouts/header.njk" %}
{{ content | safe }} {% include "layouts/footer.njk" %}
================================================ FILE: site/src/_includes/layouts/favicons.njk ================================================ ================================================ FILE: site/src/_includes/layouts/footer.njk ================================================ ================================================ FILE: site/src/_includes/layouts/head.njk ================================================ {{ title + " | " + site.title if title else site.title }} {{ extra_head | safe }} {% include "layouts/favicons.njk" -%} {% include "layouts/social.njk" -%} {# TODO: opt for theme and tweak the background color by avoiding github markdown #} ================================================ FILE: site/src/_includes/layouts/header.njk ================================================ ================================================ FILE: site/src/_includes/layouts/highlighted-section-wrapper.njk ================================================
{% block section %} {% endblock %}
================================================ FILE: site/src/_includes/layouts/normal-section-wrapper.njk ================================================
{% block section %} {% endblock %}
================================================ FILE: site/src/_includes/layouts/social.njk ================================================ {% set socialImagePath = site.url + site.socialImage -%} ================================================ FILE: site/src/api.njk ================================================ --- title: API layout: layouts/base.njk description: Faster subsequent page-loads by prefetching in-viewport links during idle time. eleventyNavigation: key: API order: 1 bottomResource: caption: README on GitHub link: https://github.com/GoogleChromeLabs/quicklink#api --- {% extends "layouts/normal-section-wrapper.njk" %} {% block section %} {% markdownConvert %} ## API ### quicklink.listen(options) Returns: `Function` A "reset" function is returned, which will empty the active `IntersectionObserver` and the cache of URLs that have already been prefetched or prerendered. This can be used between page navigations and/or when significant DOM changes have occurred. #### options.prerender - Type: `Boolean` - Default: `false` Whether to switch from the default prefetching mode to the prerendering mode for the links inside the viewport. > **Note:** The prerendering mode (when this option is set to true) will fallback to the prefetching mode if the browser does not support prerender. > Once the element exits the viewport, the `speculationrules` script is removed from the DOM. This approach makes it possible to exceed the limit of 10 prerenders imposed for the 'immediate' and 'eager' settings for eagerness. #### options.eagerness - Type: `String` - Default: `immediate` Determines the mode to be used for prerendering specified within the speculation rules. #### options.prerenderAndPrefetch * Type: `Boolean` * Default: `false` Whether to activate both the prefetching and prerendering mode at the same time. #### options.delay - Type: `Number` - Default: `0` The _amount of time_ each link needs to stay inside the viewport before being prefetched, in milliseconds. #### options.el - Type: `HTMLElement|NodeList` - Default: `document.body` The DOM element to observe for in-viewport links to prefetch or the NodeList of Anchor Elements. #### options.limit - Type: `Number` - Default: `Infinity` The _total_ requests that can be prefetched or prerendered while observing the `options.el` container. #### options.threshold - Type: `Number` - Default: `0` The _area percentage_ of each link that must have entered the viewport to be fetched, in its decimal form (e.g. 0.25 = 25%). #### options.throttle - Type: `Number` - Default: `Infinity` The _concurrency limit_ for simultaneous requests while observing the `options.el` container. #### options.timeout - Type: `Number` - Default: `2000` The `requestIdleCallback` timeout, in milliseconds. > **Note:** The browser must be idle for the configured duration before prefetching. #### options.timeoutFn - Type: `Function` - Default: `requestIdleCallback` A function used for specifying a `timeout` delay. This can be swapped out for a custom function like [networkIdleCallback](https://github.com/pastelsky/network-idle-callback) (see demos). By default, this uses [`requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback) or the embedded polyfill. #### options.priority - Type: `Boolean` - Default: `false` Whether or not the URLs within the `options.el` container should be treated as high priority. When `true`, quicklink will attempt to use the `fetch()` API if supported (rather than `link[rel=prefetch]`). #### options.origins - Type: `Array` - Default: `[location.hostname]` A static array of URL hostnames that are allowed to be prefetched. Defaults to the same domain origin, which prevents _any_ cross-origin requests. **Important:** An empty array (`[]`) allows **_all origins_** to be prefetched. #### options.ignores - Type: `RegExp` or `Function` or `Array` - Default: `[]` Determine if a URL should be prefetched. When a `RegExp` tests positive, a `Function` returns `true`, or an `Array` contains the string, then the URL is _not_ prefetched. > **Note:** An `Array` may contain `String`, `RegExp`, or `Function` values. > **Important:** This logic is executed _after_ origin matching! #### options.onError - Type: `Function` - Default: None An optional error handler that will receive any errors from prefetched requests. By default, these errors are silently ignored. #### options.hrefFn - Type: `Function` - Default: None An optional function to generate the URL to prefetch. It receives an [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) as the argument. ### quicklink.prefetch(urls, isPriority) Returns: `Promise` The `urls` provided are always passed through `Promise.all`, which means the result will always resolve to an Array. > **Important:** You much `catch` you own request error(s). #### urls - Type: `String` or `Array` - Required: `true` One or many URLs to be prefetched. > **Note:** Each `url` value is resolved from the current location. #### isPriority - Type: `Boolean` - Default: `false` Whether or not the URL(s) should be treated as "high priority" targets. By default, calls to `prefetch()` are low priority. > **Note:** This behaves identically to `listen()`'s `priority` option. ### quicklink.prerender(urls, eagerness) Returns: `Promise` > **Important:** You much `catch` you own request error(s). #### urls - Type: `String` or `Array` - Required: `true` One or many URLs to be prerendered. > **Note:** Speculative Rules API supports same-site cross origin Prerendering with [opt-in header](https://bit.ly/ss-cross-origin-pre). #### eagerness - Type: `String` - Default: `immediate` Determines the mode to be used for prerendering specified within the speculation rules. ## Polyfills `quicklink`: - Includes a very small fallback for [requestIdleCallback](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback) - 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. ## Recipes ### Set a custom timeout for prefetching resources Defaults to 2 seconds (via `requestIdleCallback`). Here we override it to 4 seconds: {% endmarkdownConvert %} {% highlight "js" %} quicklink.listen({ timeout: 4000, }); {% endhighlight %} {% markdownConvert %} ### Set a specific Anchor Elements NodeList to observe for in-viewport links Defaults to `document` otherwise. {% endmarkdownConvert %} {% highlight "js" %} quicklink.listen({ el: document.querySelectorAll('a.linksToPrefetch'), }); {% endhighlight %} {% markdownConvert %} ### Set the DOM element to observe for in-viewport links Defaults to `document` otherwise. {% endmarkdownConvert %} {% highlight "js" %} quicklink.listen({ el: document.getElementById('carousel'), }); {% endhighlight %} {% markdownConvert %} ### Programmatically `prefetch()` URLs If you would prefer to provide a static list of URLs to be prefetched, instead of detecting those in-viewport, customizing URLs is supported. {% endmarkdownConvert %} {% highlight "js" %} // Single URL quicklink.prefetch('2.html'); // Multiple URLs quicklink.prefetch(['2.html', '3.html', '4.js']); // Multiple URLs, with high priority // Note: Can also be use with single URL! quicklink.prefetch(['2.html', '3.html', '4.js'], true); {% endhighlight %} {% markdownConvert %} ### Programmatically `prerender()` URLs If you would prefer to provide a static list of URLs to be prerendered, instead of detecting those in-viewport, customizing URLs is supported. {% endmarkdownConvert %} {% highlight "js" %} // Single URL quicklink.prerender('2.html'); // Multiple URLs quicklink.prerender(['2.html', '3.html', '4.js']); {% endhighlight %} {% markdownConvert %} ### Set the request priority for prefetches while scrolling Defaults to low-priority (`rel=prefetch` or XHR). For high-priority (`priority: true`), attempts to use `fetch()` or falls back to XHR. > **Note:** This runs `prefetch(..., true)` with URLs found within the `options.el` container. {% endmarkdownConvert %} {% highlight "js" %} quicklink.listen({priority: true}); {% endhighlight %} {% markdownConvert %} ### Specify a custom list of allowed origins Provide a list of hostnames that should be prefetch-able. Only the same origin is allowed by default. > **Important:** You must also include your own hostname! {% endmarkdownConvert %} {% highlight "js" %} quicklink.listen({ origins: [ // add mine 'my-website.com', 'api.my-website.com', // add third-parties 'other-website.com', 'example.com', // ... ], }); {% endhighlight %} {% markdownConvert %} ### Allow all origins Enables all cross-origin requests to be made. > **Note:** You may run into [CORB](https://chromium.googlesource.com/chromium/src/+/main/services/network/cross_origin_read_blocking_explainer.md) and [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) issues! {% endmarkdownConvert %} {% highlight "js" %} quicklink.listen({ origins: true, // or origins: [], }); {% endhighlight %} {% markdownConvert %} ### Custom Ignore Patterns These filters run _after_ the `origins` matching has run. Ignores can be useful for avoiding large file downloads or for responding to DOM attributes dynamically. {% endmarkdownConvert %} {% highlight "js" %} // Same-origin restraint is enabled by default. // // This example will ignore all requests to: // - all "/api/*" pathnames // - all ".zip" extensions // - all tags with "noprefetch" attribute // quicklink.listen({ ignores: [ /\/api\/?/, uri => uri.includes('.zip'), (uri, elem) => elem.hasAttribute('noprefetch'), ], }); {% endhighlight %} {% markdownConvert %} You may also wish to ignore prefetches to URLs which contain a URL fragment (e.g. `index.html#top`). This can be useful if you (1) are using anchors to headings in a page or (2) have URL fragments setup for a single-page application, and which to avoid firing prefetches for similar URLs. Using `ignores` this can be achieved as follows: {% endmarkdownConvert %} {% highlight "js" %} quicklink.listen({ ignores: [ uri => uri.includes('#'), // or RegExp: /#(.+)/ // or element matching: (uri, elem) => !!elem.hash ], }); {% endhighlight %} {% markdownConvert %} ### Custom URL to prefetch via hrefFn callback The hrefFn method allows to build the URL to prefetch (e.g. API endpoint) on the fly instead of the prefetching the `href` attribute URL. {% endmarkdownConvert %} {% highlight "js" %} quicklink.listen({ hrefFn(element) { return element.href.replace('html', 'json'); }, }); {% endhighlight %} {% markdownConvert %} ## Browser Support The prefetching provided by `quicklink` can be viewed as a [progressive enhancement](https://www.smashingmagazine.com/2009/04/progressive-enhancement-what-it-is-and-how-to-use-it/). Cross-browser support is as follows: - Without polyfills: Chrome, Safari ≥ 12.1, Firefox, Edge, Opera, Android Browser, Samsung Internet. - With [Intersection Observer polyfill](https://github.com/GoogleChromeLabs/intersection-observer) ~6KB gzipped/minified: Safari ≤ 12.0, IE11 - With the above and a [Set()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) and [Array.from](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from) polyfill: IE9 and IE10. [Core.js](https://github.com/zloirock/core-js) provides both `Set()` and `Array.from()` shims. Projects like [es6-shim](https://github.com/paulmillr/es6-shim/blob/master/README.md) are an alternative you can consider. Certain features have layered support: - The [Network Information API](https://wicg.github.io/netinfo/), which is used to check if the user has a slow effective connection type (via `navigator.connection.effectiveType`) is only available in [Chrome 61+ and Opera 57+](https://caniuse.com/netinfo) - If opting for `{priority: true}` and the [Fetch API](https://fetch.spec.whatwg.org/) isn't available, XHR will be used instead. ## Using the prefetcher directly A `prefetch` method can be individually imported for use in other projects. This method includes the logic to respect Data Saver and 2G connections. It also issues requests thru `fetch()`, XHRs, or `link[rel=prefetch]` depending on (a) the `isPriority` value and (b) the current browser's support. After installing `quicklink` as a dependency, you can use it as follows: {% endmarkdownConvert %} {% highlight "html" %} {% endhighlight %} {% endblock %} ================================================ FILE: site/src/approach.njk ================================================ --- title: Approach layout: layouts/base.njk description: Faster subsequent page-loads by prefetching in-viewport links during idle time. eleventyNavigation: key: Approach order: 2 sections: howItWorks: title: "How it works" summary: "Quicklink attempts to make navigations to subsequent pages load faster. It:" --- {% extends "layouts/normal-section-wrapper.njk" %} {% block section %} {% markdownConvert %} ## How it works {% endmarkdownConvert %} Image shows how prefetching improves page load speed by fetching resources in advance {% markdownConvert %} Quicklink attempts to make navigations to subsequent pages load faster. It: * **Detects links within the viewport** (using [Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)) * **Waits until the browser is idle** (using [requestIdleCallback](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback)) * **Checks if the user isn't on a slow connection** (using `navigator.connection.effectiveType`) or has data-saver enabled (using `navigator.connection.saveData`) * **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). {% endmarkdownConvert %} Image shows page load times comparison with and without Quicklink prefetching, showing faster performance when prefetch is enabled {% markdownConvert %} ## Future: Double-keyed HTTP Cache 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. 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. 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. 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. ## Session Stitching Cross-origin prefetching (e.g `a.com/foo.html` prefetches `b.com/bar.html`) has a number of limitations. One such limitation is with session-stitching. `b.com` may expect `a.com`'s navigation requests to include session information (e.g a temporary ID - e.g `b.com/bar.html?hash=<>×tamp=<>`), where this information is used to customize the experience or log information to analytics. If session-stitching requires a timestamp in the URL, what is prefetched and stored in the HTTP cache may not be the same as the one the user ultimately navigates to. This introduces a challenge as it can result in double prefetches. To workaround this problem, you can consider passing along session information via the [ping attribute](https://caniuse.com/ping) (separately) so the origin can stitch a session together asynchronously. ## Ad-related considerations 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). Ads appear on sites mostly in two ways: - **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. - **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). {% endmarkdownConvert %} {% endblock %} ================================================ FILE: site/src/assets/js/script.js ================================================ window.addEventListener('load', () => { 'use strict'; function initGoToTopBtn() { const goTopBtn = document.querySelector('.back-to-top'); function trackScroll() { const scrolled = window.pageYOffset; const threshold = 400; if (scrolled > threshold) { goTopBtn.classList.remove('hidden'); } if (scrolled < threshold) { goTopBtn.classList.add('hidden'); } } function scrollToTop() { const c = document.documentElement.scrollTop || document.body.scrollTop; if (c > 0) { window.requestAnimationFrame(scrollToTop); window.scrollTo(0, c - c / 8); } } function backToTop() { if (window.pageYOffset > 0) { scrollToTop(); } } window.addEventListener('scroll', trackScroll, {passive: true}); goTopBtn.addEventListener('click', backToTop); } initGoToTopBtn(); const clipboard = new ClipboardJS('#copy-snippet-button', { text: trigger => trigger.parentNode.previousElementSibling.textContent.trim(), }); clipboard.on('success', event => { event.clearSelection(); event.trigger.blur(); const notifyCopiedSnippet = document.querySelector('.notify-copied-snippet'); notifyCopiedSnippet.classList.add('notify-copied-snippet--displayed'); }); clipboard.on('error', event => { console.error('[clipboard error] Action:', event.action); console.error('[clipboard error] Trigger:', event.trigger); }); }); ================================================ FILE: site/src/assets/styles/_copy-snippet.scss ================================================ .copy-snippet-widget { background: linear-gradient(hsla(0deg, 0%, 100%, .775), hsla(0deg, 0%, 100%, .7)); border-radius: 6px; padding: 10px 18px 17px; margin: 1.5em 0; box-shadow: inset 0 1px 0 hsla(0deg, 0%, 100%, 1), 0 3px 20px hsla(0deg, 0%, 0%, .1); } .snippet-for-copy { // border: 1px solid hsla(0, 0%, 0%, .15); border-radius: 1px; // background: hsla(0, 0%, 0%, .075); margin: 10px 0; font-size: .95em; } .copy-snippet-widget .snippet-for-copy pre { margin-bottom: 0; } .notify-copied-snippet { font-size: .8em; margin-top: .35em; margin-left: .35em; display: none; } .notify-copied-snippet--displayed { display: block; } .button { background: linear-gradient(hsla(0deg, 0%, 95%, 1), hsla(0deg, 0%, 90%, 1)); border: 1px solid; --border-color-opacity: .25; --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)); border-color: var(--border-color); border-radius: 4px; padding: 4px 10px; --light-opacity: 1; --shadow-opacity: .05; --shadow-blur-radius: 1px; --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 box-shadow: var(--box-shadow); cursor: pointer; font-size: 16px; } .button--copy-snippet { background: radial-gradient(ellipse at top, hsla(0deg, 0%, 100%, .25), transparent), linear-gradient(hsla(330deg, 73%, 49%, .65), hsla(330deg, 73%, 49%, 1)); color: #fff; text-shadow: 0 1px 1px hsla(0deg, 0%, 0%, .25); --light-opacity: .2; --border-color-opacity: .25; vertical-align: top; font-size: .8em; padding: 7px 14px; border-radius: 7px; --shadow-opacity: .25; --shadow-blur-radius: 3px; font-weight: 700; letter-spacing: .01em; } @media (min-width: 600px) { .button--copy-snippet { font-size: 1em; } } ================================================ FILE: site/src/assets/styles/github-markdown.scss ================================================ // stylelint-disable declaration-no-important @use "vendor/github-markdown"; .markdown-body { box-sizing: border-box; min-width: 200px; max-width: 980px; margin: 0 auto; padding: 45px; // TODO: !important use does not look pefect, the github font looks better font-family: Arial, Helvetica, sans-serif !important; // TODO: opt for font color color: hsl(226deg, 52%, 27%) !important; } .markdown-body h1, h2, h3, h4, h5 { // Google Sans is for headlines only per Google guidelines font-family: Arial, Helvetica, sans-serif !important; } @media (max-width: 767px) { .markdown-body { padding: 15px; } .markdown-body h2 { font-size: 1.2em !important; } } ================================================ FILE: site/src/assets/styles/main.scss ================================================ @use "vendor/prism"; @use "copy-snippet"; html { height: 100%; } html, body { min-height: 100%; } body { margin: 0; font-family: Arial, Helvetica, sans-serif; } p, ul { margin: 0 0 16px; font-style: normal; } .page-header { text-align: center; background: #fff; padding-top: 1rem; box-shadow: 0 0 6px rgba(57, 73, 76, .35); } .page-header__title { display: flex; justify-content: flex-start; align-items: center; padding: 7px; } .page-header__subtitle { font-size: 1em; margin: 0; } .page-header__logo-link { margin-right: 14px; border: none; border-right: 2px solid rgb(197, 197, 197); } .page-header__logo-image { width: 160px; height: auto; margin-right: 6px; } .page-header__navigation a { position: relative; display: inline-block; text-decoration: none; border: none; white-space: nowrap; padding: 8px 0; min-width: 40px; z-index: 1; } .page-header__navigation a::before { content: ""; position: absolute; z-index: -1; top: 0; bottom: 0; left: -.5em; right: -.5em; background-color: #46aee8; transform-origin: 50% 100%; transform: scaleY(0); transition: transform .15s ease-in-out; } .page-header__navigation a:hover::before { transform: scaleY(1); transform-origin: 50% 100%; } .page-header__navigation a.active { pointer-events: none; } .page-header__navigation a.active::before { transform: scaleY(1); opacity: .5; transform-origin: 50% 100%; } .page-header__navigation ul { margin: 0; padding: .5rem .5rem 0; } .page-header__navigation li { display: inline; margin: 6px 0 0 15px; } .page-header__navigation li:last-child { margin-right: 0; } main.page-main { padding: 0; width: 96%; margin-top: 16px; margin-bottom: 16px; } .page-main > h1 { color: #fff; } .page-main > ul { margin: 0; padding: 0; } .hidden { display: none; } .text-center { text-align: center; } .center { display: flex; justify-content: center; } .back-to-top { opacity: 1; pointer-events: all; position: fixed; bottom: 3rem; right: 3rem; z-index: 9999; width: 30px; height: 30px; text-align: center; line-height: 30px; background: #fff; box-shadow: 0 2px 8px rgba(0, 0, 0, .35); cursor: pointer; border-radius: 50%; transform: translate3d(0, 0, 0); transition: transform .3s ease; } .back-to-top:hover { transform: translate3d(0, -2px, 0); } .back-to-top.hidden { opacity: 0; pointer-events: none; transform: translate3d(300%, 0, 0); transition: transform .3s ease, opacity .3s ease; } // no bullet list .page-main ul.no-bullet { list-style-type: none; padding-left: 0; } .list-icon svg { margin-right: 1rem; } // grid .flex-grid { display: flex; } .overflow-x-auto { overflow-x: auto; } .flex-grid .flex-grid__item { margin-left: 24px; } .flex-grid .flex-grid__item:last-child { margin-right: 16px; } .normal-section { padding: 0 8px; } .trusted-by img { max-width: 92px; } .use-with img { width: 112px; min-width: 92px; } .highlighted-section { padding: 16px 24px; margin: 16px 0; } .highlighted-section__text { // TODO: opt for bg color // background-color: #d8217d; background-color: #283646; color: #fff; } .highlighted-section__text p { color: #fff; } .highlighted-section.highlighted-section__text h2 { border: 0; } // heading .primary-font-color { // TODO: opt for font color color: hsl(226deg, 52%, 27%); } .secondary-font-color { color: #fe8ec6; } .tertiary-font-color { color: #d74b91; } main.page-main .heading { letter-spacing: -1px; font-size: 1.5em; font-weight: 900; margin: 0; border: 0; } main.page-main .heading em { font-style: normal; white-space: nowrap; } .flex-between-center { display: flex; justify-content: space-between; align-items: center; } .large-github { display: none; } .article-image { margin: 20px auto; display: block; max-width: 100%; height: auto; } @media (min-width: 600px) { main.page-main .heading { font-size: 2.1em; margin: 24px 0 18px; } .page-header__navigation a { padding: 16px 0; min-width: 50px; } .page-header__subtitle { margin: 1.7em 0; } .page-header__logo-image { width: 200px; } .small-github { display: none; } .large-github { display: block; } } @media (min-width: 992px) { body { background-color: #eee; } main.page-main { background-color: rgb(255, 255, 255); box-shadow: 0 1px 6px rgba(57, 73, 76, .35); margin-top: 3.2rem; margin-bottom: 3.2rem; padding-bottom: 1rem; } .page-header { padding-top: 0; } .page-header-content { display: flex; justify-content: space-between; align-items: center; min-width: 200px; max-width: 980px; margin: 0 auto; } .page-header__title { padding: 14px; } .page-header__logo-link { display: inline-block; } .page-header__logo-image { width: 222px; } .page-header nav { display: inline-block; } main.page-main .heading { margin: 24px 0; } .normal-section { padding: 0 24px; } .highlighted-section { padding: 16px 24px; margin: 16px 0; } } .site-footer { margin-top: 1rem; border-top: 1px dotted #cecece; } .site-footer p.text-center { margin-top: 16px; } ================================================ FILE: site/src/assets/styles/vendor/_github-markdown.scss ================================================ .markdown-body .octicon { display: inline-block; fill: currentcolor; vertical-align: text-bottom; } .markdown-body .anchor { float: left; line-height: 1; margin-left: -20px; padding-right: 4px; } .markdown-body .anchor:focus { outline: 0; } .markdown-body h1 .octicon-link, .markdown-body h2 .octicon-link, .markdown-body h3 .octicon-link, .markdown-body h4 .octicon-link, .markdown-body h5 .octicon-link, .markdown-body h6 .octicon-link { color: #1b1f23; vertical-align: middle; visibility: hidden; } .markdown-body h1:hover .anchor, .markdown-body h2:hover .anchor, .markdown-body h3:hover .anchor, .markdown-body h4:hover .anchor, .markdown-body h5:hover .anchor, .markdown-body h6:hover .anchor { text-decoration: none; } .markdown-body h1:hover .anchor .octicon-link, .markdown-body h2:hover .anchor .octicon-link, .markdown-body h3:hover .anchor .octicon-link, .markdown-body h4:hover .anchor .octicon-link, .markdown-body h5:hover .anchor .octicon-link, .markdown-body h6:hover .anchor .octicon-link { visibility: visible; } .markdown-body { text-size-adjust: 100%; color: #24292e; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-size: 16px; line-height: 1.5; word-wrap: break-word; } .markdown-body .pl-c { color: #6a737d; } .markdown-body .pl-c1, .markdown-body .pl-s .pl-v { color: #005cc5; } .markdown-body .pl-e, .markdown-body .pl-en { color: #6f42c1; } .markdown-body .pl-s .pl-s1, .markdown-body .pl-smi { color: #24292e; } .markdown-body .pl-ent { color: #22863a; } .markdown-body .pl-k { color: #d73a49; } .markdown-body .pl-pds, .markdown-body .pl-s, .markdown-body .pl-s .pl-pse .pl-s1, .markdown-body .pl-sr, .markdown-body .pl-sr .pl-cce, .markdown-body .pl-sr .pl-sra, .markdown-body .pl-sr .pl-sre { color: #032f62; } .markdown-body .pl-smw, .markdown-body .pl-v { color: #e36209; } .markdown-body .pl-bu { color: #b31d28; } .markdown-body .pl-ii { background-color: #b31d28; color: #fafbfc; } .markdown-body .pl-c2 { background-color: #d73a49; color: #fafbfc; } .markdown-body .pl-c2::before { content: "^M"; } .markdown-body .pl-sr .pl-cce { color: #22863a; font-weight: 700; } .markdown-body .pl-ml { color: #735c0f; } .markdown-body .pl-mh, .markdown-body .pl-mh .pl-en, .markdown-body .pl-ms { color: #005cc5; font-weight: 700; } .markdown-body .pl-mi { color: #24292e; font-style: italic; } .markdown-body .pl-mb { color: #24292e; font-weight: 700; } .markdown-body .pl-md { background-color: #ffeef0; color: #b31d28; } .markdown-body .pl-mi1 { background-color: #f0fff4; color: #22863a; } .markdown-body .pl-mc { background-color: #ffebda; color: #e36209; } .markdown-body .pl-mi2 { background-color: #005cc5; color: #f6f8fa; } .markdown-body .pl-mdr { color: #6f42c1; font-weight: 700; } .markdown-body .pl-ba { color: #586069; } .markdown-body .pl-sg { color: #959da5; } .markdown-body .pl-corl { color: #032f62; text-decoration: underline; } .markdown-body details { display: block; } .markdown-body summary { display: list-item; } .markdown-body a { background-color: transparent; } .markdown-body a:active, .markdown-body a:hover { outline-width: 0; } .markdown-body strong { font-weight: inherit; font-weight: bolder; } .markdown-body h1 { font-size: 2em; margin: .67em 0; } .markdown-body img { border-style: none; } .markdown-body code, .markdown-body kbd, .markdown-body pre { font-family: monospace, monospace; font-size: 1em; } .markdown-body hr { box-sizing: content-box; height: 0; overflow: visible; } .markdown-body input { font: inherit; margin: 0; } .markdown-body input { overflow: visible; } .markdown-body [type="checkbox"] { box-sizing: border-box; padding: 0; } .markdown-body * { box-sizing: border-box; } .markdown-body input { font-family: inherit; font-size: inherit; line-height: inherit; } .markdown-body a { color: #0366d6; text-decoration: none; } .markdown-body a:hover { text-decoration: underline; } .markdown-body strong { font-weight: 600; } .markdown-body hr { background: 0 0; border: 0; border-bottom: 1px solid #dfe2e5; height: 0; margin: 15px 0; overflow: hidden; } .markdown-body hr::before { content: ""; display: table; } .markdown-body hr::after { clear: both; content: ""; display: table; } .markdown-body table { border-collapse: collapse; border-spacing: 0; } .markdown-body td, .markdown-body th { padding: 0; } .markdown-body details summary { cursor: pointer; } .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 { margin-bottom: 0; margin-top: 0; } .markdown-body h1 { font-size: 32px; } .markdown-body h1, .markdown-body h2 { font-weight: 600; } .markdown-body h2 { font-size: 24px; } .markdown-body h3 { font-size: 20px; } .markdown-body h3, .markdown-body h4 { font-weight: 600; } .markdown-body h4 { font-size: 16px; } .markdown-body h5 { font-size: 14px; } .markdown-body h5, .markdown-body h6 { font-weight: 600; } .markdown-body h6 { font-size: 12px; } .markdown-body p { margin-bottom: 10px; margin-top: 0; } .markdown-body blockquote { margin: 0; } .markdown-body ol, .markdown-body ul { margin-bottom: 0; margin-top: 0; padding-left: 0; } .markdown-body ol ol, .markdown-body ul ol { list-style-type: lower-roman; } .markdown-body ol ol ol, .markdown-body ol ul ol, .markdown-body ul ol ol, .markdown-body ul ul ol { list-style-type: lower-alpha; } .markdown-body dd { margin-left: 0; } .markdown-body code, .markdown-body pre { font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 12px; } .markdown-body pre { margin-bottom: 0; margin-top: 0; } .markdown-body input::-webkit-inner-spin-button, .markdown-body input::-webkit-outer-spin-button { appearance: none; margin: 0; } .markdown-body .border { border: 1px solid #e1e4e8 !important; } .markdown-body .border-0 { border: 0 !important; } .markdown-body .border-bottom { border-bottom: 1px solid #e1e4e8 !important; } .markdown-body .rounded-1 { border-radius: 3px !important; } .markdown-body .bg-white { background-color: #fff !important; } .markdown-body .bg-gray-light { background-color: #fafbfc !important; } .markdown-body .text-gray-light { color: #6a737d !important; } .markdown-body .mb-0 { margin-bottom: 0 !important; } .markdown-body .my-2 { margin-bottom: 8px !important; margin-top: 8px !important; } .markdown-body .pl-0 { padding-left: 0 !important; } .markdown-body .py-0 { padding-bottom: 0 !important; padding-top: 0 !important; } .markdown-body .pl-1 { padding-left: 4px !important; } .markdown-body .pl-2 { padding-left: 8px !important; } .markdown-body .py-2 { padding-bottom: 8px !important; padding-top: 8px !important; } .markdown-body .pl-3, .markdown-body .px-3 { padding-left: 16px !important; } .markdown-body .px-3 { padding-right: 16px !important; } .markdown-body .pl-4 { padding-left: 24px !important; } .markdown-body .pl-5 { padding-left: 32px !important; } .markdown-body .pl-6 { padding-left: 40px !important; } .markdown-body .f6 { font-size: 12px !important; } .markdown-body .lh-condensed { line-height: 1.25 !important; } .markdown-body .text-bold { font-weight: 600 !important; } .markdown-body::before { content: ""; display: table; } .markdown-body::after { clear: both; content: ""; display: table; } .markdown-body > :first-child { margin-top: 0 !important; } .markdown-body > :last-child { margin-bottom: 0 !important; } .markdown-body a:not([href]) { color: inherit; text-decoration: none; } .markdown-body blockquote, .markdown-body dl, .markdown-body ol, .markdown-body p, .markdown-body pre, .markdown-body table, .markdown-body ul { margin-bottom: 16px; margin-top: 0; } .markdown-body hr { background-color: #e1e4e8; border: 0; height: .25em; margin: 24px 0; padding: 0; } .markdown-body blockquote { border-left: .25em solid #dfe2e5; color: #6a737d; padding: 0 1em; } .markdown-body blockquote > :first-child { margin-top: 0; } .markdown-body blockquote > :last-child { margin-bottom: 0; } .markdown-body kbd { background-color: #fafbfc; border: 1px solid #c6cbd1; border-bottom-color: #959da5; border-radius: 3px; box-shadow: inset 0 -1px 0 #959da5; color: #444d56; display: inline-block; font-size: 11px; line-height: 10px; padding: 3px 5px; vertical-align: middle; } .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 { font-weight: 600; line-height: 1.25; margin-bottom: 16px; margin-top: 24px; } .markdown-body h1 { font-size: 2em; } .markdown-body h1, .markdown-body h2 { border-bottom: 1px solid #eaecef; padding-bottom: .3em; } .markdown-body h2 { font-size: 1.5em; } .markdown-body h3 { font-size: 1.25em; } .markdown-body h4 { font-size: 1em; } .markdown-body h5 { font-size: .875em; } .markdown-body h6 { color: #6a737d; font-size: .85em; } .markdown-body ol, .markdown-body ul { padding-left: 2em; } .markdown-body ol ol, .markdown-body ol ul, .markdown-body ul ol, .markdown-body ul ul { margin-bottom: 0; margin-top: 0; } .markdown-body li { word-wrap: break-all; } .markdown-body li > p { margin-top: 16px; } .markdown-body li + li { margin-top: .3em; } .markdown-body dl { padding: 0; } .markdown-body dl dt { font-size: 1em; font-style: italic; font-weight: 600; margin-top: 16px; padding: 0; } .markdown-body dl dd { margin-bottom: 16px; padding: 0 16px; } .markdown-body table { display: block; overflow: auto; width: 100%; } .markdown-body table th { font-weight: 600; } .markdown-body table td, .markdown-body table th { border: 1px solid #dfe2e5; padding: 6px 13px; } .markdown-body table tr { background-color: #fff; border-top: 1px solid #c6cbd1; } .markdown-body table tr:nth-child(2n) { background-color: #f6f8fa; } .markdown-body img { background-color: #fff; box-sizing: content-box; max-width: 100%; height: auto; } .markdown-body img[align="right"] { padding-left: 20px; } .markdown-body img[align="left"] { padding-right: 20px; } .markdown-body code { background-color: rgba(27, 31, 35, .05); border-radius: 3px; font-size: 85%; margin: 0; padding: .2em .4em; } .markdown-body pre { word-wrap: normal; } .markdown-body pre > code { background: 0 0; border: 0; font-size: 100%; margin: 0; padding: 0; white-space: pre; word-break: normal; } .markdown-body .highlight { margin-bottom: 16px; } .markdown-body .highlight pre { margin-bottom: 0; word-break: normal; } .markdown-body .highlight pre, .markdown-body pre { background-color: #f6f8fa; border-radius: 3px; font-size: 85%; line-height: 1.45; overflow: auto; padding: 16px; } .markdown-body pre code { background-color: transparent; border: 0; display: inline; line-height: inherit; margin: 0; max-width: auto; overflow: visible; padding: 0; word-wrap: normal; } .markdown-body .commit-tease-sha { color: #444d56; display: inline-block; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 90%; } .markdown-body .blob-wrapper { border-bottom-left-radius: 3px; border-bottom-right-radius: 3px; overflow-x: auto; overflow-y: hidden; } .markdown-body .blob-wrapper-embedded { max-height: 240px; overflow-y: auto; } .markdown-body .blob-num { color: rgba(27, 31, 35, .3); cursor: pointer; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 12px; line-height: 20px; min-width: 50px; padding-left: 10px; padding-right: 10px; text-align: right; user-select: none; vertical-align: top; white-space: nowrap; width: 1%; } .markdown-body .blob-num:hover { color: rgba(27, 31, 35, .6); } .markdown-body .blob-num::before { content: attr(data-line-number); } .markdown-body .blob-code { line-height: 20px; padding-left: 10px; padding-right: 10px; position: relative; vertical-align: top; } .markdown-body .blob-code-inner { color: #24292e; font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 12px; overflow: visible; white-space: pre; word-wrap: normal; } .markdown-body .pl-token.active, .markdown-body .pl-token:hover { background: #ffea7f; cursor: pointer; } .markdown-body kbd { background-color: #fafbfc; border: 1px solid #d1d5da; border-bottom-color: #c6cbd1; border-radius: 3px; box-shadow: inset 0 -1px 0 #c6cbd1; color: #444d56; display: inline-block; font: 11px SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace; line-height: 10px; padding: 3px 5px; vertical-align: middle; } .markdown-body :checked + .radio-label { border-color: #0366d6; position: relative; z-index: 1; } .markdown-body .tab-size[data-tab-size="1"] { tab-size: 1; } .markdown-body .tab-size[data-tab-size="2"] { tab-size: 2; } .markdown-body .tab-size[data-tab-size="3"] { tab-size: 3; } .markdown-body .tab-size[data-tab-size="4"] { tab-size: 4; } .markdown-body .tab-size[data-tab-size="5"] { tab-size: 5; } .markdown-body .tab-size[data-tab-size="6"] { tab-size: 6; } .markdown-body .tab-size[data-tab-size="7"] { tab-size: 7; } .markdown-body .tab-size[data-tab-size="8"] { tab-size: 8; } .markdown-body .tab-size[data-tab-size="9"] { tab-size: 9; } .markdown-body .tab-size[data-tab-size="10"] { tab-size: 10; } .markdown-body .tab-size[data-tab-size="11"] { tab-size: 11; } .markdown-body .tab-size[data-tab-size="12"] { tab-size: 12; } .markdown-body .task-list-item { list-style-type: none; } .markdown-body .task-list-item + .task-list-item { margin-top: 3px; } .markdown-body .task-list-item input { margin: 0 .2em .25em -1.6em; vertical-align: middle; } .markdown-body hr { border-bottom-color: #eee; } .markdown-body .pl-0 { padding-left: 0 !important; } .markdown-body .pl-1 { padding-left: 4px !important; } .markdown-body .pl-2 { padding-left: 8px !important; } .markdown-body .pl-3 { padding-left: 16px !important; } .markdown-body .pl-4 { padding-left: 24px !important; } .markdown-body .pl-5 { padding-left: 32px !important; } .markdown-body .pl-6 { padding-left: 40px !important; } .markdown-body .pl-7 { padding-left: 48px !important; } .markdown-body .pl-8 { padding-left: 64px !important; } .markdown-body .pl-9 { padding-left: 80px !important; } .markdown-body .pl-10 { padding-left: 96px !important; } .markdown-body .pl-11 { padding-left: 112px !important; } .markdown-body .pl-12 { padding-left: 128px !important; } ================================================ FILE: site/src/assets/styles/vendor/_prism.scss ================================================ /** * prism.js tomorrow night eighties for JavaScript, CoffeeScript, CSS and HTML * Based on https://github.com/chriskempson/tomorrow-theme * @author Rose Pritchard */ code[class*="language-"], pre[class*="language-"] { color: #ccc; background: none; font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; font-size: 1em; text-align: left; white-space: pre; word-spacing: normal; word-break: normal; word-wrap: normal; line-height: 1.5; -moz-tab-size: 4; -o-tab-size: 4; tab-size: 4; -webkit-hyphens: none; -moz-hyphens: none; -ms-hyphens: none; hyphens: none; } /* Code blocks */ pre[class*="language-"] { padding: 1em; margin: .5em 0; overflow: auto; } :not(pre) > code[class*="language-"], pre[class*="language-"] { background: #2d2d2d; } /* Inline code */ :not(pre) > code[class*="language-"] { padding: .1em; border-radius: .3em; white-space: normal; } .token.comment, .token.block-comment, .token.prolog, .token.doctype, .token.cdata { color: #999; } .token.punctuation { color: #ccc; } .token.tag, .token.attr-name, .token.namespace, .token.deleted { color: #e2777a; } .token.function-name { color: #6196cc; } .token.boolean, .token.number, .token.function { color: #f08d49; } .token.property, .token.class-name, .token.constant, .token.symbol { color: #f8c555; } .token.selector, .token.important, .token.atrule, .token.keyword, .token.builtin { color: #cc99cd; } .token.string, .token.char, .token.attr-value, .token.regex, .token.variable { color: #7ec699; } .token.operator, .token.entity, .token.url { color: #67cdcc; } .token.important, .token.bold { font-weight: bold; } .token.italic { font-style: italic; } .token.entity { cursor: help; } .token.inserted { color: green; } ================================================ FILE: site/src/demo.njk ================================================ --- title: Demo layout: layouts/base.njk description: Faster subsequent page-loads by prefetching in-viewport links during idle time. eleventyNavigation: key: Demo order: 3 --- {% extends "layouts/normal-section-wrapper.njk" %} {% block section %} {% markdownConvert %} ## Demos This page contains some demo sites that use Quicklink to improve navigation, grouped by architecture: **Multi Page Apps** / **Single Page Apps**. If you like the library, and want to try them on your site, check out the **Installation** section of the [home page](/). #### Multi Page Apps In this demo you’ll compare an ecommerce site with and without Quicklink to see how navigation is improved thanks to the library. The following waterfall shows a typical navigation for a site without Quicklink (top) vs. the same site using the library (bottom): {% endmarkdownConvert %} Image shows that use of quicklink improves navigation for a site by 1 second {% markdownConvert %} To try the demo: 1. Open the [unoptimized site](https://mini-ecomm.glitch.me/) in Chrome. 1. Open **DevTools** and go to the **Network panel** to simulate a **Fast 3G** Connection. 1. Pick **Galaxy S5** as a simulated device. 1. Make sure **Disable cache** is not checked. 1. Reload the page. {% endmarkdownConvert %} Image shows chrome dev tools inspection for site not using quicklink {% markdownConvert %} Now, measure performance on the same site, that uses Quicklink: 1. Open the [optimized site](https://mini-ecomm-quicklink.glitch.me/) in Chrome. 1. Open **DevTools** and go to the **Network panel** to simulate a **Fast 3G** Connection. 1. Pick **Galaxy S5** as a simulated device. 1. Make sure **Disable cache** is not checked. 1. Reload the page. Prefetched links can be identified in the **Network** panel by having `quicklink` as the **Initiator** and **Lowest** as the **Priority**: {% endmarkdownConvert %} Image shows chrome dev tools inspection for site using quicklink {% markdownConvert %} To measure the impact of `quicklink` on navigations: 1. Clear the **Network** trace. 1. Click on a list item. 1. Take a look at the **Network** panel. {% endmarkdownConvert %} Image shows the site using quicklink along with prefetch cache improves data fetching time by 97 percent {% markdownConvert %} 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. Here is a comparison video: {% endmarkdownConvert %} The gif shows comparison between same site being loaded with quicklink takes 1 second less than unoptimised version {% markdownConvert %} ### Single Page Apps 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/). To try the demo: 1. Open the [optimized site](https://create-react-app-quicklink.glitch.me/) in Chrome. 1. Open DevTools and go to the **Network** panel to simulate a **Fast 3G** Connection. 1. Pick **Galaxy S5** as a simulated device. 1. Make sure **Disable cache** is not checked. 1. Reload the page. 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: {% endmarkdownConvert %} Image shows that data for loading links available on the page being viewed are loaded beforehand by quicklink {% markdownConvert %} Next: 1. Clear the **Network** log again. 1. Make sure **Disable** cache is not checked. 1. Click the Blog link to navigate to that page. {% endmarkdownConvert %} Image shows that quicklink takes only 2 milliseconds to load a site using prefetch cache {% markdownConvert %} 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! {% endmarkdownConvert %} {% endblock %} ================================================ FILE: site/src/index.njk ================================================ --- title: Home layout: layouts/base.njk description: Faster subsequent page-loads by prefetching in-viewport links during idle time. eleventyNavigation: key: Home order: 0 extra_head: --- {% include "components/heading.njk" %} {% include "components/why-quicklink.njk" %} {% include "components/copy-snippet.njk" %} {% include "components/download.njk" %} {% include "components/trusted-by.njk" %} {% include "components/installation.njk" %} {% include "components/usage.njk" %} {% include "components/over-prefetching.njk" %} {% include "components/react.njk" %} {% include "components/chrome-extension.njk" %} {% include "components/why-prefetch.njk" %} {% include "components/use-with.njk" %} ================================================ FILE: site/src/measure.njk ================================================ --- title: Measure layout: layouts/base.njk description: Faster subsequent page-loads by prefetching in-viewport links during idle time. eleventyNavigation: key: Measure order: 4 --- {% extends "layouts/normal-section-wrapper.njk" %} {% block section %} {% markdownConvert %} ## Measuring impact of Quicklink in sites Implementing Quicklink in sites can speed up navigations, by automatically prefetching in-viewport links during idle time. 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). In this section, we explore different ways of measuring the impact of Quicklink in sites. To showcase that, we’ll use the following sites: - 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). - 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). 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: {% endmarkdownConvert %} {% highlight "js" %} {% endhighlight %} {% markdownConvert %} ## Using Chrome DevTools 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. First, measure performance before implementing Quicklink: - Open the [unoptimized demo](https://mini-ecomm.glitch.me/) in Chrome. - Open the **Network** panel and simulate a **Fast 3G** Connection. - Pick **Galaxy S5** as simulated device. - Make sure **Disable cache** is not checked. - Reload the page. - Click on the first product in the listing. Take a look at the **Time** column: the product page takes approximately **2.5s** to load: {% endmarkdownConvert %} Image shows chrome dev tools inspection for site not using quicklink {% markdownConvert %} Now, measure performance after implementing Quicklink: - Open the [optimized demo](https://mini-ecomm-quicklink.glitch.me/) in Chrome. - Open the **Network** panel and simulate a Fast 3G Connection. - Pick **Galaxy S5** as simulated device. - Make sure **Disable cache** is not checked. - Reload the page. Prefetched links can be identified in the Network panel by having Quicklink as the **Initiator** and **Lowest** as the Priority: {% endmarkdownConvert %} Image shows chrome dev tools inspection for site using quicklink {% markdownConvert %} To measure the impact of Quicklink on navigations: - Click on a list item. - Take a look at the **Network** panel. {% endmarkdownConvert %} Image shows the site using quicklink along with prefetch cache improves data fetching time by 97 percent {% markdownConvert %} 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. ## Using Webpagetest 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. Open to webpagetest.org. Pick **Nexus 5** as Test Location. In the Advanced Settings tab, pick **3GFast** in the connection type. In the Script tab, place the following script: ``` logData 0 navigate https://mini-ecomm.glitch.me/ logData 1 execAndWait document.querySelector('a').click() ``` 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/). Repeat the process on [the demo](https://mini-ecomm-quicklink.glitch.me/) that uses Quicklink. The script looks like: ``` logData 0 navigate https://mini-ecomm.glitch.me/ logData 1 execAndWait document.querySelector('a').click() ``` Here is the [resultiing test](https://www.webpagetest.org/result/191103_E3_f8217e45ad837ac084868d4f3b9a4a73/). The following table compares the main metrics obtained for each of the sites: {% endmarkdownConvert %} Image shows WPT results for site loaded with quicklink are much faster than without quicklink {% markdownConvert %} Next, create a comparison between the tests. - Open the [WPT test](https://www.webpagetest.org/result/191103_TM_e68d81788d8744762301b44c6e3e72d2/) for the unoptimized site. - Click on the median run, that appears in the "First View" cell of the report. - Repeat the same process in the [optimized test](https://www.webpagetest.org/result/191103_E3_f8217e45ad837ac084868d4f3b9a4a73/). 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`: ``` https://www.webpagetest.org/video/compare.php?tests=test_id_1,test_id_2 ``` 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). ### Visual Comparison The unoptimized site starts rendering approximately at **2.5s**, the demo that uses Quicklink, starts at **1.2s**. {% endmarkdownConvert %} Image shows that use of quicklink improves load time for a site by 1.3 seconds {% markdownConvert %} ### Video 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**. {% endmarkdownConvert %} The gif shows comparison between same site being loaded with quicklink takes 1 second less than the unoptimised version {% markdownConvert %} ### Using RUM (Real user monitoring) tools 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). 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. 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). 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. **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. ### Using Quicklink Chrome extension [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. 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. ### Conclusion 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. 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. 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. {% endmarkdownConvert %} {% endblock %} ================================================ FILE: site/src/robots.njk ================================================ --- permalink: /robots.txt layout: null eleventyExcludeFromCollections: true --- # www.robotstxt.org User-agent: * Disallow:{% if site.isNetlify %} /{% endif %} Sitemap: {{ site.url + "/sitemap.xml" }} ================================================ FILE: site/src/site.webmanifest ================================================ { "name": "Quicklink", "short_name": "Quicklink", "icons": [ { "src": "/assets/images/icons/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/assets/images/icons/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } ], "start_url": "/", "theme_color": "#fff", "background_color": "#fff", "display": "standalone" } ================================================ FILE: site/src/sitemap.njk ================================================ --- permalink: /sitemap.xml layout: null eleventyExcludeFromCollections: true --- {% for page in collections.all -%} {{ site.url + page.url }} {{ page.date.toISOString() }} {%- endfor %} ================================================ FILE: site/watch.json ================================================ { "install": { "include": [ "^package\\.json$", "^\\.env$" ] }, "restart": { "exclude": [ "^src/", "^dist/" ], "include": [ "watch.json$", ".eleventy.js$" ] }, "throttle": 1000 } ================================================ FILE: src/chunks.mjs ================================================ /** * Copyright 2018 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. **/ import throttle from 'throttles'; import {viaFetch, supported} from './prefetch.mjs'; import requestIdleCallback from './request-idle-callback.mjs'; // Cache of URLs we've prefetched // Its `size` is compared against `opts.limit` value. const toPrefetch = new Set(); /** * Determine if the anchor tag should be prefetched. * A filter can be a RegExp, Function, or Array of both. * - Function receives `node.href, node` arguments * - RegExp receives `node.href` only (the full URL) * @param {Element} node The anchor () tag. * @param {Mixed} filter The custom filter(s) * @return {Boolean} If true, then it should be ignored */ function isIgnored(node, filter) { if (Array.isArray(filter)) { return filter.some(x => isIgnored(node, x)); } return (filter.test || filter).call(filter, node.href, node); } /** * Prefetch an array of URLs if the user's effective * connection type and data-saver preferences suggests * it would be useful. By default, looks at in-viewport * links for `document`. Can also work off a supplied * DOM element or static array of URLs. * @param {Object} options - Configuration options for quicklink * @param {Object} [options.el] - DOM element to prefetch in-viewport links of * @param {Boolean} [options.priority] - Attempt higher priority fetch (low or high) * @param {Array} [options.origins] - Allowed origins to prefetch (empty allows all) * @param {Array|RegExp|Function} [options.ignores] - Custom filter(s) that run after origin checks * @param {Number} [options.timeout] - Timeout after which prefetching will occur * @param {Number} [options.throttle] - The concurrency limit for prefetching * @param {Number} [options.limit] - The total number of prefetches to allow * @param {Function} [options.timeoutFn] - Custom timeout function * @param {Function} [options.onError] - Error handler for failed `prefetch` requests * @param {Function} [options.prefetchChunks] - Function to prefetch chunks for route URLs (with route manifest for URL mapping) * @return {undefined} */ export function listen(options = {}) { if (!window.IntersectionObserver) return; const [toAdd, isDone] = throttle(options.throttle || Number.Infinity); const limit = options.limit || Number.Infinity; const allowed = options.origins || [location.hostname]; const ignores = options.ignores || []; const timeoutFn = options.timeoutFn || requestIdleCallback; const {prefetchChunks} = options; const prefetchHandler = urls => { prefetch(urls, options.priority) .then(isDone) .catch(error => { isDone(); if (options.onError) options.onError(error); }); }; const observer = new IntersectionObserver(entries => { for (const {isIntersecting, target} of entries) { if (!isIntersecting) continue; observer.unobserve(target); // Do not prefetch if will match/exceed limit if (toPrefetch.size < limit) { toAdd(() => { prefetchChunks ? prefetchChunks(target, prefetchHandler) : prefetchHandler(target.href); }); } } }); timeoutFn(() => { // Find all links & Connect them to IO if allowed const links = (options.el || document).querySelectorAll('a[href]'); for (const link of links) { // If the anchor matches a permitted origin // ~> A `[]` or `true` means everything is allowed if (!allowed.length || allowed.includes(link.hostname)) { // If there are any filters, the link must not match any of them if (!isIgnored(link, ignores)) observer.observe(link); } } }, { timeout: options.timeout || 2000, }); return () => { // wipe url list toPrefetch.clear(); // detach IO entries observer.disconnect(); }; } /** * Prefetch a given URL with an optional preferred fetch priority * @param {String} url - the URL to fetch * @param {Boolean} [isPriority] - if is "high" priority * @return {Object} a Promise */ export function prefetch(url, isPriority) { const {connection} = navigator; if (!connection) return Promise.resolve(); // Don't prefetch if using 2G or if Save-Data is enabled. if (connection.saveData) { return Promise.reject(new Error('Cannot prefetch, Save-Data is enabled')); } if (/2g/.test(connection.effectiveType)) { return Promise.reject(new Error('Cannot prefetch, network conditions are poor')); } // Dev must supply own catch() return Promise.all([url].flat().map(str => { if (toPrefetch.has(str)) return []; // Add it now, regardless of its success // ~> so that we don't repeat broken links toPrefetch.add(str); return (isPriority ? viaFetch : supported)(new URL(str, location.href).toString()); })); } ================================================ FILE: src/index.mjs ================================================ /** * Copyright 2018 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. **/ import throttle from 'throttles'; import {prefetchOnHover, supported, viaFetch} from './prefetch.mjs'; import requestIdleCallback from './request-idle-callback.mjs'; import {addSpeculationRules, removeSpeculationRule, hasSpecRulesSupport} from './prerender.mjs'; // Cache of URLs we've prefetched // Its `size` is compared against `opts.limit` value. const toPrefetch = new Set(); // Cache of URLs we've prerendered const toPrerender = new Set(); // global var to keep prerenderAndPrefer option let shouldPrerenderAndPrefetch = false; /** * Determine if the anchor tag should be prefetched. * A filter can be a RegExp, Function, or Array of both. * - Function receives `node.href, node` arguments * - RegExp receives `node.href` only (the full URL) * @param {Element} node The anchor () tag. * @param {Mixed} filter The custom filter(s) * @return {Boolean} If true, then it should be ignored */ function isIgnored(node, filter) { return Array.isArray(filter) ? filter.some(x => isIgnored(node, x)) : (filter.test || filter).call(filter, node.href, node); } /** * Checks network conditions * @param {NetworkInformation} conn The connection information to be checked * @return {Boolean|Object} Error Object if the constrainsts are met or boolean otherwise */ function checkConnection(conn) { // If no connection object, assume it's okay to prefetch if (!conn) return true; // Don't prefetch if Save-Data is enabled. if (conn.saveData) { return new Error('Save-Data is enabled'); } // Don't prefetch if using 2G connection. if (/2g/.test(conn.effectiveType)) { return new Error('network conditions are poor'); } return true; } /** * Prefetch an array of URLs if the user's effective * connection type and data-saver preferences suggests * it would be useful. By default, looks at in-viewport * links for `document`. Can also work off a supplied * DOM element or static array of URLs. * @param {Object} options - Configuration options for quicklink * @param {Object|Array} [options.el] - DOM element(s) to prefetch in-viewport links of * @param {Boolean} [options.priority] - Attempt higher priority fetch (low or high) * @param {Boolean} [options.checkAccessControlAllowOrigin] - Check Access-Control-Allow-Origin response header * @param {Boolean} [options.checkAccessControlAllowCredentials] - Check the Access-Control-Allow-Credentials response header * @param {Boolean} [options.onlyOnMouseover] - Enable the prefetch only on mouseover event * @param {Array} [options.origins] - Allowed origins to prefetch (empty allows all) * @param {Array|RegExp|Function} [options.ignores] - Custom filter(s) that run after origin checks * @param {Number} [options.timeout] - Timeout after which prefetching will occur * @param {Number} [options.throttle] - The concurrency limit for prefetching * @param {Number} [options.threshold] - The area percentage of each link that must have entered the viewport to be fetched * @param {Number} [options.limit] - The total number of prefetches to allow * @param {Number} [options.delay] - Time each link needs to stay inside viewport before prefetching (milliseconds) * @param {Function} [options.timeoutFn] - Custom timeout function * @param {Function} [options.onError] - Error handler for failed `prefetch` requests * @param {Function} [options.hrefFn] - Function to use to build the URL to prefetch. * If it's not a valid function, then it will use the entry href. * @param {Boolean} [options.prerender] - Option to switch from prefetching and use prerendering only * @param {String} [options.eagerness] - Prerender eagerness mode - default immediate * @param {Boolean} [options.prerenderAndPrefetch] - Option to use both prerendering and prefetching * @return {Function} */ export function listen(options = {}) { if (!window.IntersectionObserver || !('isIntersecting' in IntersectionObserverEntry.prototype)) return; const [toAdd, isDone] = throttle(options.throttle || 1 / 0); const limit = options.limit || 1 / 0; const threshold = options.threshold || 0; const allowed = options.origins || [location.hostname]; const ignores = options.ignores || []; const delay = options.delay || 0; const hrefsInViewport = new Map(); const specRulesInViewport = new Map(); const timeoutFn = options.timeoutFn || requestIdleCallback; const hrefFn = typeof options.hrefFn === 'function' && options.hrefFn; const shouldOnlyPrerender = options.prerender || false; shouldPrerenderAndPrefetch = options.prerenderAndPrefetch || false; const setTimeoutIfDelay = (callback, delay) => { if (!delay) { callback(); return; } setTimeout(callback, delay); }; const observer = new IntersectionObserver(entries => { for (let entry of entries) { const set = hrefsInViewport.get(entry.target.href) || new Set(); hrefsInViewport.set(entry.target.href, set); // On enter if (entry.isIntersecting) { entry = entry.target; // Adding href to set of hrefsInViewport set.add(entry); // Setting timeout setTimeoutIfDelay(() => { // Do not prefetch if not found in viewport if (!set || !set.size) return; if (!shouldOnlyPrerender && !shouldPrerenderAndPrefetch) { observer.unobserve(entry); } // prerender, if.. // either it's the prerender + prefetch mode or it's prerender *only* mode // Prerendering limit is following options.limit. UA may impose arbitraty numeric limit // The same URL is not already present as a speculation rule if ( (shouldPrerenderAndPrefetch || shouldOnlyPrerender) && toPrerender.size < limit && !specRulesInViewport.has(entry.href) ) { prerender(hrefFn ? hrefFn(entry) : entry.href, options.eagerness) .then(specMap => { for (const [key, value] of specMap) { specRulesInViewport.set(key, value); } }) .catch(error => { if (options.onError) { options.onError(error); } else { throw error; } }); return; } // Do not prefetch if will match/exceed limit and user has not switched to shouldOnlyPrerender mode if (toPrefetch.size < limit && !shouldOnlyPrerender) { toAdd(() => { prefetch( hrefFn ? hrefFn(entry) : entry.href, options.priority, options.checkAccessControlAllowOrigin, options.checkAccessControlAllowCredentials, options.onlyOnMouseover, ) .then(isDone) .catch(error => { isDone(); if (options.onError) options.onError(error); }); }); } }, delay); // On exit } else { entry = entry.target; set.delete(entry); if (specRulesInViewport.has(entry.href)) { specRulesInViewport.set(removeSpeculationRule(specRulesInViewport, entry.href)); } } } }, { threshold, }); timeoutFn(() => { // Find all links & Connect them to IO if allowed const isAnchorElement = options.el && options.el.length > 0 && options.el[0].nodeName === 'A'; const elementsToListen = isAnchorElement ? options.el : (options.el || document).querySelectorAll('a'); for (const link of elementsToListen) { // If the anchor matches a permitted origin // ~> A `[]` or `true` means everything is allowed if (!allowed.length || allowed.includes(link.hostname)) { // If there are any filters, the link must not match any of them if (!isIgnored(link, ignores)) observer.observe(link); } } }, { timeout: options.timeout || 2000, }); return () => { // wipe url list toPrefetch.clear(); // detach IO entries observer.disconnect(); }; } /** * Prefetch a given URL with an optional preferred fetch priority * @param {String | String[]} urls - the URLs to fetch * @param {Boolean} isPriority - if is "high" priority * @param {Boolean} checkAccessControlAllowOrigin - true to set crossorigin="anonymous" for DOM prefetch * and mode:'cors' for API fetch * @param {Boolean} checkAccessControlAllowCredentials - true to set credentials:'include' for API fetch * @param {Boolean} onlyOnMouseover - true to enable prefetch only on mouseover event * @return {Object} a Promise */ export function prefetch(urls, isPriority, checkAccessControlAllowOrigin, checkAccessControlAllowCredentials, onlyOnMouseover) { const chkConn = checkConnection(navigator.connection); if (chkConn instanceof Error) { return Promise.reject(new Error(`Cannot prefetch, ${chkConn.message}`)); } if (toPrerender.size > 0 && !shouldPrerenderAndPrefetch) { console.warn('[Warning] You are using both prefetching and prerendering on the same document'); } // Dev must supply own catch() return Promise.all([urls].flat().map(str => { if (toPrefetch.has(str)) return []; // Add it now, regardless of its success // ~> so that we don't repeat broken links toPrefetch.add(str); return prefetchOnHover( isPriority ? viaFetch : supported, new URL(str, location.href).toString(), onlyOnMouseover, checkAccessControlAllowOrigin, checkAccessControlAllowCredentials, isPriority, ); })); } /** * Prerender a given URL * @param {String | String[]} urls - the URLs to fetch * @param {String} eagerness - prerender eagerness mode - default immediate * @return {Object} a Promise */ export function prerender(urls, eagerness = 'immediate') { urls = [urls].flat(); const chkConn = checkConnection(navigator.connection); if (chkConn instanceof Error) { return Promise.reject(new Error(`Cannot prerender, ${chkConn.message}`)); } // prerendering preconditions: // 1) whether UA supports spec rules.. If not, fallback to prefetch // Note: Prerendering supports same-site cross origin with opt-in header if (!hasSpecRulesSupport()) { prefetch(urls, true, false, false, eagerness === 'moderate' || eagerness === 'conservative'); return Promise.reject(new Error('This browser does not support the speculation rules API. Falling back to prefetch.')); } for (const url of urls) { toPrerender.add(url); } // check if both prerender and prefetch exists.. throw a warning but still proceed if (toPrefetch.size > 0 && !shouldPrerenderAndPrefetch) { console.warn('[Warning] You are using both prefetching and prerendering on the same document'); } const specMap = addSpeculationRules(urls, eagerness); return specMap.size > 0 ? Promise.resolve(specMap) : Promise.reject(specMap); } ================================================ FILE: src/prefetch.mjs ================================================ /** * Portions copyright 2018 Google Inc. * Inspired by Gatsby's prefetching logic, with those portions * remaining MIT. Additions include support for Fetch API, * XHR switching, SaveData and Effective Connection Type checking. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. **/ /** * Checks if a feature on `link` is natively supported. * Examples of features include `prefetch` and `preload`. * @param {Object} link - Link object. * @return {Boolean} whether the feature is supported */ function hasPrefetch(link) { link = document.createElement('link'); return link.relList && link.relList.supports && link.relList.supports('prefetch'); } /** * Fetches a given URL using `` * @param {string} url - the URL to fetch * @param {Boolean} hasCrossorigin - true to set crossorigin="anonymous" * @return {Object} a Promise */ function viaDOM(url, hasCrossorigin) { return new Promise((resolve, reject, link) => { link = document.createElement('link'); link.rel = 'prefetch'; link.href = url; if (hasCrossorigin) { link.setAttribute('crossorigin', 'anonymous'); } link.onload = resolve; link.onerror = reject; document.head.append(link); }); } /** * Fetches a given URL using XMLHttpRequest * @param {string} url - the URL to fetch * @param {Boolean} hasCredentials - true to set withCredentials:true * @return {Object} a Promise */ function viaXHR(url, hasCredentials) { return new Promise((resolve, reject, request) => { request = new XMLHttpRequest(); request.open('GET', url, request.withCredentials = hasCredentials); request.setRequestHeader('Accept', '*/*'); request.onload = () => { if (request.status === 200) { resolve(); } else { // eslint-disable-next-line prefer-promise-reject-errors reject(); } }; request.send(); }); } /** * Fetches a given URL using the Fetch API. Falls back * to XMLHttpRequest if the API is not supported. * @param {string} url - the URL to fetch * @param {Boolean} hasModeCors - true to set mode:'cors' * @param {Boolean} hasCredentials - true to set credentials:'include' * @param {Boolean} isPriority - true to set priority:'high' * @return {Object} a Promise */ export function viaFetch(url, hasModeCors, hasCredentials, isPriority) { // TODO: Investigate using preload for high-priority // fetches. May have to sniff file-extension to provide // valid 'as' values. In the future, we may be able to // use Priority Hints here. // // As of 2018, fetch() is high-priority in Chrome // and medium-priority in Safari. const options = {headers: {accept: '*/*'}}; if (!hasModeCors) options.mode = 'no-cors'; if (hasCredentials) options.credentials = 'include'; options.priority = isPriority ? 'high' : 'low'; return window.fetch ? fetch(url, options) : viaXHR(url, hasCredentials); } /** * Calls the prefetch function immediately * or only on the mouseover event. * @param {Function} callback - original prefetch function * @param {String} url - url to prefetch * @param {Boolean} onlyOnMouseover - true to add the mouseover listener * @return {Object} a Promise */ export function prefetchOnHover(callback, url, onlyOnMouseover, ...args) { if (!onlyOnMouseover) return callback(url, ...args); const elements = document.querySelectorAll(`a[href="${url}"]`); const timerMap = new Map(); for (const el of elements) { const mouseenterListener = () => { const timer = setTimeout(() => { el.removeEventListener('mouseenter', mouseenterListener); el.removeEventListener('mouseleave', mouseleaveListener); return callback(url, ...args); }, 200); timerMap.set(el, timer); }; const mouseleaveListener = () => { const timer = timerMap.get(el); if (timer) { clearTimeout(timer); timerMap.delete(el); } }; el.addEventListener('mouseenter', mouseenterListener); el.addEventListener('mouseleave', mouseleaveListener); } } export const supported = hasPrefetch() ? viaDOM : viaFetch; ================================================ FILE: src/prerender.mjs ================================================ /** * Portions copyright 2018 Google Inc. * Inspired by Gatsby's prefetching logic, with those portions * remaining MIT. Additions include support for Fetch API, * XHR switching, SaveData and Effective Connection Type checking. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. **/ /** * Add a given set of urls to the speculation rules * @param {String[]} urlsToPrerender - the URLs to add to speculation rules * @param {String} eagerness - prerender eagerness mode * @return {Map|Error} Map of script elements to their URLs or Error Object */ export function addSpeculationRules(urlsToPrerender, eagerness) { const specMap = new Map(); try { for (const url of urlsToPrerender) { const specScript = document.createElement('script'); specScript.type = 'speculationrules'; specScript.text = JSON.stringify({ prerender: [{ source: 'list', urls: [url], eagerness, }], }); document.head.append(specScript); specMap.set(url, specScript); } } catch (error) { return error; } return specMap; } /** * Removes a speculation rule script associated with a given URL * @param {Map} specMap - Map of URLs to their script elements * @param {string} url - The URL whose speculation rule should be removed * @return {Map|Error} The updated map after removal or Error Object */ export function removeSpeculationRule(specMap, url) { const specScript = specMap.get(url); try { specScript.remove(); specMap.delete(url); } catch (error) { return error; } return specMap; } /** * Check whether UA supports Speculation Rules API * @return {Boolean} whether UA has support for Speculation Rules API */ export function hasSpecRulesSupport() { return HTMLScriptElement.supports('speculationrules'); } ================================================ FILE: src/react-chunks.js ================================================ /** * Copyright 2019-2020 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. **/ import React, {useEffect, useRef, useState} from 'react'; import rmanifest from 'route-manifest'; import {listen} from './quicklink.js'; const useIntersect = ({root = null, rootMargin, threshold = 0} = {}) => { const [entry, updateEntry] = useState({}); const [node, setNode] = useState(null); const observer = useRef(null); useEffect(() => { if (observer.current) observer.current.disconnect(); observer.current = new window.IntersectionObserver( ([entry]) => updateEntry(entry), { root, rootMargin, threshold, }, ); const {current: currentObserver} = observer; if (node) currentObserver.observe(node); return () => currentObserver.disconnect(); }, [node, root, rootMargin, threshold]); return [setNode, entry]; }; const __defaultAccessor = mix => (mix && mix.href) || mix || ''; const prefetchChunks = (entry, prefetchHandler, accessor = __defaultAccessor) => { const {files} = rmanifest(window.__rmanifest, entry.pathname); const chunkURLs = files.map(accessor).filter(Boolean); if (chunkURLs.length > 0) { prefetchHandler(chunkURLs); } else { // also prefetch regular links in-viewport prefetchHandler(entry.href); } }; const withQuicklink = (Component, options = {}) => { // eslint-disable-next-line react/display-name return props => { const [ref, entry] = useIntersect({root: document.body.parentElement}); const {intersectionRatio} = entry; useEffect(() => { options.prefetchChunks = prefetchChunks; if (intersectionRatio > 0) { listen(options); } }, [intersectionRatio]); return (
); }; }; export { withQuicklink, }; ================================================ FILE: src/request-idle-callback.mjs ================================================ /** * Copyright 2018 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. **/ // RIC and shim for browsers setTimeout() without it const requestIdleCallback = window.requestIdleCallback || (cb => { const start = Date.now(); return setTimeout(() => { cb({ didTimeout: false, timeRemaining: () => Math.max(0, 50 - (Date.now() - start)), }); }, 1); }); export default requestIdleCallback; ================================================ FILE: test/fixtures/1.html ================================================ Prefetch experiments

Page 1

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!

================================================ FILE: test/fixtures/2.html ================================================ Prefetch experiments

Page 2

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!

================================================ FILE: test/fixtures/3.html ================================================ Prefetch experiments

Page 3

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!

================================================ FILE: test/fixtures/4.html ================================================ Prefetch experiments

Page 4

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!

================================================ FILE: test/fixtures/index.html ================================================ Prefetch experiments
Link 1 Link 2 Link 3
CSS
Link 4 ================================================ FILE: test/fixtures/main.css ================================================ body { font-family: Roboto, Arial, sans-serif; } .screen { height: 100vh; } ================================================ FILE: test/fixtures/rmanifest.json ================================================ { "/about": [ { "type": "style", "href": "/test/static/css/about.00ec0d84.chunk.css" }, { "type": "script", "href": "/test/static/js/about.921ebc84.chunk.js" } ], "/blog": [ { "type": "style", "href": "/test/static/css/blog.2a8b6ab6.chunk.css" }, { "type": "script", "href": "/test/static/js/blog.1dcce8a6.chunk.js" } ], "/": [ { "type": "style", "href": "/test/static/css/home.6d953f22.chunk.css" }, { "type": "script", "href": "/test/static/js/home.14835906.chunk.js" }, { "type": "image", "href": "/test/static/media/video.b9b6e9e1.svg" } ], "/blog/:title": [ { "type": "style", "href": "/test/static/css/article.cb6f97df.chunk.css" }, { "type": "script", "href": "/test/static/js/article.cb6f97df.chunk.js" } ], "*": [ { "type": "script", "href": "/test/static/js/6.7f61b1a1.chunk.js" } ] } ================================================ FILE: test/fixtures/test-allow-origin-all.html ================================================ Prefetch: Allow All Origins Link 1 Link 2 Link 3 Spinner
CSS
Link 4 ================================================ FILE: test/fixtures/test-allow-origin.html ================================================ Prefetch: Allowed Origins Link 1 Link 2 Spinner
CSS
Link 4 ================================================ FILE: test/fixtures/test-basic-usage.html ================================================ Prefetch: Basic Usage Link 1 Link 2 Link 3
CSS
Link 4 Link 1 again ================================================ FILE: test/fixtures/test-custom-dom-source.html ================================================ Prefetch: Custom DOM source Link 1 Link 2 Link 3
CSS
Link 4 ================================================ FILE: test/fixtures/test-custom-href-function.html ================================================ Prefetch: Custom function to build the URL. Link 1 Link 2 Link 3 Link 4 ================================================ FILE: test/fixtures/test-delay.html ================================================ Prefetch: Basic Usage Link 1 Link 2 Link 3
CSS
Link 4 ================================================ FILE: test/fixtures/test-es-modules.html ================================================ Prefetch: ES Modules Link 1 Link 2 Link 3
CSS
Link 4 ================================================ FILE: test/fixtures/test-ignore-basic.html ================================================ Prefetch: Ignore Basic Link 1 Link 2 Link 3 Spinner
CSS
Link 4 ================================================ FILE: test/fixtures/test-ignore-multiple.html ================================================ Prefetch: Ignore Multiple Link 1 Link 2 Link 3 Spinner
CSS
Link 4 ================================================ FILE: test/fixtures/test-limit.html ================================================ Prefetch: Basic Usage Link 1 Link 2 Link 3 Link 4 ================================================ FILE: test/fixtures/test-node-list.html ================================================ Prefetch: NodeList case Link 1
Link 2
Link 3
Link 4 ================================================ FILE: test/fixtures/test-prefetch-chunks.html ================================================ Prefetch: Chunk URL list Home Blog About
CSS
Link 4 ================================================ FILE: test/fixtures/test-prefetch-duplicate-shared.html ================================================ Prefetch: Static URL list Link 2
CSS
Link 4 ================================================ FILE: test/fixtures/test-prefetch-duplicate.html ================================================ Prefetch: Static URL list Link 1 Link 2 Link 3
CSS
Link 4 ================================================ FILE: test/fixtures/test-prefetch-multiple.html ================================================ Prefetch: Static URL list Link 1 Link 2 Link 3
CSS
Link 4 ================================================ FILE: test/fixtures/test-prefetch-single.html ================================================ Prefetch: Static URL list Link 1 Link 2 Link 3
CSS
Link 4 ================================================ FILE: test/fixtures/test-prerender-andPrefetch.html ================================================ Prefetch: Basic Usage Link 1 Link 2 Link 3
CSS
Link 4 ================================================ FILE: test/fixtures/test-prerender-only.html ================================================ Prefetch: Basic Usage Link 1 Link 2 Link 3
CSS
Link 4 ================================================ FILE: test/fixtures/test-prerender-wrapper-multiple.html ================================================ Prefetch: Basic Usage Link 1 Link 2 Link 3
CSS
Link 4 ================================================ FILE: test/fixtures/test-prerender-wrapper-single.html ================================================ Prefetch: Basic Usage Link 1 Link 2 Link 3
CSS
Link 4 ================================================ FILE: test/fixtures/test-same-origin.html ================================================ Prefetch: Same Origin Link 1 Link 2 Spinner
CSS
Link 4 ================================================ FILE: test/fixtures/test-threshold.html ================================================ Prefetch: Basic Usage ================================================ FILE: test/fixtures/test-throttle.html ================================================ Prefetch: Basic Usage Link 1 Link 2 Link 3 Link 4 ================================================ FILE: test/quicklink.spec.js ================================================ 'use strict'; const puppeteer = require('puppeteer'); const {suite} = require('uvu'); const assert = require('uvu/assert'); const host = 'http://127.0.0.1:8080'; const server = `${host}/test/fixtures`; const mainSuite = suite('quicklink tests'); // Default 1000 ms const sleep = (ms = 1000) => new Promise(resolve => { setTimeout(resolve, ms); }); const puppeteerOptions = { headless: true, slowMo: 100, timeout: 20000, }; mainSuite.before(async context => { context.browser = await puppeteer.launch(puppeteerOptions); context.page = await context.browser.newPage(); }); mainSuite.after(async context => { await context.page.close(); context.browser.close(); }); mainSuite('should prefetch in-viewport links correctly (UMD)', async context => { const responseURLs = []; context.page.on('response', resp => { responseURLs.push(resp.url()); }); await context.page.goto(`${server}/test-basic-usage.html`); await sleep(); assert.instance(responseURLs, Array); assert.ok(responseURLs.includes(`${server}/1.html`)); assert.ok(responseURLs.includes(`${server}/2.html`)); assert.ok(responseURLs.includes(`${server}/3.html`)); assert.ok(!responseURLs.includes(`${server}/4.html`)); }); mainSuite('should prefetch in-viewport links correctly (ES Modules)', async context => { const responseURLs = []; context.page.on('response', resp => { responseURLs.push(resp.url()); }); await context.page.goto(`${server}/test-es-modules.html`); await sleep(); assert.instance(responseURLs, Array); assert.ok(responseURLs.includes(`${server}/1.html`)); assert.ok(responseURLs.includes(`${server}/2.html`)); assert.ok(responseURLs.includes(`${server}/3.html`)); assert.ok(!responseURLs.includes(`${server}/4.html`)); }); mainSuite('should prefetch in-viewport links that scroll into view correctly (UMD)', async context => { const responseURLs = []; context.page.on('response', resp => { responseURLs.push(resp.url()); }); await context.page.goto(`${server}/test-basic-usage.html`); await context.page.setViewport({ width: 1200, height: 800, }); await context.page.evaluate(_ => { window.scrollBy(0, window.innerHeight); }); await sleep(); assert.instance(responseURLs, Array); assert.ok(responseURLs.includes(`${server}/1.html`)); assert.ok(responseURLs.includes(`${server}/2.html`)); assert.ok(responseURLs.includes(`${server}/3.html`)); assert.ok(responseURLs.includes(`${server}/4.html`)); }); mainSuite('should prefetch in-viewport links from a custom DOM source', async context => { const responseURLs = []; context.page.on('response', resp => { responseURLs.push(resp.url()); }); await context.page.goto(`${server}/test-custom-dom-source.html`); await sleep(); assert.instance(responseURLs, Array); assert.ok(responseURLs.includes(`${server}/main.css`)); }); mainSuite('should prefetch in-viewport links from NodeList', async context => { const responseURLs = []; context.page.on('response', resp => { responseURLs.push(resp.url()); }); await context.page.goto(`${server}/test-node-list.html`); await sleep(); assert.instance(responseURLs, Array); assert.ok(responseURLs.includes(`${server}/2.html`)); assert.ok(responseURLs.includes(`${server}/3.html`)); }); mainSuite('should only prefetch links if allowed in origins list', async context => { const responseURLs = []; context.page.on('response', resp => { responseURLs.push(resp.url()); }); await context.page.goto(`${server}/test-allow-origin.html`); await sleep(1000); assert.instance(responseURLs, Array); // => origins: ['github.githubassets.com'] assert.not.ok(responseURLs.includes(`${server}/2.html`)); assert.ok(responseURLs.includes('https://example.com/1.html')); assert.ok(responseURLs.includes('https://github.githubassets.com/images/spinners/octocat-spinner-32.gif')); }); mainSuite('should prefetch all links when allowing all origins', async context => { const responseURLs = []; context.page.on('response', resp => { responseURLs.push(resp.url()); }); await context.page.goto(`${server}/test-allow-origin-all.html`); await sleep(); assert.instance(responseURLs, Array); // => origins: true assert.ok(responseURLs.includes(`${server}/2.html`)); assert.ok(responseURLs.includes('https://google.com/')); assert.ok(responseURLs.includes('https://example.com/1.html')); assert.ok(responseURLs.includes(`${server}/2.html`)); assert.ok(responseURLs.includes('https://github.githubassets.com/images/spinners/octocat-spinner-32.gif')); }); mainSuite('should only prefetch links of same origin (default)', async context => { const responseURLs = []; context.page.on('response', resp => { responseURLs.push(resp.url()); }); await context.page.goto(`${server}/test-same-origin.html`); await sleep(); assert.instance(responseURLs, Array); // => origins: [location.hostname] (default) assert.ok(responseURLs.includes(`${server}/2.html`)); assert.not.ok(responseURLs.includes('https://example.com/1.html')); assert.not.ok(responseURLs.includes('https://github.githubassets.com/images/spinners/octocat-spinner-32.gif')); }); mainSuite('should only prefetch links after ignore patterns allowed it', async context => { const responseURLs = []; context.page.on('response', resp => { responseURLs.push(resp.url()); }); await context.page.goto(`${server}/test-ignore-basic.html`); await sleep(); assert.instance(responseURLs, Array); // => origins: [location.hostname] (default) // => ignores: /2.html/ // via ignores assert.not.ok(responseURLs.includes(`${server}/2.html`)); // via same origin assert.not.ok(responseURLs.includes('https://example.com/1.html')); // via same origin assert.not.ok(responseURLs.includes('https://github.githubassets.com/images/spinners/octocat-spinner-32.gif')); }); mainSuite('should only prefetch links after ignore patterns allowed it (multiple)', async context => { const responseURLs = []; context.page.on('response', resp => { responseURLs.push(resp.url()); }); await context.page.goto(`${server}/test-ignore-multiple.html`); await sleep(); assert.instance(responseURLs, Array); // => origins: true (all) // => ignores: [...] assert.ok(responseURLs.includes(`${server}/2.html`)); // /example/ assert.not.ok(responseURLs.includes('https://example.com/1.html')); // (uri) => uri.includes('foobar') assert.not.ok(responseURLs.includes('https://foobar.com/3.html')); // (uri, elem) => elem.textContent.includes('Spinner') assert.not.ok(responseURLs.includes('https://github.githubassets.com/images/spinners/octocat-spinner-32.gif')); }); mainSuite('should accept a single URL to prefetch()', async context => { const responseURLs = []; context.page.on('response', resp => { responseURLs.push(resp.url()); }); await context.page.goto(`${server}/test-prefetch-single.html`); await sleep(); assert.instance(responseURLs, Array); assert.ok(responseURLs.includes(`${server}/2.html`)); }); mainSuite('should accept multiple URLs to prefetch()', async context => { const responseURLs = []; context.page.on('response', resp => { responseURLs.push(resp.url()); }); await context.page.goto(`${server}/test-prefetch-multiple.html`); await sleep(); // don't care about first 3 URLs (markup) const ours = responseURLs.slice(3); assert.is(ours.length, 3); assert.ok(ours.includes(`${server}/2.html`)); assert.ok(ours.includes(`${server}/3.html`)); assert.ok(ours.includes(`${server}/4.html`)); }); mainSuite('should not prefetch() the same URL repeatedly', async context => { const responseURLs = []; context.page.on('response', resp => { responseURLs.push(resp.url()); }); await context.page.goto(`${server}/test-prefetch-duplicate.html`); await sleep(); // don't care about first 3 URLs (markup) const ours = responseURLs.slice(3); assert.is(ours.length, 1); assert.ok(ours.includes(`${server}/2.html`)); }); // TODO Fix and enable the test later mainSuite.skip('should not call the same URL repeatedly (shared)', async context => { const responseURLs = []; context.page.on('response', resp => { responseURLs.push(resp.url()); }); await context.page.goto(`${server}/test-prefetch-duplicate-shared.html`); await sleep(); // count occurrences of our link const target = responseURLs.filter(x => x === `${server}/2.html`); assert.is(target.length, 1); }); mainSuite('should not exceed the `limit` total', async context => { const responseURLs = []; context.page.on('response', resp => { responseURLs.push(resp.url()); }); await context.page.goto(`${server}/test-limit.html`); await sleep(); // don't care about first 3 URLs (markup) const ours = responseURLs.slice(3); assert.is(ours.length, 1); assert.ok(ours.includes(`${server}/1.html`)); }); mainSuite('should respect the `throttle` concurrency', async context => { const URLs = []; // Note: Page makes 4 requests // Make HTML requests take a long time // ~> so that we can ensure throttling occurs await context.page.setRequestInterception(true); context.page.on('request', async req => { const url = req.url(); if (/test\/fixtures\/\d+\.html$/i.test(url)) { await sleep(100); URLs.push(url); return req.respond({status: 200}); } req.continue(); }); await context.page.goto(`${server}/test-throttle.html`); // Only 2 should be done by now // Note: Parallel requests, w/ 50ms buffer await sleep(150); assert.is(URLs.length, 2); // All should be done by now // Note: Parallel requests, w/ 50ms buffer await sleep(250); assert.is(URLs.length, 4); }); mainSuite('should prefetch using a custom function to build the URL', async context => { const responseURLs = []; context.page.on('response', resp => { responseURLs.push(resp.url()); }); await context.page.goto(`${server}/test-custom-href-function.html`); await sleep(); // don't care about first 3 URLs (markup) const ours = responseURLs.slice(3); assert.ok(ours.includes(`https://example.com/?url=${server}/1.html`)); assert.ok(ours.includes(`https://example.com/?url=${server}/2.html`)); assert.ok(ours.includes(`https://example.com/?url=${server}/3.html`)); assert.ok(ours.includes(`https://example.com/?url=${server}/4.html`)); }); mainSuite('should delay prefetch for in-viewport links correctly (UMD)', async context => { const responseURLs = []; context.page.on('response', resp => { responseURLs.push(resp.url()); }); await context.page.goto(`${server}/test-delay.html`); await sleep(); assert.instance(responseURLs, Array); assert.ok(responseURLs.includes(`${server}/1.html`)); assert.ok(responseURLs.includes(`${server}/2.html`)); assert.ok(responseURLs.includes(`${server}/3.html`)); // Scroll down and up await context.page.evaluate(_ => { window.scrollBy(0, window.innerHeight); }); await sleep(100); await context.page.evaluate(_ => { window.scrollBy(0, -window.innerHeight); }); assert.not.ok(responseURLs.includes(`${server}/4.html`)); // Scroll down and test await context.page.evaluate(_ => { window.scrollBy(0, window.innerHeight); }); await sleep(200); assert.ok(responseURLs.includes(`${server}/4.html`)); }); mainSuite('should consider threshold option before prefetching (UMD)', async context => { const responseURLs = []; context.page.on('response', resp => { responseURLs.push(resp.url()); }); await context.page.goto(`${server}/test-threshold.html`); await context.page.setViewport({ width: 1000, height: 800, }); await sleep(); assert.instance(responseURLs, Array); assert.ok(responseURLs.includes(`${server}/1.html`)); assert.ok(responseURLs.includes(`${server}/2.html`)); await context.page.evaluate(_ => { window.scrollBy(0, window.innerHeight); }); await sleep(400); assert.ok(responseURLs.includes(`${server}/3.html`)); assert.ok(responseURLs.includes(`${server}/4.html`)); }); mainSuite.run(); ================================================ FILE: translations/zh-cn/README.md ================================================


npm gzip size ci

# quicklink > 可以在空闲时间预获取页面可视区域(以下简称视区)内的链接,加快后续加载速度。 ## 工作原理 Quicklink 通过以下方式加快后续页面的加载速度: - **检测视区中的链接**(使用 [Intersection Observer](https://developer.mozilla.org/zh-CN/docs/Web/API/Intersection_Observer_API))。 - **等待浏览器空闲**(使用 [requestIdleCallback](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback))。 - **确认用户并未处于慢速连接**(使用 `navigator.connection.effectiveType`)或启用省流模式(使用 `navigator.connection.saveData`)。 - **预获取视区内的 URL**(使用 [``](https://www.w3.org/TR/resource-hints/#prefetch) 或 XHR)。可根据请求优先级进行控制(若支持 fetch() 可进行切换)。 ## 开发原因 该项目旨在为网站提供一套解决方案,预获取处于用户视区中的链接,同时保持极小的体积(**minify/gzip 后 <1KB**)。 ## 安装方法 [node](http://nodejs.org) 或 [npm](https://npmjs.com) 用户: ```sh npm install --save quicklink ``` 或者从 [unpkg.com/quicklink](https://unpkg.com/quicklink) 获取 `quicklink`。 ## 用法 初始化后,`quicklink` 将自动在闲时预获取视区内的链接 URL。 快速上手: ```html ``` 举个例子,你可以在 `load` 方法触发之后进行初始化: ```html ``` 或者导入 ES 模块: ```js import quicklink from "dist/quicklink.mjs"; quicklink(); ``` 以上方法适用于多页网站。单页应用可以搭配 router 使用 quicklink: - 进入新路由地址后,调用 `quicklink()`。 - 针对特定 DOM 元素/组件调用 `quicklink()`。 - 调用 `quicklink({urls:[...]})`,传入自定义 URL 集合进行预获取。 ## API `quicklink` 接受带有以下参数的 option 对象(可选): - `el`:指定需要预获取的 DOM 元素视区。 - `urls`:预获取的静态 URL 数组(若此参数非空,则不会检测视区中 `document` 或 DOM 元素的链接)。 - `timeout`:整型数,为 requestIdleCallback 设置超时。浏览器必须在此之前进行预获取(以毫秒为单位), 默认取 2 秒。 - `timeoutFn`:指定超时处理函数。默认为 requestIdleCallback。也可以替换为 [networkIdleCallback](https://github.com/pastelsky/network-idle-callback)(详见 demo)等自定义函数。 - `priority`:布尔值,指定 fetch 的优先级。默认为 `false`。若配置为 `true` 将会尝试使用 `fetch()` API(而非 rel=prefetch)。 - `origins`: 静态字符串数组,包含允许进行预获取操作的 URL 主机名。默认为同域请求源,可阻止跨域请求。 - `ignores`: RegExp(正则表达式),Function(函数)或者 Array(数组),用于进一步确定某 URL 是否可被预获取。会在匹配请求源之后执行。 待探索: - 支持资源扩展名检测及使用 [rel=preload](https://w3c.github.io/preload/) 获取高优资源。 - 使用 [Priority Hints](https://github.com/WICG/priority-hints) 进行重要性提示。 ## Polyfills `quicklink`: - [requestIdleCallback](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback) 的一个非常小的回退。 - 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. ## 方法 ### 为预获取操作自定义超时时间 默认超时时间为 2 秒(通过 `requestIdleCallback`),这里我们重写为 4 秒: ```js quicklink({ timeout: 4000, }); ``` ### 设置用于检测链接的 DOM 元素 默认值为 `document`。 ```js const elem = document.getElementById('carousel'); quicklink({ el: elem, }); ``` ### 自定义预获取 URL 数组 如果你想指定用于预获取的静态 URL 列表,而不是视区内的链接,你可以使用自定义 URL。 ```js quicklink({ urls: ['2.html', '3.html', '4.js'], }); ``` ### 为预获取设置请求优先级 默认为低优先级(`rel=prefetch` 或 XHR)。对于高优先级(`priority: true`)的操作,尝试使用 `fetch()` 或退阶使用 XHR。 ```js quicklink({ priority: true }); ``` ### 自定义受允许请求源列表 指定可被预获取的主机名列表,默认情况下仅允许同源主机名。 > **划重点**:你还得加上自己的主机名! ```js quicklink({ origins: [ // 添加我自己的 'my-website.com', 'api.my-website.com', // 添加第三方的 'other-website.com', 'example.com', // ... ], }); ``` ### 允许所有源 允许所有跨域请求。 > **注意**:可能会导致 [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) 问题! ```js quicklink({ origins: true, // 或者 origins: [], }); ``` ### 自定义忽略模式 以下过滤器会在匹配 `origin` 之后执行。Ignores 在避免大型文件下载或 DOM 属性动态响应时十分有用! ```js // 默认启用同源限制。 // // 这个示例会忽略所有对以下模式的请求: // - 所有 "/api/*" 路径名 // - 所有 ".zip" 扩展名 // - 所有带有 noprefetch 扩展名的 标签 // quicklink({ ignores: [ /\/api\/?/, uri => uri.includes('.zip'), (uri, elem) => elem.hasAttribute('noprefetch'), ], }); ``` 也许你还想忽略那些包含 URL fragment 的 URL(比如 `index.html#top`),不对它们进行预获取。那么对于在页面中使用了锚点标题,或者在单页应用设置了 URL fragment 的情况,这个功能可以避免对类似的 URL 进行预获取。 使用 `ignores` 来实现: ```js quicklink({ ignores: [ uri => uri.includes('#'), // 或者使用正则表达式: /#(.+)/ // 或者使用元素匹配: (uri, elem) => !!elem.hash ], }); ``` ## 浏览器支持 quicklink 提供的预获取是[渐进增强](https://www.smashingmagazine.com/2009/04/progressive-enhancement-what-it-is-and-how-to-use-it/)的,跨浏览器支持如下: - 不使用 polyfills:Chrome,Firefox,Edge,Opera,Android Browser,Samsung Internet 支持。 - 使用 [Intersection Observer polyfill](https://github.com/GoogleChromeLabs/intersection-observer)(gzipped/minified 后大约 6KB):Safari,IE9+ 支持。 部分功能支持分层实现: - 用于检查用户是否处于低速联网状态(通过`navigator.connection.effectiveType`)的 [Network Information API](https://wicg.github.io/netinfo/) 仅适用于 [Chrome 61+ 和 Opera 57+](https://caniuse.com/#feat=netinfo)。 - 如果 `{priority:true}` 和 [fetch()](https://fetch.spec.whatwg.org/) 均不可用,则将使用 XHR。 ## 直接使用预获取器 `quicklink` 包含一个预获取器,可以单独导入其他项目中。方法是先将 `quicklink` 作为依赖项安装,然后按如下方式使用: ```html ``` ## Demo 这个 [WebPageTest demo](https://www.webpagetest.org/video/view.php?id=181212_4c294265117680f2636676721cc886613fe2eede&data=1) 演示了 quicklink 的预获取功能,它将页面加载性能提高了 4 秒! [这个 Youtube 视频](https://youtu.be/rQ75YEbJicw) 对使用预获取之前和之后进行了对比。 为了做演示,我们在 Firebase 上部署了一个 [Google Blog](https://blog.google/),接着部署了另一个在主页添加了 quicklink 的版本,测试从主页导航到一个自动预获取的文章所用时间。结果表明预获取版本加载速度更快。 请注意:这绝不是对这项技术优缺点的详尽测试,只是演示了该方法可能带来的潜在改进。你自己的实现可能不尽相同。 ## 相关项目 - 在用 [Gatsby](https://gatsbyjs.org/) 吗? 现在可以免费下载它了。它使用 `Intersection Observer` 预获取视图中的所有链接,本项目灵感亦来源于此。 - 想要更加数据驱动的方案吗? 参见 [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/) 的插件。 ## 许可证 本项目已获得 Apache-2.0 许可。