Full Code of GoogleChrome/web-vitals for AI

main 8e4e68944724 cached
91 files
430.7 KB
108.7k tokens
82 symbols
1 requests
Download .txt
Showing preview only (455K chars total). Download the full file or copy to clipboard to get everything.
Repository: GoogleChrome/web-vitals
Branch: main
Commit: 8e4e68944724
Files: 91
Total size: 430.7 KB

Directory structure:
gitextract_ttgfkz8n/

├── .editorconfig
├── .github/
│   └── workflows/
│       ├── lint.yml
│       └── tests.yml
├── .gitignore
├── .husky/
│   ├── .gitignore
│   └── pre-commit
├── .nvmrc
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── attribution.d.ts
├── attribution.js
├── docs/
│   ├── upgrading-to-v4.md
│   └── upgrading-to-v5.md
├── eslint.config.js
├── package.json
├── rollup.config.js
├── src/
│   ├── attribution/
│   │   ├── index.ts
│   │   ├── onCLS.ts
│   │   ├── onFCP.ts
│   │   ├── onINP.ts
│   │   ├── onLCP.ts
│   │   └── onTTFB.ts
│   ├── index.ts
│   ├── lib/
│   │   ├── InteractionManager.ts
│   │   ├── LCPEntryManager.ts
│   │   ├── LayoutShiftManager.ts
│   │   ├── bfcache.ts
│   │   ├── bindReporter.ts
│   │   ├── doubleRAF.ts
│   │   ├── generateUniqueID.ts
│   │   ├── getActivationStart.ts
│   │   ├── getLoadState.ts
│   │   ├── getNavigationEntry.ts
│   │   ├── getSelector.ts
│   │   ├── getVisibilityWatcher.ts
│   │   ├── initMetric.ts
│   │   ├── initUnique.ts
│   │   ├── observe.ts
│   │   ├── polyfills/
│   │   │   ├── getFirstHiddenTimePolyfill.ts
│   │   │   └── interactionCountPolyfill.ts
│   │   ├── runOnce.ts
│   │   ├── whenActivated.ts
│   │   └── whenIdleOrHidden.ts
│   ├── onCLS.ts
│   ├── onFCP.ts
│   ├── onINP.ts
│   ├── onLCP.ts
│   ├── onTTFB.ts
│   ├── types/
│   │   ├── base.ts
│   │   ├── cls.ts
│   │   ├── fcp.ts
│   │   ├── inp.ts
│   │   ├── lcp.ts
│   │   └── ttfb.ts
│   └── types.ts
├── test/
│   ├── css/
│   │   └── styles.css
│   ├── e2e/
│   │   ├── onCLS-test.js
│   │   ├── onFCP-test.js
│   │   ├── onINP-test.js
│   │   ├── onLCP-test.js
│   │   └── onTTFB-test.js
│   ├── script/
│   │   ├── async.js
│   │   └── defer.js
│   ├── server.js
│   ├── tsconfig.json
│   ├── unit/
│   │   ├── attribution-test.js
│   │   ├── bindReporter-test.js
│   │   └── index-test.js
│   ├── utils/
│   │   ├── assertIsCloseTo.js
│   │   ├── beacons.js
│   │   ├── browserSupportsEntry.js
│   │   ├── domReadyState.js
│   │   ├── firstContentfulPaint.js
│   │   ├── imagesPainted.js
│   │   ├── navigateTo.js
│   │   ├── nextFrame.js
│   │   ├── stubForwardBack.js
│   │   ├── stubVisibilityChange.js
│   │   ├── waitUntilIdle.js
│   │   └── webVitalsLoaded.js
│   └── views/
│       ├── cls.njk
│       ├── fcp.njk
│       ├── inp.njk
│       ├── layout.njk
│       ├── lcp.njk
│       └── ttfb.njk
├── tsconfig.json
└── wdio.conf.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
# editorconfig.org
# For Visual Studio code use this extension to enforce below rules:
# https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig
# Other IDEs maye have built in editorconfig support, or their own extensions
#
# The similar .ecrc file is used by an editorconfig GitHub action to catch those
# that don't use this.
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true


================================================
FILE: .github/workflows/lint.yml
================================================
name: Lint Code Base
permissions:
  contents: read
on:
  pull_request:
  push:
    branches:
      - main
  workflow_dispatch:
jobs:
  lint:
    name: Lint Code Base
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v6
      - name: NPM install
        run: npm install
      - name: Run Prettier
        run: npm run format:check
      - name: Run ESlint
        run: npm run lint


================================================
FILE: .github/workflows/tests.yml
================================================
name: Run tests
permissions:
  contents: read
on:
  pull_request:
  push:
    branches:
      - main
  workflow_dispatch:
jobs:
  unit-tests:
    name: Run unit tests
    # Doesn't require anything special so let's use ubuntu as more available
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v6
      - name: NPM install
        run: npm install
      - name: Build
        run: npm run build
      - name: Run unit tests
        run: npm run test:unit
  chrome-tests:
    name: Run Chrome e2e tests
    # Runs best on macos for CI as linux requires extra chrome flags
    runs-on: macos-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v6
      - name: NPM install
        run: npm install
      - name: Build
        run: npm run build
      - name: Run server
        run: npm run test:server &
      - name: Run e2e tests for chrome
        run: npm run test:e2e -- --browsers=chrome
  firefox-tests:
    name: Run Firefox e2e tests
    # Runs best on macos for CI as linux requires extra setup
    runs-on: macos-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v6
      - name: NPM install
        run: npm install
      - name: Build
        run: npm run build
      - name: Run server
        run: npm run test:server &
      - name: Run e2e tests for firefox
        run: npm run test:e2e -- --browsers=firefox
  safari-tests:
    name: Run Safari e2e tests
    # Requires macos
    runs-on: macos-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v6
      - name: NPM install
        run: npm install
      - name: Build
        run: npm run build
      - name: Run server
        run: npm run test:server &
      - name: Run e2e tests for safari
        run: npm run test:e2e -- --browsers=safari


================================================
FILE: .gitignore
================================================
.DS_Store
.vscode
node_modules

# Log files
*.log

# Generated TypeScript files and build data
tsconfig.tsbuildinfo

# Dist files
dist


================================================
FILE: .husky/.gitignore
================================================
_


================================================
FILE: .husky/pre-commit
================================================
lint-staged

grep -r "\.only(" test/e2e \
  && echo "ERROR: found .only() use in test" && exit 1

grep -r "browser\.debug(" test/e2e \
  && echo "ERROR: found browser.debug() use in test" && exit 1

exit 0


================================================
FILE: .nvmrc
================================================
lts/Hydrogen


================================================
FILE: CHANGELOG.md
================================================
# Changelog

### v5.1.0 (2025-07-31)

- Register `visibility-change` early ([#637](https://github.com/GoogleChrome/web-vitals/pull/637))
- Only finalize LCP on user events (`isTrusted=true`) ([#635](https://github.com/GoogleChrome/web-vitals/pull/635))
- Fallback to default `getSelector` if custom function is null or undefined ([#634](https://github.com/GoogleChrome/web-vitals/pull/634))

### v5.0.3 (2025-06-11)

- Remove visibilitychange event listeners when no longer required ([#627](https://github.com/GoogleChrome/web-vitals/pull/627))

### v5.0.2 (2025-05-29)

- Handle layout shifts with no sources ([#623](https://github.com/GoogleChrome/web-vitals/pull/623))

### v5.0.1 (2025-05-13)

- Fix missing FCP and LCP for prerendered pages ([#621](https://github.com/GoogleChrome/web-vitals/pull/621))

### v5.0.0 (2025-05-07)

[!NOTE]
See the [upgrading to v5](/docs/upgrading-to-v5.md) guide for a complete list of all API changes in version 5.

- **[BREAKING]** Remove the deprecated `onFID()` function ([#519](https://github.com/GoogleChrome/web-vitals/pull/519))
- **[BREAKING]** Change browser support policy to Baseline Widely available ([#525](https://github.com/GoogleChrome/web-vitals/pull/525))
- **[BREAKING]** Sort the classes that appear in attribution selectors to reduce cardinality ([#518](https://github.com/GoogleChrome/web-vitals/pull/518))
- Extend INP attribution with extra LoAF information: longest script and buckets ([#592](https://github.com/GoogleChrome/web-vitals/pull/592))
- Add support for generating custom targets in the attribution build ([#585](https://github.com/GoogleChrome/web-vitals/pull/585))
- Support multiple calls to `onINP()` with different config options ([#583](https://github.com/GoogleChrome/web-vitals/pull/583))
- Use visibility-state performance entries ([#612](https://github.com/GoogleChrome/web-vitals/pull/612))
- Ensure idle callbacks don't run twice ([#541](https://github.com/GoogleChrome/web-vitals/pull/541)) and ([#548](https://github.com/GoogleChrome/web-vitals/pull/548))
- Cap `nextPaintTime` at `processingStart` ([#540](https://github.com/GoogleChrome/web-vitals/pull/540)) and ([#546](https://github.com/GoogleChrome/web-vitals/pull/546))
- Cap INP breakdowns to INP duration ([#528](https://github.com/GoogleChrome/web-vitals/pull/528))
- Cap LCP load duration to LCP time ([#527](https://github.com/GoogleChrome/web-vitals/pull/527))

### v4.2.4 (2024-10-22)

- Fix memory leak in registering new event listeners on every keydown and click ([#554](https://github.com/GoogleChrome/web-vitals/pull/554))

### v4.2.3 (2024-08-06)

- Fix missing LoAF entries in INP attribution ([#512](https://github.com/GoogleChrome/web-vitals/pull/512))

### v4.2.2 (2024-07-17)

- Fix interaction count after bfcache restore ([#505](https://github.com/GoogleChrome/web-vitals/pull/505))

### v4.2.1 (2024-06-30)

- Fix compatibility issues with TypeScript v5.5 ([#497](https://github.com/GoogleChrome/web-vitals/pull/497))

### v4.2.0 (2024-06-20)

- Refactor INP attribution code to fix errors on Windows 10 ([#495](https://github.com/GoogleChrome/web-vitals/pull/495))

### v4.1.1 (2024-06-10)

- Fix pending LoAF cleanup logic ([#493](https://github.com/GoogleChrome/web-vitals/pull/493))

### v4.1.0 (2024-06-06)

- Move the support check to the top of the onINP() function ([#490](https://github.com/GoogleChrome/web-vitals/pull/490))
- Fix missing LoAF attribution when entries are dispatched before event entries ([#487](https://github.com/GoogleChrome/web-vitals/pull/487))

### v4.0.1 (2024-05-21)

- Add the `ReportCallback` type back but deprecate it ([#483](https://github.com/GoogleChrome/web-vitals/pull/483))

### v4.0.0 (2024-05-13)

[!NOTE]
See the [upgrading to v4](/docs/upgrading-to-v4.md) guide for a complete list of all API changes in version 4.

- **[BREAKING]** Update types to support more generic usage ([#471](https://github.com/GoogleChrome/web-vitals/pull/471))
- **[BREAKING]** Split `waitingDuration` to make it easier to understand redirect delays ([#458](https://github.com/GoogleChrome/web-vitals/pull/458))
- **[BREAKING]** Rename `TTFBAttribution` fields from `*Time` to `*Duration` ([#453](https://github.com/GoogleChrome/web-vitals/pull/453))
- **[BREAKING]** Rename `resourceLoadTime` to `resourceLoadDuration` in LCP attribution ([#450](https://github.com/GoogleChrome/web-vitals/pull/450))
- **[BREAKING]** Add INP breakdown timings and LoAF attribution ([#442](https://github.com/GoogleChrome/web-vitals/pull/442))
- **[BREAKING]** Deprecate `onFID()` and remove previously deprecated APIs ([#435](https://github.com/GoogleChrome/web-vitals/pull/435))
- Expose the target element in INP attribution ([#479](https://github.com/GoogleChrome/web-vitals/pull/479))
- Save INP target after interactions to reduce null values when removed from the DOM ([#477](https://github.com/GoogleChrome/web-vitals/pull/477))
- Cap TTFB in attribution ([#440](https://github.com/GoogleChrome/web-vitals/pull/440))
- Fix `reportAllChanges` behavior for LCP when library is loaded late ([#468](https://github.com/GoogleChrome/web-vitals/pull/468))

### v3.5.2 (2024-01-25)

- Pick the first non-null `target` for INP attribution ([#421](https://github.com/GoogleChrome/web-vitals/pull/421))

### v3.5.1 (2023-12-27)

- Add extra guard for `PerformanceEventTiming` not existing ([#403](https://github.com/GoogleChrome/web-vitals/pull/403))

### v3.5.0 (2023-09-28)

- Run `onLCP` callback in separate task ([#386](https://github.com/GoogleChrome/web-vitals/pull/386))
- Fix INP durationThreshold bug when set to 0 ([#372](https://github.com/GoogleChrome/web-vitals/pull/372))
- Prevent FID entries being emitted as INP for non-supporting browsers ([#368](https://github.com/GoogleChrome/web-vitals/pull/368))

### v3.4.0 (2023-07-11)

- Make `bindReporter` generic over metric type ([#359](https://github.com/GoogleChrome/web-vitals/pull/359))
- Update INP status in README ([#362](https://github.com/GoogleChrome/web-vitals/pull/362))
- Fix Metric types for better TypeScript support ([#356](https://github.com/GoogleChrome/web-vitals/pull/356))
- Fix selector for SVGs for attribution build ([#354](https://github.com/GoogleChrome/web-vitals/pull/354))

### v3.3.2 (2023-05-29)

- Fix attribution types ([#348](https://github.com/GoogleChrome/web-vitals/pull/348))
- Safe access navigation entry type ([#290](https://github.com/GoogleChrome/web-vitals/pull/290))

### v3.3.1 (2023-04-04)

- Export metric rating thresholds in attribution build as well.

### v3.3.0 (2023-03-09)

- Export metric rating thresholds, add explicit `MetricRatingThresholds` type ([#323](https://github.com/GoogleChrome/web-vitals/pull/323))
- Trim classname selector ([#328](https://github.com/GoogleChrome/web-vitals/pull/328))
- Add link to CrUX versus RUM blog post ([#327](https://github.com/GoogleChrome/web-vitals/pull/327))
- Prevent LCP being reported for hidden prerendered pages ([#326](https://github.com/GoogleChrome/web-vitals/pull/326))
- Add Server Timing information to docs ([#324](https://github.com/GoogleChrome/web-vitals/pull/324))
- Fix link in `onINP()` thresholds comment ([#318](https://github.com/GoogleChrome/web-vitals/pull/318))
- Update web.dev link for `onINP()` ([#307](https://github.com/GoogleChrome/web-vitals/pull/307))
- Add a note about when to load the library ([#305](https://github.com/GoogleChrome/web-vitals/pull/305))

### v3.2.0

- Version number skipped

### v3.1.1 (2023-01-10)

- Defer CLS logic until after `onFCP()` callback ([#297](https://github.com/GoogleChrome/web-vitals/pull/297))

### v3.1.0 (2022-11-15)

- Add support for `'restore'` as a `navigationType` ([#284](https://github.com/GoogleChrome/web-vitals/pull/284))
- Report initial CLS value when `reportAllChanges` is true ([#283](https://github.com/GoogleChrome/web-vitals/pull/283))
- Defer all observers until after activation ([#282](https://github.com/GoogleChrome/web-vitals/pull/282))
- Ignore TTFB for loads where responseStart is zero ([#281](https://github.com/GoogleChrome/web-vitals/pull/281))
- Defer execution of observer callbacks ([#278](https://github.com/GoogleChrome/web-vitals/pull/278))

### v3.0.4 (2022-10-18)

- Clamp LCP and FCP to 0 for prerendered pages ([#270](https://github.com/GoogleChrome/web-vitals/pull/270))

### v3.0.3 (2022-10-04)

- Ensure `attribution` object is always present in attribution build ([#265](https://github.com/GoogleChrome/web-vitals/pull/265))

### v3.0.2 (2022-09-14)

- Set an explicit unpkg dist file ([#261](https://github.com/GoogleChrome/web-vitals/pull/261))

### v3.0.1 (2022-08-31)

- Use the cjs extension for all UMD builds ([#257](https://github.com/GoogleChrome/web-vitals/pull/257))

### v3.0.0 (2022-08-24)

- **[BREAKING]** Add a config object param to all metric functions ([#225](https://github.com/GoogleChrome/web-vitals/pull/225))
- **[BREAKING]** Report TTFB after a bfcache restore ([#220](https://github.com/GoogleChrome/web-vitals/pull/220))
- **[BREAKING]** Only include last LCP entry in metric entries ([#218](https://github.com/GoogleChrome/web-vitals/pull/218))
- Update the metric ID prefix for v3 ([#251](https://github.com/GoogleChrome/web-vitals/pull/251))
- Move the Navigation Timing API polyfill to the base+polyfill build ([#248](https://github.com/GoogleChrome/web-vitals/pull/248))
- Add a metric rating property ([#246](https://github.com/GoogleChrome/web-vitals/pull/246))
- Add deprecation notices for base+polyfill builds ([#242](https://github.com/GoogleChrome/web-vitals/pull/242))
- Add a new attribution build for debugging issues in the field ([#237](https://github.com/GoogleChrome/web-vitals/pull/237), [#244](https://github.com/GoogleChrome/web-vitals/pull/244))
- Add support for prerendered pages ([#233](https://github.com/GoogleChrome/web-vitals/pull/233))
- Rename the `ReportHandler` type to `ReportCallback`, with alias for back-compat ([#225](https://github.com/GoogleChrome/web-vitals/pull/225), [#227](https://github.com/GoogleChrome/web-vitals/pull/227))
- Add support for the new INP metric ([#221](https://github.com/GoogleChrome/web-vitals/pull/221), [#232](https://github.com/GoogleChrome/web-vitals/pull/232))
- Rename `getXXX()` functions to `onXXX()` ([#222](https://github.com/GoogleChrome/web-vitals/pull/222))
- Add a `navigationType` property to the Metric object ([#219](https://github.com/GoogleChrome/web-vitals/pull/219))

### v2.1.4 (2022-01-20)

- Prevent TTFB from reporting after bfcache restore ([#201](https://github.com/GoogleChrome/web-vitals/pull/201))

### v2.1.3 (2022-01-06)

- Only call report if LCP occurs before first hidden ([#197](https://github.com/GoogleChrome/web-vitals/pull/197))

### v2.1.2 (2021-10-11)

- Ensure reported TTFB values are less than the current page time ([#187](https://github.com/GoogleChrome/web-vitals/pull/187))

### v2.1.1 (2021-10-06)

- Add feature detects to support Opera mini in extreme data saver mode ([#186](https://github.com/GoogleChrome/web-vitals/pull/186))

### v2.1.0 (2021-07-01)

- Add batch reporting support and guidance ([#166](https://github.com/GoogleChrome/web-vitals/pull/166))

### v2.0.1 (2021-06-02)

- Detect getEntriesByName support before calling ([#158](https://github.com/GoogleChrome/web-vitals/pull/158))

### v2.0.0 (2021-06-01)

- **[BREAKING]** Update CLS to max session window 5s cap 1s gap ([#148](https://github.com/GoogleChrome/web-vitals/pull/148))
- Ensure CLS is only reported if page was visible ([#149](https://github.com/GoogleChrome/web-vitals/pull/149))
- Only report CLS when FCP is reported ([#154](https://github.com/GoogleChrome/web-vitals/pull/154))
- Update the unique ID version prefix ([#157](https://github.com/GoogleChrome/web-vitals/pull/157))

### v1.1.2 (2021-05-05)

- Ignore negative TTFB values in Firefox ([#147](https://github.com/GoogleChrome/web-vitals/pull/147))
- Add workaround for Safari FCP bug ([#145](https://github.com/GoogleChrome/web-vitals/pull/145))
- Add more extensive FID feature detect ([#143](https://github.com/GoogleChrome/web-vitals/pull/143))

### v1.1.1 (2021-03-13)

- Remove use of legacy API to detect Firefox ([#128](https://github.com/GoogleChrome/web-vitals/pull/128))

### v1.1.0 (2021-01-13)

- Fix incorrect UMD config for base+polyfill script ([#117](https://github.com/GoogleChrome/web-vitals/pull/117))
- Fix missing getter in polyfill ([#114](https://github.com/GoogleChrome/web-vitals/pull/114))
- Add support for Set in place of WeakSet for IE11 compat ([#110](https://github.com/GoogleChrome/web-vitals/pull/110))

### v1.0.1 (2020-11-16)

- Fix missing `typings` declaration ([#90](https://github.com/GoogleChrome/web-vitals/pull/90))

### v1.0.0 (2020-11-16)

- **[BREAKING]** Add support for reporting metrics on back/forward cache restore ([#87](https://github.com/GoogleChrome/web-vitals/pull/87))
- **[BREAKING]** Remove the `isFinal` flag from the Metric interface ([#86](https://github.com/GoogleChrome/web-vitals/pull/86))
- Remove the scroll listener to stop LCP observing ([#85](https://github.com/GoogleChrome/web-vitals/pull/85))

### v0.2.4 (2020-07-23)

- Remove the unload listener ([#68](https://github.com/GoogleChrome/web-vitals/pull/68))

### v0.2.3 (2020-06-26)

- Ensure reports only occur if a PO was created ([#58](https://github.com/GoogleChrome/web-vitals/pull/58))

### v0.2.2 (2020-05-12)

- Remove package `type` field ([#35](https://github.com/GoogleChrome/web-vitals/pull/35))

### v0.2.1 (2020-05-06)

- Ensure all modules are pure modules ([#23](https://github.com/GoogleChrome/web-vitals/pull/23))
- Ensure proper TypeScript exports and config ([#22](https://github.com/GoogleChrome/web-vitals/pull/22))

### v0.2.0 (2020-05-03)

- Initial public release

### v0.1.0 (2020-04-24)

- Initial pre-release


================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Google Open Source Community Guidelines

At Google, we recognize and celebrate the creativity and collaboration of open
source contributors and the diversity of skills, experiences, cultures, and
opinions they bring to the projects and communities they participate in.

Every one of Google's open source projects and communities are inclusive
environments, based on treating all individuals respectfully, regardless of
gender identity and expression, sexual orientation, disabilities,
neurodiversity, physical appearance, body size, ethnicity, nationality, race,
age, religion, or similar personal characteristic.

We value diverse opinions, but we value respectful behavior more.

Respectful behavior includes:

- Being considerate, kind, constructive, and helpful.
- Not engaging in demeaning, discriminatory, harassing, hateful, sexualized, or
  physically threatening behavior, speech, and imagery.
- Not engaging in unwanted physical contact.

Some Google open source projects [may adopt][] an explicit project code of
conduct, which may have additional detailed expectations for participants. Most
of those projects will use our [modified Contributor Covenant][].

[may adopt]: https://opensource.google/docs/releasing/preparing/#conduct
[modified contributor covenant]: https://opensource.google/docs/releasing/template/CODE_OF_CONDUCT/

## Resolve peacefully

We do not believe that all conflict is necessarily bad; healthy debate and
disagreement often yields positive results. However, it is never okay to be
disrespectful.

If you see someone behaving disrespectfully, you are encouraged to address the
behavior directly with those involved. Many issues can be resolved quickly and
easily, and this gives people more control over the outcome of their dispute.
If you are unable to resolve the matter for any reason, or if the behavior is
threatening or harassing, report it. We are dedicated to providing an
environment where participants feel welcome and safe.

## Reporting problems

Some Google open source projects may adopt a project-specific code of conduct.
In those cases, a Google employee will be identified as the Project Steward,
who will receive and handle reports of code of conduct violations. In the event
that a project hasn’t identified a Project Steward, you can report problems by
emailing opensource@google.com.

We will investigate every complaint, but you may not receive a direct response.
We will use our discretion in determining when and how to follow up on reported
incidents, which may range from not taking action to permanent expulsion from
the project and project-sponsored spaces. We will notify the accused of the
report and provide them an opportunity to discuss it before any action is
taken. The identity of the reporter will be omitted from the details of the
report supplied to the accused. In potentially harmful situations, such as
ongoing harassment or threats to anyone's safety, we may take action without
notice.

_This document was adapted from the [IndieWeb Code of Conduct][] and can also
be found at <https://opensource.google/conduct/>._

[indieweb code of conduct]: https://indieweb.org/code-of-conduct


================================================
FILE: CONTRIBUTING.md
================================================
# How to Contribute

We'd love to accept your patches and contributions to this project. There are
just a few small guidelines you need to follow.

## Contributor License Agreement

Contributions to this project must be accompanied by a Contributor License
Agreement. You (or your employer) retain the copyright to your contribution;
this simply gives us permission to use and redistribute your contributions as
part of the project. Head over to <https://cla.developers.google.com/> to see
your current agreements on file or to sign a new one.

You generally only need to submit a CLA once, so if you've already submitted one
(even if it was for a different project), you probably don't need to do it
again.

## Code reviews

All submissions, including submissions by project members, require review. We
use GitHub pull requests for this purpose. Consult
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
information on using pull requests.

## Testing

To test the full suite run `npm run test`.

To test a subset of browsers or metrics, run the following in separate terminals:

- `npm run watch`
- `npm run test:server`
- `npm run test:e2e -- --browsers=chrome --metrics=TTFB`

The last command can be replaced as you see fit and include comma, separated values. For example:

- `npm run test:e2e -- --browsers=chrome,firefox --metrics=TTFB,LCP`

To run an individual test, change `it('test name')` to `it.only('test name')`.

You can also add `await browser.debug()` lines to the individual test files to pause execution, and press `CTRL+C` in the command line to continue the tests.

See the https://webdriver.io/ for more information.

## Community Guidelines

This project follows [Google's Open Source Community
Guidelines](https://opensource.google/conduct/).


================================================
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 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.


================================================
FILE: README.md
================================================
# `web-vitals`

- [Overview](#overview)
- [Install and load the library](#installation)
  - [From npm](#import-web-vitals-from-npm)
  - [From a CDN](#load-web-vitals-from-a-cdn)
- [Usage](#usage)
  - [Basic usage](#basic-usage)
  - [Report the value on every change](#report-the-value-on-every-change)
  - [Report only the delta of changes](#report-only-the-delta-of-changes)
  - [Send the results to an analytics endpoint](#send-the-results-to-an-analytics-endpoint)
  - [Send the results to Google Analytics](#send-the-results-to-google-analytics)
  - [Send the results to Google Tag Manager](#send-the-results-to-google-tag-manager)
  - [Send attribution data](#send-attribution-data)
  - [Batch multiple reports together](#batch-multiple-reports-together)
- [Build options](#build-options)
  - [Which build is right for you?](#which-build-is-right-for-you)
- [API](#api)
  - [Types](#types)
  - [Functions](#functions)
  - [Rating Thresholds](#rating-thresholds)
  - [Attribution](#attribution)
- [Browser Support](#browser-support)
- [Limitations](#limitations)
- [Development](#development)
- [Integrations](#integrations)
- [License](#license)

## Overview

The `web-vitals` library is a tiny (~2K, brotli'd), modular library for measuring all the [Web Vitals](https://web.dev/articles/vitals) metrics on real users, in a way that accurately matches how they're measured by Chrome and reported to other Google tools (e.g. [Chrome User Experience Report](https://developers.google.com/web/tools/chrome-user-experience-report), [Page Speed Insights](https://developers.google.com/speed/pagespeed/insights/), [Search Console's Speed Report](https://webmasters.googleblog.com/2019/11/search-console-speed-report.html)).

The library supports all of the [Core Web Vitals](https://web.dev/articles/vitals#core_web_vitals) as well as a number of other metrics that are useful in diagnosing [real-user](https://web.dev/articles/user-centric-performance-metrics) performance issues.

### Core Web Vitals

- [Cumulative Layout Shift (CLS)](https://web.dev/articles/cls)
- [Interaction to Next Paint (INP)](https://web.dev/articles/inp)
- [Largest Contentful Paint (LCP)](https://web.dev/articles/lcp)

### Other metrics

- [First Contentful Paint (FCP)](https://web.dev/articles/fcp)
- [Time to First Byte (TTFB)](https://web.dev/articles/ttfb)

<a name="installation"></a>
<a name="load-the-library"></a>

## Install and load the library

<a name="import-web-vitals-from-npm"></a>

The `web-vitals` library uses the `buffered` flag for [PerformanceObserver](https://developer.mozilla.org/docs/Web/API/PerformanceObserver/observe), allowing it to access performance entries that occurred before the library was loaded.

This means you do not need to load this library early in order to get accurate performance data. In general, this library should be deferred until after other user-impacting code has loaded.

### From npm

You can install this library from npm by running:

```sh
npm install web-vitals
```

> [!NOTE]
> If you're not using npm, you can still load `web-vitals` via `<script>` tags from a CDN like [unpkg.com](https://unpkg.com). See the [load `web-vitals` from a CDN](#load-web-vitals-from-a-cdn) usage example below for details.

There are a few different builds of the `web-vitals` library, and how you load the library depends on which build you want to use.

For details on the difference between the builds, see <a href="#which-build-is-right-for-you">which build is right for you</a>.

**1. The "standard" build**

To load the "standard" build, import modules from the `web-vitals` package in your application code (as you would with any npm package and node-based build tool):

```js
import {onLCP, onINP, onCLS} from 'web-vitals';
```

<a name="attribution-build"></a>

**2. The "attribution" build**

Measuring the Web Vitals scores for your real users is a great first step toward optimizing the user experience. But if your scores aren't _good_, the next step is to understand why they're not good and work to improve them.

The "attribution" build helps you do that by including additional diagnostic information with each metric to help you identify the root cause of poor performance as well as prioritize the most important things to fix.

The "attribution" build is slightly larger than the "standard" build (by about 1.5K, brotli'd), so while the code size is still small, it's only recommended if you're actually using these features.

To load the "attribution" build, change any `import` statements that reference `web-vitals` to `web-vitals/attribution`:

```diff
import {onLCP, onINP, onCLS} from 'web-vitals';
import {onLCP, onINP, onCLS} from 'web-vitals/attribution';
```

Usage for each of the imported function is identical to the standard build, but when importing from the attribution build, the [metric](#metric) objects will contain an additional [`attribution`](#attribution) property.

See [Send attribution data](#send-attribution-data) for usage examples, and the [`attribution` reference](#attribution) for details on what values are added for each metric.

<a name="load-web-vitals-from-a-cdn"></a>

### From a CDN

The recommended way to use the `web-vitals` package is to install it from npm and integrate it into your build process. However, if you're not using npm, it's still possible to use `web-vitals` by requesting it from a CDN that serves npm package files.

The following examples show how to load `web-vitals` from [unpkg.com](https://unpkg.com/browse/web-vitals/). It is also possible to load this from [jsDelivr](https://www.jsdelivr.com/package/npm/web-vitals), and [cdnjs](https://cdnjs.com/libraries/web-vitals).

_**Important!** The [unpkg.com](https://unpkg.com), [jsDelivr](https://www.jsdelivr.com/), and [cdnjs](https://cdnjs.com) CDNs are shown here for example purposes only. `unpkg.com`, `jsDelivr`, and `cdnjs` are not affiliated with Google, and there are no guarantees that loading the library from those CDNs will continue to work in the future. Self-hosting the built files rather than loading from the CDN is better for security, reliability, and performance reasons._

**Load the "standard" build** _(using a module script)_

```html
<!-- Append the `?module` param to load the module version of `web-vitals` -->
<script type="module">
  import {onCLS, onINP, onLCP} from 'https://unpkg.com/web-vitals@5?module';

  onCLS(console.log);
  onINP(console.log);
  onLCP(console.log);
</script>
```

Note: When the web-vitals code is isolated from the application code in this way, there is less need to depend on dynamic imports so this code uses a regular `import` line.

**Load the "standard" build** _(using a classic script)_

```html
<script>
  (function () {
    var script = document.createElement('script');
    script.src = 'https://unpkg.com/web-vitals@5/dist/web-vitals.iife.js';
    script.onload = function () {
      // When loading `web-vitals` using a classic script, all the public
      // methods can be found on the `webVitals` global namespace.
      webVitals.onCLS(console.log);
      webVitals.onINP(console.log);
      webVitals.onLCP(console.log);
    };
    document.head.appendChild(script);
  })();
</script>
```

**Load the "attribution" build** _(using a module script)_

```html
<!-- Append the `?module` param to load the module version of `web-vitals` -->
<script type="module">
  import {
    onCLS,
    onINP,
    onLCP,
  } from 'https://unpkg.com/web-vitals@5/dist/web-vitals.attribution.js?module';

  onCLS(console.log);
  onINP(console.log);
  onLCP(console.log);
</script>
```

**Load the "attribution" build** _(using a classic script)_

```html
<script>
  (function () {
    var script = document.createElement('script');
    script.src =
      'https://unpkg.com/web-vitals@5/dist/web-vitals.attribution.iife.js';
    script.onload = function () {
      // When loading `web-vitals` using a classic script, all the public
      // methods can be found on the `webVitals` global namespace.
      webVitals.onCLS(console.log);
      webVitals.onINP(console.log);
      webVitals.onLCP(console.log);
    };
    document.head.appendChild(script);
  })();
</script>
```

## Usage

### Basic usage

Each of the Web Vitals metrics is exposed as a single function that takes a `callback` function that will be called any time the metric value is available and ready to be reported.

The following example measures each of the Core Web Vitals metrics and logs the result to the console once its value is ready to report.

_(The examples below import the "standard" build, but they will work with the "attribution" build as well.)_

```js
import {onCLS, onINP, onLCP} from 'web-vitals';

onCLS(console.log);
onINP(console.log);
onLCP(console.log);
```

Note that some of these metrics will not report until the user has interacted with the page, switched tabs, or the page starts to unload. If you don't see the values logged to the console immediately, try reloading the page (with [preserve log](https://developer.chrome.com/docs/devtools/console/reference/#persist) enabled) or switching tabs and then switching back.

Also, in some cases a metric callback may never be called:

- INP is not reported if the user never interacts with the page.
- CLS, FCP, and LCP are not reported if the page was loaded in the background.

In other cases, a metric callback may be called more than once:

- CLS and INP should be reported any time the [page's `visibilityState` changes to hidden](https://developer.chrome.com/blog/page-lifecycle-api/#advice-hidden).
- All metrics are reported again (with the above exceptions) after a page is restored from the [back/forward cache](https://web.dev/articles/bfcache).

> [!WARNING]
> Do not call any of the Web Vitals functions (e.g. `onCLS()`, `onINP()`, `onLCP()`) more than once per page load. Each of these functions creates a `PerformanceObserver` instance and registers event listeners for the lifetime of the page. While the overhead of calling these functions once is negligible, calling them repeatedly on the same page may eventually result in a memory leak.

### Report the value on every change

In most cases, you only want the `callback` function to be called when the metric is ready to be reported. However, it is possible to report every change (e.g. each larger layout shift as it happens) by setting `reportAllChanges` to `true` in the optional, [configuration object](#reportopts) (second parameter).

> [!IMPORTANT] > `reportAllChanges` only reports when the **metric changes**, not for each **input to the metric**. For example, a new layout shift that does not increase the CLS metric will not be reported even with `reportAllChanges` set to `true` because the CLS metric has not changed. Similarly, for INP, each interaction is not reported even with `reportAllChanges` set to `true`—just when an interaction causes an increase to INP.

This can be useful when debugging, but in general using `reportAllChanges` is not needed (or recommended) for measuring these metrics in production.

```js
import {onCLS} from 'web-vitals';

// Logs CLS as the value changes.
onCLS(console.log, {reportAllChanges: true});
```

### Report only the delta of changes

Some analytics providers allow you to update the value of a metric, even after you've already sent it to their servers (overwriting the previously-sent value with the same `id`).

Other analytics providers, however, do not allow this, so instead of reporting the new value, you need to report only the delta (the difference between the current value and the last-reported value). You can then compute the total value by summing all metric deltas sent with the same ID.

The following example shows how to use the `id` and `delta` properties:

```js
import {onCLS, onINP, onLCP} from 'web-vitals';

function logDelta({name, id, delta}) {
  console.log(`${name} matching ID ${id} changed by ${delta}`);
}

onCLS(logDelta);
onINP(logDelta);
onLCP(logDelta);
```

> [!NOTE]
> The first time the `callback` function is called, its `value` and `delta` properties will be the same.

In addition to using the `id` field to group multiple deltas for the same metric, it can also be used to differentiate different metrics reported on the same page. For example, after a back/forward cache restore, a new metric object is created with a new `id` (since back/forward cache restores are considered separate page visits).

### Send the results to an analytics endpoint

The following example measures each of the Core Web Vitals metrics and reports them to a hypothetical `/analytics` endpoint, as soon as each is ready to be sent.

The `sendToAnalytics()` function uses the [`navigator.sendBeacon()`](https://developer.mozilla.org/docs/Web/API/Navigator/sendBeacon) method, which is widely available across browsers, and supports sending data as the page is being unloaded.

```js
import {onCLS, onINP, onLCP} from 'web-vitals';

function sendToAnalytics(metric) {
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    id: metric.id,

    // Include additional data as needed...
  });

  // Use `navigator.sendBeacon()` to send the data, which supports
  // sending while the page is unloading.
  navigator.sendBeacon('/analytics', body);
}

onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onLCP(sendToAnalytics);
```

### Send the results to Google Analytics

Google Analytics does not support reporting metric distributions in any of its built-in reports; however, if you set a unique event parameter value (in this case, the metric_id, as shown in the example below) on every metric instance that you send to Google Analytics, you can create a report yourself by first getting the data via the [Google Analytics Data API](https://developers.google.com/analytics/devguides/reporting/data/v1) or via [BigQuery export](https://support.google.com/analytics/answer/9358801) and then visualizing it any charting library you choose.

[Google Analytics 4](https://support.google.com/analytics/answer/10089681) introduces a new Event model allowing custom parameters instead of a fixed category, action, and label. It also supports non-integer values, making it easier to measure Web Vitals metrics compared to previous versions.

```js
import {onCLS, onINP, onLCP} from 'web-vitals';

function sendToGoogleAnalytics({name, delta, value, id}) {
  // Assumes the global `gtag()` function exists, see:
  // https://developers.google.com/analytics/devguides/collection/ga4
  gtag('event', name, {
    // Built-in params:
    value: delta, // Use `delta` so the value can be summed.
    // Custom params:
    metric_id: id, // Needed to aggregate events.
    metric_value: value, // Optional.
    metric_delta: delta, // Optional.

    // OPTIONAL: any additional params or debug info here.
    // See: https://web.dev/articles/debug-performance-in-the-field
    // metric_rating: 'good' | 'needs-improvement' | 'poor',
    // debug_info: '...',
    // ...
  });
}

onCLS(sendToGoogleAnalytics);
onINP(sendToGoogleAnalytics);
onLCP(sendToGoogleAnalytics);
```

For details on how to query this data in [BigQuery](https://cloud.google.com/bigquery), or visualise it in [Looker Studio](https://lookerstudio.google.com/), see [Measure and debug performance with Google Analytics 4 and BigQuery](https://web.dev/articles/vitals-ga4).

### Send the results to Google Tag Manager

While `web-vitals` can be called directly from Google Tag Manager, using a pre-defined custom template makes this considerably easier. Some recommended templates include:

- [Core Web Vitals](https://tagmanager.google.com/gallery/#/owners/gtm-templates-simo-ahava/templates/core-web-vitals) by [Simo Ahava](https://www.simoahava.com/). See [Track Core Web Vitals in GA4 with Google Tag Manager](https://www.simoahava.com/analytics/track-core-web-vitals-in-ga4-with-google-tag-manager/) for usage and installation instructions.
- [Web Vitals Template for Google Tag Manager](https://github.com/google-marketing-solutions/web-vitals-gtm-template) by The Google Marketing Solutions team. See the [README](https://github.com/google-marketing-solutions/web-vitals-gtm-template?tab=readme-ov-file#web-vitals-template-for-google-tag-manager) for usage and installation instructions.

### Send attribution data

When using the [attribution build](#attribution-build), you can send additional data to help you debug _why_ the metric values are the way they are.

This example sends an additional `debug_target` param to Google Analytics, corresponding to the element most associated with each metric.

```js
import {onCLS, onINP, onLCP} from 'web-vitals/attribution';

function sendToGoogleAnalytics({name, delta, value, id, attribution}) {
  const eventParams = {
    // Built-in params:
    value: delta, // Use `delta` so the value can be summed.
    // Custom params:
    metric_id: id, // Needed to aggregate events.
    metric_value: value, // Optional.
    metric_delta: delta, // Optional.
  };

  switch (name) {
    case 'CLS':
      eventParams.debug_target = attribution.largestShiftTarget;
      break;
    case 'INP':
      eventParams.debug_target = attribution.interactionTarget;
      break;
    case 'LCP':
      eventParams.debug_target = attribution.target;
      break;
  }

  // Assumes the global `gtag()` function exists, see:
  // https://developers.google.com/analytics/devguides/collection/ga4
  gtag('event', name, eventParams);
}

onCLS(sendToGoogleAnalytics);
onINP(sendToGoogleAnalytics);
onLCP(sendToGoogleAnalytics);
```

> [!NOTE]
> This example relies on custom [event parameters](https://support.google.com/analytics/answer/11396839) in Google Analytics 4.

See [Debug performance in the field](https://web.dev/articles/debug-performance-in-the-field) for more information and examples.

### Batch multiple reports together

Rather than reporting each individual Web Vitals metric separately, you can minimize your network usage by batching multiple metric reports together in a single network request.

However, since not all Web Vitals metrics become available at the same time, and since not all metrics are reported on every page, you cannot simply defer reporting until all metrics are available.

Instead, you should keep a queue of all metrics that were reported and flush the queue whenever the page is backgrounded or unloaded:

```js
import {onCLS, onINP, onLCP} from 'web-vitals';

const queue = new Set();
function addToQueue(metric) {
  queue.add(metric);
}

function flushQueue() {
  if (queue.size > 0) {
    // Replace with whatever serialization method you prefer.
    // Note: JSON.stringify will likely include more data than you need.
    const body = JSON.stringify([...queue]);

    // Use `navigator.sendBeacon()` to send the data, which supports
    // sending while the page is unloading.
    navigator.sendBeacon('/analytics', body);

    queue.clear();
  }
}

onCLS(addToQueue);
onINP(addToQueue);
onLCP(addToQueue);

// Report all available metrics whenever the page is backgrounded or unloaded.
addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') {
    flushQueue();
  }
});
```

> [!NOTE]
> See [the Page Lifecycle guide](https://developers.google.com/web/updates/2018/07/page-lifecycle-api#legacy-lifecycle-apis-to-avoid) for an explanation of why `visibilitychange` is recommended over events like `beforeunload` and `unload`.

<a name="bundle-versions"></a>

## Build options

The `web-vitals` package includes both "standard" and "attribution" builds, as well as different formats of each to allow developers to choose the format that best meets their needs or integrates with their architecture.

The following table lists all the builds distributed with the `web-vitals` package on npm.

<table>
  <tr>
    <td width="35%">
      <strong>Filename</strong> <em>(all within <code>dist/*</code>)</em>
    </td>
    <td><strong>Export</strong></td>
    <td><strong>Description</strong></td>
  </tr>
  <tr>
    <td><code>web-vitals.js</code></td>
    <td><code>pkg.module</code></td>
    <td>
      <p>An ES module bundle of all metric functions, without any attribution features.</p>
      This is the "standard" build and is the simplest way to consume this library out of the box.
    </td>
  </tr>
  <tr>
    <td><code>web-vitals.umd.cjs</code></td>
    <td><code>pkg.main</code></td>
    <td>
      A UMD version of the <code>web-vitals.js</code> bundle (exposed on the <code>self.webVitals.*</code> namespace).
    </td>
  </tr>
  <tr>
    <td><code>web-vitals.iife.js</code></td>
    <td>--</td>
    <td>
      An IIFE version of the <code>web-vitals.js</code> bundle (exposed on the <code>self.webVitals.*</code> namespace).
    </td>
  </tr>
  <tr>
    <td><code>web-vitals.attribution.js</code></td>
    <td>--</td>
    <td>
      An ES module version of all metric functions that includes <a href="#attribution-build">attribution</a> features.
    </td>
  </tr>
    <tr>
    <td><code>web-vitals.attribution.umd.cjs</code></td>
    <td>--</td>
    <td>
      A UMD version of the <code>web-vitals.attribution.js</code> build (exposed on the <code>self.webVitals.*</code> namespace).
    </td>
  </tr>
  </tr>
    <tr>
    <td><code>web-vitals.attribution.iife.js</code></td>
    <td>--</td>
    <td>
      An IIFE version of the <code>web-vitals.attribution.js</code> build (exposed on the <code>self.webVitals.*</code> namespace).
    </td>
  </tr>
</table>

<a name="which-build-is-right-for-you"></a>

### Which build is right for you?

Most developers will generally want to use "standard" build (via either the ES module or UMD version, depending on your bundler/build system), as it's the easiest to use out of the box and integrate into existing tools.

However, if you'd like to collect additional debug information to help you diagnose performance bottlenecks based on real-user issues, use the ["attribution" build](#attribution-build).

For guidance on how to collect and use real-user data to debug performance issues, see [Debug performance in the field](https://web.dev/debug-performance-in-the-field/).

## API

### Types:

#### `Metric`

All metrics types inherit from the following base interface:

```ts
interface Metric {
  /**
   * The name of the metric (in acronym form).
   */
  name: 'CLS' | 'FCP' | 'INP' | 'LCP' | 'TTFB';

  /**
   * The current value of the metric.
   */
  value: number;

  /**
   * The rating as to whether the metric value is within the "good",
   * "needs improvement", or "poor" thresholds of the metric.
   */
  rating: 'good' | 'needs-improvement' | 'poor';

  /**
   * The delta between the current value and the last-reported value.
   * On the first report, `delta` and `value` will always be the same.
   */
  delta: number;

  /**
   * A unique ID representing this particular metric instance. This ID can
   * be used by an analytics tool to dedupe multiple values sent for the same
   * metric instance, or to group multiple deltas together and calculate a
   * total. It can also be used to differentiate multiple different metric
   * instances sent from the same page, which can happen if the page is
   * restored from the back/forward cache (in that case new metrics object
   * get created).
   */
  id: string;

  /**
   * Any performance entries relevant to the metric value calculation.
   * The array may also be empty if the metric value was not based on any
   * entries (e.g. a CLS value of 0 given no layout shifts).
   */
  entries: PerformanceEntry[];

  /**
   * The type of navigation.
   *
   * This will be the value returned by the Navigation Timing API (or
   * `undefined` if the browser doesn't support that API), with the following
   * exceptions:
   * - 'back-forward-cache': for pages that are restored from the bfcache.
   * - 'back_forward' is renamed to 'back-forward' for consistency.
   * - 'prerender': for pages that were prerendered.
   * - 'restore': for pages that were discarded by the browser and then
   * restored by the user.
   */
  navigationType:
    | 'navigate'
    | 'reload'
    | 'back-forward'
    | 'back-forward-cache'
    | 'prerender'
    | 'restore';
}
```

Metric-specific subclasses:

##### `CLSMetric`

```ts
interface CLSMetric extends Metric {
  name: 'CLS';
  entries: LayoutShift[];
}
```

##### `FCPMetric`

```ts
interface FCPMetric extends Metric {
  name: 'FCP';
  entries: PerformancePaintTiming[];
}
```

##### `INPMetric`

```ts
interface INPMetric extends Metric {
  name: 'INP';
  entries: PerformanceEventTiming[];
}
```

##### `LCPMetric`

```ts
interface LCPMetric extends Metric {
  name: 'LCP';
  entries: LargestContentfulPaint[];
}
```

##### `TTFBMetric`

```ts
interface TTFBMetric extends Metric {
  name: 'TTFB';
  entries: PerformanceNavigationTiming[];
}
```

#### `MetricRatingThresholds`

The thresholds of metric's "good", "needs improvement", and "poor" ratings.

- Metric values up to and including [0] are rated "good"
- Metric values up to and including [1] are rated "needs improvement"
- Metric values above [1] are "poor"

| Metric value    | Rating              |
| --------------- | ------------------- |
| ≦ [0]           | "good"              |
| > [0] and ≦ [1] | "needs improvement" |
| > [1]           | "poor"              |

```ts
type MetricRatingThresholds = [number, number];
```

_See also [Rating Thresholds](#rating-thresholds)._

#### `ReportOpts`

```ts
interface ReportOpts {
  reportAllChanges?: boolean;
}
```

Metric-specific subclasses:

##### `INPReportOpts`

```ts
interface INPReportOpts extends ReportOpts {
  durationThreshold?: number;
}
```

#### `AttributionReportOpts`

A subclass of `ReportOpts` used for each metric function exported in the [attribution build](#attribution).

```ts
interface AttributionReportOpts extends ReportOpts {
  generateTarget?: (el: Node | null) => string | null | undefined;
}
```

Metric-specific subclasses:

##### `INPAttributionReportOpts`

```ts
interface INPAttributionReportOpts extends AttributionReportOpts {
  durationThreshold?: number;
}
```

#### `LoadState`

The `LoadState` type is used in several of the metric [attribution objects](#attribution).

```ts
/**
 * The loading state of the document. Note: this value is similar to
 * `document.readyState` but it subdivides the "interactive" state into the
 * time before and after the DOMContentLoaded event fires.
 *
 * State descriptions:
 * - `loading`: the initial document response has not yet been fully downloaded
 *   and parsed. This is equivalent to the corresponding `readyState` value.
 * - `dom-interactive`: the document has been fully loaded and parsed, but
 *   scripts may not have yet finished loading and executing.
 * - `dom-content-loaded`: the document is fully loaded and parsed, and all
 *   scripts (except `async` scripts) have loaded and finished executing.
 * - `complete`: the document and all of its sub-resources have finished
 *   loading. This is equivalent to the corresponding `readyState` value.
 */
type LoadState =
  | 'loading'
  | 'dom-interactive'
  | 'dom-content-loaded'
  | 'complete';
```

### Functions:

#### `onCLS()`

```ts
function onCLS(callback: (metric: CLSMetric) => void, opts?: ReportOpts): void;
```

Calculates the [CLS](https://web.dev/articles/cls) value for the current page and calls the `callback` function once the value is ready to be reported, along with all `layout-shift` performance entries that were used in the metric value calculation. The reported value is a [double](https://heycam.github.io/webidl/#idl-double) (corresponding to a [layout shift score](https://web.dev/articles/cls#layout_shift_score)).

> [!IMPORTANT]
> CLS should be continually monitored for changes throughout the entire lifespan of a page—including if the user returns to the page after it's been hidden/backgrounded. However, since browsers often [will not fire additional callbacks once the user has backgrounded a page](https://developer.chrome.com/blog/page-lifecycle-api/#advice-hidden), `callback` is always called when the page's visibility state changes to hidden. As a result, the `callback` function might be called multiple times during the same page load (see [Reporting only the delta of changes](#report-only-the-delta-of-changes) for how to manage this).

If the `reportAllChanges` [configuration option](#reportopts) is set to `true`, the `callback` function will be called as soon as the value is initially determined as well as any time the value changes throughout the page lifespan (though [not necessarily for every layout shift](#report-the-value-on-every-change)). Note that regardless of whether `reportAllChanges` is used, the final reported value will be the same.

#### `onFCP()`

```ts
function onFCP(callback: (metric: FCPMetric) => void, opts?: ReportOpts): void;
```

Calculates the [FCP](https://web.dev/articles/fcp) value for the current page and calls the `callback` function once the value is ready, along with the relevant `paint` performance entry used to determine the value. The reported value is a [`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp).

#### `onINP()`

```ts
function onINP(
  callback: (metric: INPMetric) => void,
  opts?: INPReportOpts,
): void;
```

Calculates the [INP](https://web.dev/articles/inp) value for the current page and calls the `callback` function once the value is ready, along with the `event` performance entries reported for that interaction. The reported value is a [`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp).

> [!IMPORTANT]
> INP should be continually monitored for changes throughout the entire lifespan of a page—including if the user returns to the page after it's been hidden/backgrounded. However, since browsers often [will not fire additional callbacks once the user has backgrounded a page](https://developer.chrome.com/blog/page-lifecycle-api/#advice-hidden), `callback` is always called when the page's visibility state changes to hidden. As a result, the `callback` function might be called multiple times during the same page load (see [Reporting only the delta of changes](#report-only-the-delta-of-changes) for how to manage this).

A custom `durationThreshold` [configuration option](#reportopts) can optionally be passed to control the minimum duration filter for `event-timing`. Events which are faster than this threshold are not reported. Note that the `first-input` entry is always observed, regardless of duration, to ensure you always have some INP score. The default threshold, after the library is initialized, is `40` milliseconds (the `event-timing` default of `104` milliseconds applies to all events emitted before the library is initialised). This default threshold of `40` is chosen to strike a balance between usefulness and performance. Running this callback for any interaction that spans just one or two frames is likely not worth the insight that could be gained.

If the `reportAllChanges` [configuration option](#reportopts) is set to `true`, the `callback` function will be called as soon as the value is initially determined as well as any time the value changes throughout the page lifespan (though [not necessarily for every interaction](#report-the-value-on-every-change)). Note that regardless of whether `reportAllChanges` is used, the final reported value will be the same.

#### `onLCP()`

```ts
function onLCP(callback: (metric: LCPMetric) => void, opts?: ReportOpts): void;
```

Calculates the [LCP](https://web.dev/articles/lcp) value for the current page and calls the `callback` function once the value is ready (along with the relevant `largest-contentful-paint` performance entry used to determine the value). The reported value is a [`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp).

If the `reportAllChanges` [configuration option](#reportopts) is set to `true`, the `callback` function will be called any time a new `largest-contentful-paint` performance entry is dispatched, or once the final value of the metric has been determined. Note that regardless of whether `reportAllChanges` is used, the final reported value will be the same.

#### `onTTFB()`

```ts
function onTTFB(
  callback: (metric: TTFBMetric) => void,
  opts?: ReportOpts,
): void;
```

Calculates the [TTFB](https://web.dev/articles/ttfb) value for the current page and calls the `callback` function once the page has loaded, along with the relevant `navigation` performance entry used to determine the value. The reported value is a [`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp).

Note, this function waits until after the page is loaded to call `callback` in order to ensure all properties of the `navigation` entry are populated. This is useful if you want to report on other metrics exposed by the [Navigation Timing API](https://w3c.github.io/navigation-timing/).

For example, the TTFB metric starts from the page's [time origin](https://www.w3.org/TR/hr-time-2/#sec-time-origin), which means it includes time spent on DNS lookup, connection negotiation, network latency, and server processing time.

```js
import {onTTFB} from 'web-vitals';

onTTFB((metric) => {
  // Calculate the request time by subtracting from TTFB
  // everything that happened prior to the request starting.
  const requestTime = metric.value - metric.entries[0].requestStart;
  console.log('Request time:', requestTime);
});
```

> [!NOTE]
> Browsers that do not support `navigation` entries will fall back to using `performance.timing` (with the timestamps converted from epoch time to [`DOMHighResTimeStamp`](https://developer.mozilla.org/docs/Web/API/DOMHighResTimeStamp)). This ensures code referencing these values (like in the example above) will work the same in all browsers.

### Rating Thresholds:

The thresholds of each metric's "good", "needs improvement", and "poor" ratings are available as [`MetricRatingThresholds`](#metricratingthresholds).

Example:

```ts
import {CLSThresholds, INPThresholds, LCPThresholds} from 'web-vitals';

console.log(CLSThresholds); // [ 0.1, 0.25 ]
console.log(INPThresholds); // [ 200, 500 ]
console.log(LCPThresholds); // [ 2500, 4000 ]
```

> [!NOTE]
> It's typically not necessary (or recommended) to manually calculate metric value ratings using these thresholds. Use the [`Metric['rating']`](#metric) instead.

### Attribution:

In the [attribution build](#attribution-build) each of the metric functions has two primary differences from their standard build counterparts:

1. Their callback is invoked with a `MetricWithAttribution` objects instead of a `Metric` object. Each `MetricWithAttribution` extends the `Metric` object and adds an additional `attribution` object, which contains potentially-helpful debugging information that can be sent along with the metric values for the current page visit in order to help identify issues happening to real-users in the field.

2. They accept an `AttributionReportOpts` objects instead of a `ReportOpts` object. The `AttributionReportOpts` object supports an additional, optional, `generateTarget()` function that lets developers customize how DOM elements are stringified for reporting purposes. When passed, the return value of the `generateTarget()` function will be used for any "target" properties in the following attribution objects: [`CLSAttribution`](#CLSAttribution), [`INPAttribution`](#INPAttribution), and [`LCPAttribution`](#LCPAttribution). If `null` or `undefined` is returned by the `generateTarget()` function, or no function is given, then the default selector function will be used.

   ```ts
   interface AttributionReportOpts extends ReportOpts {
     generateTarget?: (el: Node | null) => string | null | undefined;
   }
   ```

   For example, if a web page has unique `data-name` attribute on many elements, you may prefer to use those over the built-in selector-style strings that are generated by default.

   ```js
   function customGenerateTarget(el) {
     if (el.dataset.name) {
       return el.dataset.name;
     }

     // Otherwise use default selector function
   }

   onLCP(sendToAnalytics, {generateTarget: customGenerateTarget});
   ```

The next sections document the shape of the `attribution` object for each of the metrics:

#### `CLSAttribution`

```ts
interface CLSAttribution {
  /**
   * By default, a selector identifying the first element (in document order)
   * that shifted when the single largest layout shift that contributed to the
   * page's CLS score occurred. If the `generateTarget` configuration option
   * was passed, then this will instead be the return value of that function,
   * falling back to the default if that returns null or undefined.
   */
  largestShiftTarget?: string;
  /**
   * The time when the single largest layout shift contributing to the page's
   * CLS score occurred.
   */
  largestShiftTime?: DOMHighResTimeStamp;
  /**
   * The layout shift score of the single largest layout shift contributing to
   * the page's CLS score.
   */
  largestShiftValue?: number;
  /**
   * The `LayoutShiftEntry` representing the single largest layout shift
   * contributing to the page's CLS score. (Useful when you need more than just
   * `largestShiftTarget`, `largestShiftTime`, and `largestShiftValue`).
   */
  largestShiftEntry?: LayoutShift;
  /**
   * The first element source (in document order) among the `sources` list
   * of the `largestShiftEntry` object. (Also useful when you need more than
   * just `largestShiftTarget`, `largestShiftTime`, and `largestShiftValue`).
   */
  largestShiftSource?: LayoutShiftAttribution;
  /**
   * The loading state of the document at the time when the largest layout
   * shift contribution to the page's CLS score occurred (see `LoadState`
   * for details).
   */
  loadState?: LoadState;
}
```

#### `FCPAttribution`

```ts
interface FCPAttribution {
  /**
   * The time from when the user initiates loading the page until when the
   * browser receives the first byte of the response (a.k.a. TTFB).
   */
  timeToFirstByte: number;
  /**
   * The delta between TTFB and the first contentful paint (FCP).
   */
  firstByteToFCP: number;
  /**
   * The loading state of the document at the time when FCP `occurred (see
   * `LoadState` for details). Ideally, documents can paint before they finish
   * loading (e.g. the `loading` or `dom-interactive` phases).
   */
  loadState: LoadState;
  /**
   * The `PerformancePaintTiming` entry corresponding to FCP.
   */
  fcpEntry?: PerformancePaintTiming;
  /**
   * The `navigation` entry of the current page, which is useful for diagnosing
   * general page load issues. This can be used to access `serverTiming` for example:
   * navigationEntry?.serverTiming
   */
  navigationEntry?: PerformanceNavigationTiming;
}
```

#### `INPAttribution`

```ts
interface INPAttribution {
  /**
   * By default, a selector identifying the element that the user first
   * interacted with as part of the frame where the INP candidate interaction
   * occurred. If this value is an empty string, that generally means the
   * element was removed from the DOM after the interaction. If the
   * `generateTarget` configuration option was passed, then this will instead
   * be the return value of that function, falling back to the default if that
   * returns null or undefined.
   */
  interactionTarget: string;
  /**
   * The time when the user first interacted during the frame where the INP
   * candidate interaction occurred (if more than one interaction occurred
   * within the frame, only the first time is reported).
   */
  interactionTime: DOMHighResTimeStamp;
  /**
   * The type of interaction, based on the event type of the `event` entry
   * that corresponds to the interaction (i.e. the first `event` entry
   * containing an `interactionId` dispatched in a given animation frame).
   * For "pointerdown", "pointerup", or "click" events this will be "pointer",
   * and for "keydown" or "keyup" events this will be "keyboard".
   */
  interactionType: 'pointer' | 'keyboard';
  /**
   * The best-guess timestamp of the next paint after the interaction.
   * In general, this timestamp is the same as the `startTime + duration` of
   * the event timing entry. However, since duration values are rounded to the
   * nearest 8ms (and can be rounded down), this value is clamped to always be
   * reported after the processing times.
   */
  nextPaintTime: DOMHighResTimeStamp;
  /**
   * An array of Event Timing entries that were processed within the same
   * animation frame as the INP candidate interaction.
   * Note this is capped to a max of 5 entries (the first 4 + the last one).
   */
  processedEventEntries: PerformanceEventTiming[];
  /**
   * The time from when the user interacted with the page until when the
   * browser was first able to start processing event listeners for that
   * interaction. This time captures the delay before event processing can
   * begin due to the main thread being busy with other work.
   */
  inputDelay: number;
  /**
   * The time from when the first event listener started running in response to
   * the user interaction until when all event listener processing has finished.
   */
  processingDuration: number;
  /**
   * The time from when the browser finished processing all event listeners for
   * the user interaction until the next frame is presented on the screen and
   * visible to the user. This time includes work on the main thread (such as
   * `requestAnimationFrame()` callbacks, `ResizeObserver` and
   * `IntersectionObserver` callbacks, and style/layout calculation) as well
   * as off-main-thread work (such as compositor, GPU, and raster work).
   */
  presentationDelay: number;
  /**
   * The loading state of the document at the time when the interaction
   * corresponding to INP occurred (see `LoadState` for details). If the
   * interaction occurred while the document was loading and executing script
   * (e.g. usually in the `dom-interactive` phase) it can result in long delays.
   */
  loadState: LoadState;
  /**
   * If the browser supports the Long Animation Frame API, this array will
   * include any `long-animation-frame` entries that intersect with the INP
   * candidate interaction's `startTime` and the `processingEnd` time of the
   * last event processed within that animation frame. If the browser does not
   * support the Long Animation Frame API or no `long-animation-frame` entries
   * are detected, this array will be empty.
   */
  longAnimationFrameEntries: PerformanceLongAnimationFrameTiming[];
  /**
   * Summary information about the longest script entry intersecting the INP
   * duration. Note, only script entries above 5 milliseconds are reported by
   * the Long Animation Frame API.
   */
  longestScript?: INPLongestScriptSummary;
  /**
   * The total duration of Long Animation Frame scripts that intersect the INP
   * duration excluding any forced style and layout (that is included in
   * totalStyleAndLayout). Note, this is limited to scripts > 5 milliseconds.
   */
  totalScriptDuration?: number;
  /**
   * The total style and layout duration from any Long Animation Frames
   * intersecting the INP interaction. This includes any end-of-frame style and
   * layout duration + any forced style and layout duration.
   */
  totalStyleAndLayoutDuration?: number;
  /**
   * The off main-thread presentation delay from the end of the last Long
   * Animation Frame (where available) until the INP end point.
   */
  totalPaintDuration?: number;
  /**
   * The total unattributed time not included in any of the previous totals.
   * This includes scripts < 5 milliseconds and other timings not attributed
   * by Long Animation Frame (including when a frame is < 50ms and so has no
   * Long Animation Frame).
   * When no Long Animation Frames are present this will be undefined, rather
   * than everything being unattributed to make it clearer when it's expected
   * to be small.
   */
  totalUnattributedDuration?: number;
}
```

#### `INPLongestScriptSummary`

```ts
interface INPLongestScriptSummary {
  /**
   * The longest Long Animation Frame script entry that intersects the INP
   * interaction.
   */
  entry: PerformanceScriptTiming;
  /**
   * The INP subpart where the longest script ran.
   */
  subpart: 'input-delay' | 'processing-duration' | 'presentation-delay';
  /**
   * The amount of time the longest script intersected the INP duration.
   */
  intersectingDuration: number;
}
```

#### `LCPAttribution`

```ts
interface LCPAttribution {
  /**
   * By default, a selector identifying the element corresponding to the
   * largest contentful paint for the page. If the `generateTarget`
   * configuration option was passed, then this will instead be the return
   * value of that function, falling back to the default if that returns null
   * or undefined.
   */
  target?: string;
  /**
   * The URL (if applicable) of the LCP image resource. If the LCP element
   * is a text node, this value will not be set.
   */
  url?: string;
  /**
   * The time from when the user initiates loading the page until when the
   * browser receives the first byte of the response (a.k.a. TTFB). See
   * [Optimize LCP](https://web.dev/articles/optimize-lcp) for details.
   */
  timeToFirstByte: number;
  /**
   * The delta between TTFB and when the browser starts loading the LCP
   * resource (if there is one, otherwise 0). See [Optimize
   * LCP](https://web.dev/articles/optimize-lcp) for details.
   */
  resourceLoadDelay: number;
  /**
   * The total time it takes to load the LCP resource itself (if there is one,
   * otherwise 0). See [Optimize LCP](https://web.dev/articles/optimize-lcp) for
   * details.
   */
  resourceLoadDuration: number;
  /**
   * The delta between when the LCP resource finishes loading until the LCP
   * element is fully rendered. See [Optimize
   * LCP](https://web.dev/articles/optimize-lcp) for details.
   */
  elementRenderDelay: number;
  /**
   * The `navigation` entry of the current page, which is useful for diagnosing
   * general page load issues. This can be used to access `serverTiming` for example:
   * navigationEntry?.serverTiming
   */
  navigationEntry?: PerformanceNavigationTiming;
  /**
   * The `resource` entry for the LCP resource (if applicable), which is useful
   * for diagnosing resource load issues.
   */
  lcpResourceEntry?: PerformanceResourceTiming;
  /**
   * The `LargestContentfulPaint` entry corresponding to LCP.
   */
  lcpEntry?: LargestContentfulPaint;
}
```

#### `TTFBAttribution`

```ts
interface TTFBAttribution {
  /**
   * The total time from when the user initiates loading the page to when the
   * page starts to handle the request. Large values here are typically due
   * to HTTP redirects, though other browser processing contributes to this
   * duration as well (so even without redirect it's generally not zero).
   */
  waitingDuration: number;
  /**
   * The total time spent checking the HTTP cache for a match. For navigations
   * handled via service worker, this duration usually includes service worker
   * start-up time as well as time processing `fetch` event listeners, with
   * some exceptions, see: https://github.com/w3c/navigation-timing/issues/199
   */
  cacheDuration: number;
  /**
   * The total time to resolve the DNS for the requested domain.
   */
  dnsDuration: number;
  /**
   * The total time to create the connection to the requested domain.
   */
  connectionDuration: number;
  /**
   * The total time from when the request was sent until the first byte of the
   * response was received. This includes network time as well as server
   * processing time.
   */
  requestDuration: number;
  /**
   * The `navigation` entry of the current page, which is useful for diagnosing
   * general page load issues. This can be used to access `serverTiming` for
   * example: navigationEntry?.serverTiming
   */
  navigationEntry?: PerformanceNavigationTiming;
}
```

## Browser Support

The `web-vitals` code is tested in Chrome, Firefox, and Safari. In addition, all JavaScript features used in the code are part of ([Baseline Widely Available](https://web.dev/baseline)), and thus should run without error in all versions of these browsers released within the last 30 months.

However, some of the APIs required to capture these metrics (notable CLS) are currently only available in some browsers. The latest browser support for each function is as follows:

- `onCLS()`: Chromium
- `onFCP()`: Chromium, Firefox, Safari
- `onINP()`: Chromium, Firefox, Safari
- `onLCP()`: Chromium, Firefox, Safari
- `onTTFB()`: Chromium, Firefox, Safari

## Limitations

The `web-vitals` library is primarily a wrapper around the Web APIs that measure the Web Vitals metrics, which means the limitations of those APIs will mostly apply to this library as well. More details on these limitations is available in [this blog post](https://web.dev/articles/crux-and-rum-differences).

The primary limitation of these APIs is they have no visibility into `<iframe>` content (not even same-origin iframes), which means pages that make use of iframes will likely see a difference between the data measured by this library and the data available in the Chrome User Experience Report (which does include iframe content).

For same-origin iframes, it's possible to use the `web-vitals` library to measure metrics, but it's tricky because it requires the developer to add the library to every frame and `postMessage()` the results to the parent frame for aggregation.

> [!NOTE]
> Given the lack of iframe support, the `onCLS()` function technically measures [DCLS](https://github.com/wicg/layout-instability#cumulative-scores) (Document Cumulative Layout Shift) rather than CLS, if the page includes iframes).

## Development

### Building the code

The `web-vitals` source code is written in TypeScript. To transpile the code and build the production bundles, run the following command.

```sh
npm run build
```

To build the code and watch for changes, run:

```sh
npm run watch
```

### Running the tests

The `web-vitals` code is tested in real browsers using [webdriver.io](https://webdriver.io/). Use the following command to run the tests:

```sh
npm test
```

To test any of the APIs manually, you can start the test server

```sh
npm run test:server
```

Then navigate to `http://localhost:9090/test/<view>`, where `<view>` is the basename of one the templates under [/test/views/](/test/views/).

You'll likely want to combine this with `npm run watch` to ensure any changes you make are transpiled and rebuilt.

## Integrations

- [**Web Vitals Connector**](https://goo.gle/web-vitals-connector): Data Studio connector to create dashboards from [Web Vitals data captured in BigQuery](https://web.dev/articles/vitals-ga4).
- [**Core Web Vitals Custom Tag template**](https://www.simoahava.com/custom-templates/core-web-vitals/): Custom GTM template tag to [add measurement handlers](https://www.simoahava.com/analytics/track-core-web-vitals-in-ga4-with-google-tag-manager/) for all Core Web Vitals metrics.
- [**`web-vitals-reporter`**](https://github.com/treosh/web-vitals-reporter): JavaScript library to batch `callback` functions and send data with a single request.

## License

[Apache 2.0](/LICENSE)


================================================
FILE: attribution.d.ts
================================================
/*
 Copyright 2022 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.
*/

export * from './dist/modules/attribution/index.js';


================================================
FILE: attribution.js
================================================
/*
 Copyright 2022 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.
*/

// Creates the `web-vitals/attribution` import in node-based bundlers.
// This will not be needed when export maps are widely supported.
export * from './dist/web-vitals.attribution.js';


================================================
FILE: docs/upgrading-to-v4.md
================================================
# Upgrading to v4

This document lists the full set of changes between version 3 and version 4 that are relevant to anyone wanting to upgrade to the new version. This document groups changes into "breaking changes", "new features", and "deprecations" across both the "standard" and "attribution" builds (see [build options](/#build-options) for details).

## ❌ Breaking changes

### Standard build

#### General

- **Removed** the "base+polyfill" build, which includes the FID polyfill and the Navigation Timing polyfill supporting legacy Safari browsers ([#435](https://github.com/GoogleChrome/web-vitals/pull/435)).
- **Removed** all `getXXX()` functions that were deprecated in v3 ([#435](https://github.com/GoogleChrome/web-vitals/pull/435)).

#### `INPMetric`

- **Changed** `entries` to only include entries with matching `interactionId` that were processed within the same animation frame. Previously it included all entries with matching `interactionId` values, which could include entries not impacting INP ([#442](https://github.com/GoogleChrome/web-vitals/pull/442)).

### Attribution build

#### `INPAttribution`

- **Renamed** `eventTarget` to `interactionTarget` ([#442](https://github.com/GoogleChrome/web-vitals/pull/442)).
- **Renamed** `eventTime` to `interactionTime` ([#442](https://github.com/GoogleChrome/web-vitals/pull/442)).
- **Renamed** `eventType` to `interactionType`. Also this property will now always be either "pointer" or "keyboard" ([#442](https://github.com/GoogleChrome/web-vitals/pull/442)).
- **Removed** `eventEntry` in favor of the new `processedEventEntries` array (see below), which includes all `event` entries processed within the same animation frame as the INP candidate interaction ([#442](https://github.com/GoogleChrome/web-vitals/pull/442)).

#### `LCPAttribution`

- **Renamed** `resourceLoadTime` to `resourceLoadDuration` ([#450](https://github.com/GoogleChrome/web-vitals/pull/450)).

#### `TTFBAttribution`

- **Renamed** `waitingTime` to `waitingDuration`, and also split out the portion of this duration spent checking the HTTP cache, see `cacheDuration` in the [new features](#-new-features) section below ([#453](https://github.com/GoogleChrome/web-vitals/pull/453), [#458](https://github.com/GoogleChrome/web-vitals/pull/458)).
- **Renamed** `dnsTime` to `dnsDuration` ([#453](https://github.com/GoogleChrome/web-vitals/pull/453)).
- **Renamed** `connectionTime` to `connectionDuration` ([#453](https://github.com/GoogleChrome/web-vitals/pull/453)).
- **Renamed** `requestTime` to `requestDuration` ([#453](https://github.com/GoogleChrome/web-vitals/pull/453)).

## 🚀 New features

### Standard build

No new features were introduced into the "standard" build, outside of the breaking changes mentioned above.

### Attribution build

#### `INPAttribution`

- **Added** `nextPaintTime`, which marks the timestamp of the next paint after the interaction ([#442](https://github.com/GoogleChrome/web-vitals/pull/442)).
- **Added** `inputDelay`, which measures the time from when the user interacted with the page until when the browser was first able to start processing event listeners for that interaction. ([#442](https://github.com/GoogleChrome/web-vitals/pull/442)).
- **Added** `processingDuration`, which measures the time from when the first event listener started running in response to the user interaction until when all event listener processing has finished ([#442](https://github.com/GoogleChrome/web-vitals/pull/442)).
- **Added** `presentationDelay`, which measures the time from when the browser finished processing all event listeners for the user interaction until the next frame is presented on the screen and visible to the user. ([#442](https://github.com/GoogleChrome/web-vitals/pull/442)).
- **Added** `processedEventEntries`, an array of `event` entries that were processed within the same animation frame as the INP candidate interaction ([#442](https://github.com/GoogleChrome/web-vitals/pull/442)).
- **Added** `longAnimationFrameEntries`, which includes any `long-animation-frame` entries that overlap with the INP candidate interaction ([#442](https://github.com/GoogleChrome/web-vitals/pull/442)).
- **Added** `interactionTargetElement` ([#479](https://github.com/GoogleChrome/web-vitals/pull/479)).

#### `TTFBAttribution`

- **Added** `cacheDuration`, which marks the total time spent checking the HTTP cache for a match ([#458](https://github.com/GoogleChrome/web-vitals/pull/458)).

## ⚠️ Deprecations

### Standard and attribution builds

- The `onFID()` function [has been deprecated](https://web.dev/blog/inp-cwv-launch#fid_deprecation_timeline). Developers should use `onINP()` instead ([#435](https://github.com/GoogleChrome/web-vitals/pull/435)).
- The `ReportCallback` type has been deprecated in favor of explicit callback types for each metric function ([#483](https://github.com/GoogleChrome/web-vitals/pull/483)).

_All deprecated APIs will be removed in the next major version._


================================================
FILE: docs/upgrading-to-v5.md
================================================
# Upgrading to v5

This document lists the full set of changes between version 4 and version 5 that are relevant to anyone wanting to upgrade to the new version. This document groups changes into "breaking changes", "new features", and "deprecations" across both the "standard" and "attribution" builds (see [build options](/#build-options) for details).

## ❌ Breaking changes

### Standard build

#### General

- **Removed** First Input Delay (FID) support and `onFID()` [as deprecated in v4](./upgrading-to-v4.md#%EF%B8%8F-deprecations). FID has been replaced by INP and this [removal was previously advertised in that announcement post](https://web.dev/blog/inp-cwv-launch#fid_deprecation_timeline). ([#519](https://github.com/GoogleChrome/web-vitals/pull/519))
- **Changed** the browser support policy to [Baseline Widely available](https://web.dev/baseline) browser support. ([#525](https://github.com/GoogleChrome/web-vitals/pull/525))

##### More details on the Baseline Widely available change:

All of the [builds](README#build-options) in `web-vitals` v5 use only [Baseline Widely available](https://web.dev/baseline) APIs, which means they should run without error in all browsers released in the last few years. Note that the Core Web Vitals metrics are only available in modern browsers, so legacy browser support is unnecessary for this library.

If your site needs to support legacy browsers, you can still use the `web-vitals` library without causing errors in those browsers by adhering to the following recommendations:

We recommend loading the `web-vitals` library in a separate script file from your site's main application bundle(s), either via `<script type="module">` and `import` statements or via your bundler's code splitting feature (for example, [rollup](https://rollupjs.org/tutorial/#code-splitting), [esbuild](https://esbuild.github.io/api/#splitting), and [webpack](https://webpack.js.org/guides/code-splitting/)). This ensures that any errors encountered while loading the library do not impact other code on your site.

If you do choose to include the `web-vitals` library code in your main application bundle—and you also need to support very old browsers—it's critical that you configure your bundler to transpile the `web-vitals` code along with the rest of you application JavaScript. This is important because most bundlers do not transpile `node_modules` by default.

### Attribution build

#### `INPAttribution`, `LCPAttribution`, and `CLSAttribution`

- **Changed** to sort the classes that appear in attribution selectors to reduce cardinality. While not a breaking change in the API, this may result in a short term difference in reports based on the selector during the change over from v4 to v5, but longer term should result in few selectors that are more easily grouped in reporting. ([#518](https://github.com/GoogleChrome/web-vitals/pull/518))
- **Changed** `LCPAttribution.element` to `LCPAttribution.target` as part of ([#585](https://github.com/GoogleChrome/web-vitals/pull/585)) for consistency.
- **Removed** `INPAttribution.interactionTargetElement` by default. If needed this can be generated as part of support for generating custom targets in the attribution build ([#585](https://github.com/GoogleChrome/web-vitals/pull/585))

## 🚀 New features

- **Added** support for generating custom targets in the attribution build ([#585](https://github.com/GoogleChrome/web-vitals/pull/585))
- **Added** extended INP attribution with extra LoAF information: longest script and buckets ([#592](https://github.com/GoogleChrome/web-vitals/pull/592))

## ⚠️ Deprecations

There were no deprecations in v5.


================================================
FILE: eslint.config.js
================================================
import {defineConfig} from 'eslint/config';
import globals from 'globals';
import tsParser from '@typescript-eslint/parser';
import path from 'node:path';
import {fileURLToPath} from 'node:url';
import js from '@eslint/js';
import {FlatCompat} from '@eslint/eslintrc';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
  baseDirectory: __dirname,
  recommendedConfig: js.configs.recommended,
  allConfig: js.configs.all,
});

export default defineConfig([
  {
    ignores: [
      '**/.DS_Store',
      '**/.vscode/',
      '**/node_modules/',
      '**/*.log',
      '**/tsconfig.tsbuildinfo',
      '**/dist/',
    ],
  },
  {
    languageOptions: {
      globals: {
        ...globals.browser,
        ...globals.node,
        ...globals.mocha,
      },

      ecmaVersion: 'latest',
      sourceType: 'module',
    },
  },
  {
    files: ['**/wdio.conf.js'],
    extends: compat.extends('eslint:recommended'),

    rules: {
      'max-len': 'off',
    },
  },
  {
    files: ['test/e2e/*.js'],
    extends: compat.extends('eslint:recommended'),

    languageOptions: {
      globals: {
        $: false,
        browser: false,
        __toSafeObject: false,
      },
    },

    rules: {
      'comma-dangle': ['error', 'always-multiline'],
      indent: ['error', 2],
      'no-invalid-this': 'off',

      'max-len': [
        2,
        {
          ignorePattern: '^\\s*import|= require\\(|^\\s*it\\(|^\\s*describe\\(',
          ignoreUrls: true,
        },
      ],
    },
  },
  {
    files: ['src/**/*.ts'],
    extends: compat.extends('plugin:@typescript-eslint/recommended'),

    languageOptions: {
      parser: tsParser,
      ecmaVersion: 2018,
      sourceType: 'module',
    },

    rules: {
      '@typescript-eslint/no-non-null-assertion': 'off',
      '@typescript-eslint/no-use-before-define': 'off',
      '@typescript-eslint/explicit-function-return-type': 'off',
      '@typescript-eslint/explicit-module-boundary-types': 'off',
      '@typescript-eslint/ban-ts-comment': 'off',
      'comma-dangle': ['error', 'always-multiline'],
      indent: ['error', 2],
      'no-dupe-class-members': 'off',
      'prefer-spread': 'off',
    },
  },
]);


================================================
FILE: package.json
================================================
{
  "name": "web-vitals",
  "version": "5.1.0",
  "description": "Easily measure performance metrics in JavaScript",
  "type": "module",
  "typings": "dist/modules/index.d.ts",
  "main": "dist/web-vitals.umd.cjs",
  "module": "dist/web-vitals.js",
  "unpkg": "dist/web-vitals.iife.js",
  "exports": {
    ".": {
      "types": "./dist/modules/index.d.ts",
      "require": "./dist/web-vitals.umd.cjs",
      "default": "./dist/web-vitals.js"
    },
    "./attribution": {
      "types": "./dist/modules/attribution/index.d.ts",
      "require": "./dist/web-vitals.attribution.umd.cjs",
      "default": "./dist/web-vitals.attribution.js"
    },
    "./attribution.js": {
      "types": "./dist/modules/attribution/index.d.ts",
      "require": "./dist/web-vitals.attribution.umd.cjs",
      "default": "./dist/web-vitals.attribution.js"
    },
    "./onCLS.js": {
      "types": "./dist/modules/onCLS.d.ts",
      "default": "./dist/modules/onCLS.js"
    },
    "./onFCP.js": {
      "types": "./dist/modules/onFCP.d.ts",
      "default": "./dist/modules/onFCP.js"
    },
    "./onINP.js": {
      "types": "./dist/modules/onINP.d.ts",
      "default": "./dist/modules/onINP.js"
    },
    "./onLCP.js": {
      "types": "./dist/modules/onLCP.d.ts",
      "default": "./dist/modules/onLCP.js"
    },
    "./onTTFB.js": {
      "types": "./dist/modules/onTTFB.d.ts",
      "default": "./dist/modules/onTTFB.js"
    },
    "./attribution/onCLS.js": {
      "types": "./dist/modules/attribution/onCLS.d.ts",
      "default": "./dist/modules/attribution/onCLS.js"
    },
    "./attribution/onFCP.js": {
      "types": "./dist/modules/attribution/onFCP.d.ts",
      "default": "./dist/modules/attribution/onFCP.js"
    },
    "./attribution/onINP.js": {
      "types": "./dist/modules/attribution/onINP.d.ts",
      "default": "./dist/modules/attribution/onINP.js"
    },
    "./attribution/onLCP.js": {
      "types": "./dist/modules/attribution/onLCP.d.ts",
      "default": "./dist/modules/attribution/onLCP.js"
    },
    "./attribution/onTTFB.js": {
      "types": "./dist/modules/attribution/onTTFB.d.ts",
      "default": "./dist/modules/attribution/onTTFB.js"
    }
  },
  "files": [
    "attribution.js",
    "attribution.d.ts",
    "dist",
    "src"
  ],
  "scripts": {
    "build": "run-s clean build:ts build:js",
    "build:ts": "tsc -b",
    "build:js": "rollup -c",
    "clean": "rm -rf dist tsconfig.tsbuildinfo",
    "dev": "run-p watch test:server",
    "format": "prettier \"**/*.{cjs,css,html,js,json,md,ts,yml,yaml}\" --write --ignore-path .gitignore",
    "format:check": "prettier \"**/*.{cjs,css,html,js,json,html,md,ts,yml,yaml}\" --check --ignore-path .gitignore",
    "lint": "eslint \"*.js\" \"src/**/*.ts\" \"test/**/*.js\"",
    "lint:fix": "eslint --fix \"*.js\" \"src/**/*.ts\" \"test/**/*.js\"",
    "postversion": "git push --follow-tags",
    "release:major": "npm version major -m 'Release v%s' && npm publish",
    "release:minor": "npm version minor -m 'Release v%s' && npm publish",
    "release:patch": "npm version patch -m 'Release v%s' && npm publish",
    "release:alpha": "npm version prerelease --preid=alpha -m 'Release v%s' && npm publish --tag next",
    "release:beta": "npm version prerelease --preid=beta -m 'Release v%s' && npm publish --tag next",
    "release:rc": "npm version prerelease --preid=rc -m 'Release v%s' && npm publish --tag next",
    "test": "npm-run-all build test:unit -p -r test:e2e test:server",
    "test:e2e": "wdio",
    "test:server": "node test/server.js",
    "test:unit": "node --test test/unit/*test.js",
    "start": "run-s build:ts test:server watch",
    "watch": "run-p watch:*",
    "watch:ts": "tsc -b -w",
    "watch:js": "rollup -c -w",
    "version": "run-s build",
    "prepare": "husky"
  },
  "keywords": [
    "crux",
    "performance",
    "metrics",
    "Core Web Vitals",
    "CLS",
    "FCP",
    "INP",
    "LCP",
    "TTFB"
  ],
  "author": {
    "name": "Philip Walton",
    "email": "philip@philipwalton.com",
    "url": "http://philipwalton.com"
  },
  "license": "Apache-2.0",
  "repository": {
    "type": "git",
    "url": "https://github.com/GoogleChrome/web-vitals.git"
  },
  "bugs": {
    "url": "https://github.com/GoogleChrome/web-vitals/issues"
  },
  "husky": {
    "hooks": {
      "pre-commit": "npm run lint"
    }
  },
  "prettier": {
    "arrowParens": "always",
    "bracketSpacing": false,
    "quoteProps": "preserve",
    "singleQuote": true
  },
  "devDependencies": {
    "@babel/core": "^7.28.5",
    "@babel/preset-env": "^7.28.5",
    "@rollup/plugin-babel": "^6.1.0",
    "@rollup/plugin-terser": "^0.4.4",
    "@typescript-eslint/eslint-plugin": "^8.52.0",
    "@typescript-eslint/parser": "^8.52.0",
    "@wdio/cli": "^9.23.0",
    "@wdio/local-runner": "^9.25.0",
    "@wdio/mocha-framework": "^9.25.0",
    "@wdio/spec-reporter": "^9.25.0",
    "eslint": "^9.39.2",
    "fs-extra": "^11.3.3",
    "husky": "^9.1.7",
    "lint-staged": "^16.2.7",
    "npm-run-all": "^4.1.5",
    "nunjucks": "^3.2.4",
    "prettier": "^3.7.4",
    "rollup": "^4.55.1",
    "typescript": "^5.9.3",
    "yargs": "^18.0.0"
  },
  "lint-staged": {
    "**/*.{js,ts}": "eslint --fix",
    "**/*.{cjs,css,html,js,json,html,md,ts,yml,yaml}": "prettier --write --ignore-path .gitignore"
  }
}


================================================
FILE: rollup.config.js
================================================
/*
 Copyright 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 babel from '@rollup/plugin-babel';
import terser from '@rollup/plugin-terser';

const configurePlugins = ({module}) => {
  return [
    babel({
      babelHelpers: 'bundled',
      presets: [
        [
          '@babel/preset-env',
          {
            bugfixes: true,
            targets: 'baseline widely available',
          },
        ],
      ],
    }),
    terser({
      module,
      compress: true,
      mangle: {
        properties: {
          // Any object properties beginning with the '_' character will be
          // mangled. Use this prefix for any object properties that are not
          // part of the public API and do that not match an existing build-in
          // API names (e.g. `.id` or `.entries`).
          regex: /^_/,
        },
      },
    }),
  ];
};

const configs = [
  {
    input: 'dist/modules/index.js',
    output: {
      format: 'esm',
      file: './dist/web-vitals.js',
    },
    plugins: configurePlugins({module: true}),
  },
  {
    input: 'dist/modules/index.js',
    output: {
      format: 'umd',
      file: `./dist/web-vitals.umd.cjs`,
      name: 'webVitals',
    },
    plugins: configurePlugins({module: false}),
  },
  {
    input: 'dist/modules/index.js',
    output: {
      format: 'iife',
      file: './dist/web-vitals.iife.js',
      name: 'webVitals',
    },
    plugins: configurePlugins({module: false}),
  },
  {
    input: 'dist/modules/attribution/index.js',
    output: {
      format: 'esm',
      file: './dist/web-vitals.attribution.js',
    },
    plugins: configurePlugins({module: true}),
  },
  {
    input: 'dist/modules/attribution/index.js',
    output: {
      format: 'umd',
      file: `./dist/web-vitals.attribution.umd.cjs`,
      name: 'webVitals',
    },
    plugins: configurePlugins({module: false}),
  },
  {
    input: 'dist/modules/attribution/index.js',
    output: {
      format: 'iife',
      file: './dist/web-vitals.attribution.iife.js',
      name: 'webVitals',
    },
    plugins: configurePlugins({module: false}),
  },
];

export default configs;


================================================
FILE: src/attribution/index.ts
================================================
/*
 * Copyright 2022 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.
 */

export {onCLS} from './onCLS.js';
export {onFCP} from './onFCP.js';
export {onINP} from './onINP.js';
export {onLCP} from './onLCP.js';
export {onTTFB} from './onTTFB.js';

export {CLSThresholds} from '../onCLS.js';
export {FCPThresholds} from '../onFCP.js';
export {INPThresholds} from '../onINP.js';
export {LCPThresholds} from '../onLCP.js';
export {TTFBThresholds} from '../onTTFB.js';

export * from '../types.js';


================================================
FILE: src/attribution/onCLS.ts
================================================
/*
 * Copyright 2022 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 {LayoutShiftManager} from '../lib/LayoutShiftManager.js';
import {getLoadState} from '../lib/getLoadState.js';
import {getSelector} from '../lib/getSelector.js';
import {initUnique} from '../lib/initUnique.js';
import {onCLS as unattributedOnCLS} from '../onCLS.js';
import {
  CLSAttribution,
  CLSMetric,
  CLSMetricWithAttribution,
  AttributionReportOpts,
} from '../types.js';

const getLargestLayoutShiftEntry = (entries: LayoutShift[]) => {
  return entries.reduce((a, b) => (a.value > b.value ? a : b));
};

const getLargestLayoutShiftSource = (sources: LayoutShiftAttribution[]) => {
  return sources.find((s) => s.node?.nodeType === 1) || sources[0];
};

/**
 * Calculates the [CLS](https://web.dev/articles/cls) value for the current page and
 * calls the `callback` function once the value is ready to be reported, along
 * with all `layout-shift` performance entries that were used in the metric
 * value calculation. The reported value is a `double` (corresponding to a
 * [layout shift score](https://web.dev/articles/cls#layout_shift_score)).
 *
 * If the `reportAllChanges` configuration option is set to `true`, the
 * `callback` function will be called as soon as the value is initially
 * determined as well as any time the value changes throughout the page
 * lifespan.
 *
 * _**Important:** CLS should be continually monitored for changes throughout
 * the entire lifespan of a page—including if the user returns to the page after
 * it's been hidden/backgrounded. However, since browsers often [will not fire
 * additional callbacks once the user has backgrounded a
 * page](https://developer.chrome.com/blog/page-lifecycle-api/#advice-hidden),
 * `callback` is always called when the page's visibility state changes to
 * hidden. As a result, the `callback` function might be called multiple times
 * during the same page load._
 */
export const onCLS = (
  onReport: (metric: CLSMetricWithAttribution) => void,
  opts: AttributionReportOpts = {},
) => {
  // Clone the opts object to ensure it's unique, so we can initialize a
  // single instance of the `LayoutShiftManager` class that's shared only with
  // this function invocation and the `unattributedOnCLS()` invocation below
  // (which is passed the same `opts` object).
  opts = Object.assign({}, opts);

  const layoutShiftManager = initUnique(opts, LayoutShiftManager);
  const layoutShiftTargetMap: WeakMap<LayoutShiftAttribution, string> =
    new WeakMap();

  layoutShiftManager._onAfterProcessingUnexpectedShift = (
    entry: LayoutShift,
  ) => {
    if (entry?.sources?.length) {
      const largestSource = getLargestLayoutShiftSource(entry.sources);
      const node = largestSource?.node;
      if (node) {
        const customTarget = opts.generateTarget?.(node) ?? getSelector(node);
        layoutShiftTargetMap.set(largestSource, customTarget);
      }
    }
  };

  const attributeCLS = (metric: CLSMetric): CLSMetricWithAttribution => {
    // Use an empty object if no other attribution has been set.
    let attribution: CLSAttribution = {};

    if (metric.entries.length) {
      const largestEntry = getLargestLayoutShiftEntry(metric.entries);
      if (largestEntry?.sources?.length) {
        const largestSource = getLargestLayoutShiftSource(largestEntry.sources);
        if (largestSource) {
          attribution = {
            largestShiftTarget: layoutShiftTargetMap.get(largestSource),
            largestShiftTime: largestEntry.startTime,
            largestShiftValue: largestEntry.value,
            largestShiftSource: largestSource,
            largestShiftEntry: largestEntry,
            loadState: getLoadState(largestEntry.startTime),
          };
        }
      }
    }

    // Use `Object.assign()` to ensure the original metric object is returned.
    const metricWithAttribution: CLSMetricWithAttribution = Object.assign(
      metric,
      {attribution},
    );
    return metricWithAttribution;
  };

  unattributedOnCLS((metric: CLSMetric) => {
    const metricWithAttribution = attributeCLS(metric);
    onReport(metricWithAttribution);
  }, opts);
};


================================================
FILE: src/attribution/onFCP.ts
================================================
/*
 * Copyright 2022 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 {getBFCacheRestoreTime} from '../lib/bfcache.js';
import {getLoadState} from '../lib/getLoadState.js';
import {getNavigationEntry} from '../lib/getNavigationEntry.js';
import {onFCP as unattributedOnFCP} from '../onFCP.js';
import {
  FCPAttribution,
  FCPMetric,
  FCPMetricWithAttribution,
  AttributionReportOpts,
} from '../types.js';

const attributeFCP = (metric: FCPMetric): FCPMetricWithAttribution => {
  // Use a default object if no other attribution has been set.
  let attribution: FCPAttribution = {
    timeToFirstByte: 0,
    firstByteToFCP: metric.value,
    loadState: getLoadState(getBFCacheRestoreTime()),
  };

  if (metric.entries.length) {
    const navigationEntry = getNavigationEntry();
    const fcpEntry = metric.entries.at(-1);

    if (navigationEntry) {
      const activationStart = navigationEntry.activationStart || 0;
      const ttfb = Math.max(0, navigationEntry.responseStart - activationStart);

      attribution = {
        timeToFirstByte: ttfb,
        firstByteToFCP: metric.value - ttfb,
        loadState: getLoadState(metric.entries[0].startTime),
        navigationEntry,
        fcpEntry,
      };
    }
  }

  // Use `Object.assign()` to ensure the original metric object is returned.
  const metricWithAttribution: FCPMetricWithAttribution = Object.assign(
    metric,
    {attribution},
  );
  return metricWithAttribution;
};

/**
 * Calculates the [FCP](https://web.dev/articles/fcp) value for the current page and
 * calls the `callback` function once the value is ready, along with the
 * relevant `paint` performance entry used to determine the value. The reported
 * value is a `DOMHighResTimeStamp`.
 */
export const onFCP = (
  onReport: (metric: FCPMetricWithAttribution) => void,
  opts: AttributionReportOpts = {},
) => {
  unattributedOnFCP((metric: FCPMetric) => {
    const metricWithAttribution = attributeFCP(metric);
    onReport(metricWithAttribution);
  }, opts);
};


================================================
FILE: src/attribution/onINP.ts
================================================
/*
 * Copyright 2022 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 {getLoadState} from '../lib/getLoadState.js';
import {getSelector} from '../lib/getSelector.js';
import {initUnique} from '../lib/initUnique.js';
import {InteractionManager, Interaction} from '../lib/InteractionManager.js';
import {observe} from '../lib/observe.js';
import {whenIdleOrHidden} from '../lib/whenIdleOrHidden.js';
import {onINP as unattributedOnINP} from '../onINP.js';
import {
  INPAttribution,
  INPAttributionReportOpts,
  INPMetric,
  INPMetricWithAttribution,
  INPLongestScriptSummary,
} from '../types.js';

interface pendingEntriesGroup {
  startTime: DOMHighResTimeStamp;
  processingStart: DOMHighResTimeStamp;
  processingEnd: DOMHighResTimeStamp;
  renderTime: DOMHighResTimeStamp;
  entries: PerformanceEventTiming[];
}

// The maximum number of previous frames for which data is kept.
// Storing data about previous frames is necessary to handle cases where event
// and LoAF entries are dispatched out of order, and so a buffer of previous
// frame data is needed to determine various bits of INP attribution once all
// the frame-related data has come in.
// In most cases this out-of-order data is only off by a frame or two, so
// keeping the most recent 10 should be more than sufficient.
const MAX_PENDING_FRAMES = 10;

/**
 * Calculates the [INP](https://web.dev/articles/inp) value for the current
 * page and calls the `callback` function once the value is ready, along with
 * the `event` performance entries reported for that interaction. The reported
 * value is a `DOMHighResTimeStamp`.
 *
 * A custom `durationThreshold` configuration option can optionally be passed
 * to control what `event-timing` entries are considered for INP reporting. The
 * default threshold is `40`, which means INP scores of less than 40 will not
 * be reported. To avoid reporting no interactions in these cases, the library
 * will fall back to the input delay of the first interaction. Note that this
 * will not affect your 75th percentile INP value unless that value is also
 * less than 40 (well below the recommended
 * [good](https://web.dev/articles/inp#what_is_a_good_inp_score) threshold).
 *
 * If the `reportAllChanges` configuration option is set to `true`, the
 * `callback` function will be called as soon as the value is initially
 * determined as well as any time the value changes throughout the page
 * lifespan.
 *
 * _**Important:** INP should be continually monitored for changes throughout
 * the entire lifespan of a page—including if the user returns to the page after
 * it has been hidden/backgrounded. However, since browsers often [will not fire
 * additional callbacks once the user has backgrounded a
 * page](https://developer.chrome.com/blog/page-lifecycle-api/#advice-hidden),
 * `callback` is always called when the page's visibility state changes to
 * hidden. As a result, the `callback` function might be called multiple times
 * during the same page load._
 */
export const onINP = (
  onReport: (metric: INPMetricWithAttribution) => void,
  opts: INPAttributionReportOpts = {},
) => {
  // Clone the opts object to ensure it's unique, so we can initialize a
  // single instance of the `InteractionManager` class that's shared only with
  // this function invocation and the `unattributedOnINP()` invocation below
  // (which is passed the same `opts` object).
  opts = Object.assign({}, opts);

  const interactionManager = initUnique(opts, InteractionManager);

  // A list of LoAF entries that have been dispatched and could potentially
  // intersect with the INP candidate interaction. Note that periodically this
  // list is cleaned up and entries that are known to not match INP are removed.
  let pendingLoAFs: PerformanceLongAnimationFrameTiming[] = [];

  // An array of groups of all the event timing entries that occurred within a
  // particular frame. Note that periodically this array is cleaned up and entries
  // that are known to not match INP are removed.
  let pendingEntriesGroups: pendingEntriesGroup[] = [];

  // The `processingEnd` time of most recently-processed event, chronologically.
  let latestProcessingEnd: number = 0;

  // A WeakMap to look up the event-timing-entries group of a given entry.
  // Note that this only maps from "important" entries: either the first input or
  // those with an `interactionId`.
  const entryToEntriesGroupMap: WeakMap<
    PerformanceEventTiming,
    pendingEntriesGroup
  > = new WeakMap();

  // A mapping of interactionIds to the target Node.
  const interactionTargetMap: WeakMap<Interaction, string> = new WeakMap();

  // A boolean flag indicating whether or not a cleanup task has been queued.
  let cleanupPending = false;

  /**
   * Adds new LoAF entries to the `pendingLoAFs` list.
   */
  const handleLoAFEntries = (
    entries: PerformanceLongAnimationFrameTiming[],
  ) => {
    pendingLoAFs = pendingLoAFs.concat(entries);
    queueCleanup();
  };

  const saveInteractionTarget = (interaction: Interaction) => {
    if (!interactionTargetMap.get(interaction)) {
      const node = interaction.entries[0].target;
      if (node) {
        const customTarget = opts.generateTarget?.(node) ?? getSelector(node);
        interactionTargetMap.set(interaction, customTarget);
      }
    }
  };

  /**
   * Groups entries that were presented within the same animation frame by
   * a common `renderTime`. This function works by referencing
   * `pendingEntriesGroups` and using an existing render time if one is found
   * (otherwise creating a new one). This function also adds all interaction
   * entries to an `entryToRenderTimeMap` WeakMap so that the "grouped" entries
   * can be looked up later.
   */
  const groupEntriesByRenderTime = (entry: PerformanceEventTiming) => {
    const renderTime = entry.startTime + entry.duration;
    let group;

    // Update `latestProcessingEnd` to correspond to the `processingEnd`
    // value of the most recently dispatched `event` entry.
    latestProcessingEnd = Math.max(latestProcessingEnd, entry.processingEnd);

    // Iterate over all previous render times in reverse order to find a match.
    // Go in reverse since the most likely match will be at the end.
    for (let i = pendingEntriesGroups.length - 1; i >= 0; i--) {
      const potentialGroup = pendingEntriesGroups[i];

      // If a group's render time is within 8ms of the entry's render time,
      // assume they were part of the same frame and add it to the group.
      if (Math.abs(renderTime - potentialGroup.renderTime) <= 8) {
        group = potentialGroup;
        group.startTime = Math.min(entry.startTime, group.startTime);
        group.processingStart = Math.min(
          entry.processingStart,
          group.processingStart,
        );
        group.processingEnd = Math.max(
          entry.processingEnd,
          group.processingEnd,
        );
        // For some frames there can be many event entries. In this case the
        // value of including all the processed entries versus the memory use
        // becomes questionable (see also https://crbug.com/484342204).
        // So limit to 5 (the first 4 + the last one) to cap the memory of
        // keeping the heavy PerformanceEventEntry objects around.
        if (group.entries.length >= 5) {
          group.entries.length = 5; // Shouldn't ever happen but let's be safe
          group.entries[4] = entry;
        } else {
          group.entries.push(entry);
        }

        break;
      }
    }

    // If there was no matching group, assume this is a new frame.
    if (!group) {
      group = {
        startTime: entry.startTime,
        processingStart: entry.processingStart,
        processingEnd: entry.processingEnd,
        renderTime,
        entries: [entry],
      };

      pendingEntriesGroups.push(group);
    }

    // Store the grouped render time for this entry for reference later.
    if (entry.interactionId || entry.entryType === 'first-input') {
      entryToEntriesGroupMap.set(entry, group);
    }

    queueCleanup();
  };

  const queueCleanup = () => {
    // Queue cleanup of entries that are not part of any INP candidates.
    if (!cleanupPending) {
      whenIdleOrHidden(cleanupEntries);
      cleanupPending = true;
    }
  };

  const cleanupEntries = () => {
    // Create a set of entries groups that are part of the longest
    // interactions (for faster lookup below).
    const longestInteractionGroups = new Set(
      interactionManager._longestInteractionList.map((i) => {
        return entryToEntriesGroupMap.get(i.entries[0]);
      }),
    );

    // Clean up the `pendingEntriesGroups` list so it doesn't grow endlessly.
    // Keep any groups that:
    // 1) Correspond to one of the current longest interactions, OR
    // 2) Are part of one of the most recent set of frames (which is
    //    determined by checking if the index in the group is within
    //    `MAX_PENDING_FRAMES` of the group's length).
    const minIndexToKeep = pendingEntriesGroups.length - MAX_PENDING_FRAMES;
    pendingEntriesGroups = pendingEntriesGroups.filter((group, i) => {
      // Check index first because it's faster.
      return i >= minIndexToKeep || longestInteractionGroups.has(group);
    });

    // Create a set of LoAF entries that intersect with entries in the newly
    // cleaned up `pendingEntriesGroups` (for faster lookup below).
    const intersectingLoAFs: Set<PerformanceLongAnimationFrameTiming> =
      new Set();

    for (const group of pendingEntriesGroups) {
      const loafs = getIntersectingLoAFs(group.startTime, group.processingEnd);
      for (const loaf of loafs) {
        intersectingLoAFs.add(loaf);
      }
    }

    // Clean up the `pendingLoAFs` list so it doesn't grow endlessly.
    // Keep all LoAFs that either:
    // 1) Intersect with one of the above pending entries groups, OR
    // 2) Occurred more recently than the most recently process event entry.
    pendingLoAFs = pendingLoAFs.filter((loaf) => {
      return (
        // Compare times first because it's faster.
        loaf.startTime > latestProcessingEnd || intersectingLoAFs.has(loaf)
      );
    });

    cleanupPending = false;
  };

  interactionManager._onBeforeProcessingEntry = groupEntriesByRenderTime;
  interactionManager._onAfterProcessingINPCandidate = saveInteractionTarget;

  const getIntersectingLoAFs = (
    start: DOMHighResTimeStamp,
    end: DOMHighResTimeStamp,
  ) => {
    const intersectingLoAFs: PerformanceLongAnimationFrameTiming[] = [];

    for (const loaf of pendingLoAFs) {
      // If the LoAF ends before the given start time, ignore it.
      if (loaf.startTime + loaf.duration < start) continue;

      // If the LoAF starts after the given end time, ignore it and all
      // subsequent pending LoAFs (because they're in time order).
      if (loaf.startTime > end) break;

      // Still here? If so this LoAF intersects with the interaction.
      intersectingLoAFs.push(loaf);
    }
    return intersectingLoAFs;
  };

  const attributeLoAFDetails = (attribution: INPAttribution) => {
    // If there is no LoAF data then nothing further to attribute
    if (!attribution.longAnimationFrameEntries?.length) {
      return;
    }

    const interactionTime = attribution.interactionTime;
    const inputDelay = attribution.inputDelay;
    const processingDuration = attribution.processingDuration;

    // Stats across all LoAF entries and scripts.
    let totalScriptDuration = 0;
    let totalStyleAndLayoutDuration = 0;
    let totalPaintDuration = 0;
    let longestScriptDuration = 0;
    let longestScriptEntry: PerformanceScriptTiming | undefined;
    let longestScriptSubpart: INPLongestScriptSummary['subpart'] | undefined;

    for (const loafEntry of attribution.longAnimationFrameEntries) {
      totalStyleAndLayoutDuration =
        totalStyleAndLayoutDuration +
        loafEntry.startTime +
        loafEntry.duration -
        loafEntry.styleAndLayoutStart;

      for (const script of loafEntry.scripts) {
        const scriptEndTime = script.startTime + script.duration;
        if (scriptEndTime < interactionTime) {
          continue;
        }
        const intersectingScriptDuration =
          scriptEndTime - Math.max(interactionTime, script.startTime);
        // Since forcedStyleAndLayoutDuration doesn't provide timestamps, we
        // apportion the total based on the intersectingScriptDuration. Not
        // correct depending on when it occurred, but the best we can do.
        const intersectingForceStyleAndLayoutDuration = script.duration
          ? (intersectingScriptDuration / script.duration) *
            script.forcedStyleAndLayoutDuration
          : 0;
        // For scripts we exclude forcedStyleAndLayout (same as DevTools does
        // in its summary totals) and instead include that in
        // totalStyleAndLayoutDuration
        totalScriptDuration +=
          intersectingScriptDuration - intersectingForceStyleAndLayoutDuration;
        totalStyleAndLayoutDuration += intersectingForceStyleAndLayoutDuration;

        if (intersectingScriptDuration > longestScriptDuration) {
          // Set the subpart this occurred in.
          longestScriptSubpart =
            script.startTime < interactionTime + inputDelay
              ? 'input-delay'
              : script.startTime >=
                  interactionTime + inputDelay + processingDuration
                ? 'presentation-delay'
                : 'processing-duration';

          longestScriptEntry = script;
          longestScriptDuration = intersectingScriptDuration;
        }
      }
    }

    // Calculate the totalPaintDuration from the last LoAF after
    // presentationDelay starts (where available)
    const lastLoAF = attribution.longAnimationFrameEntries.at(-1);
    const lastLoAFEndTime = lastLoAF
      ? lastLoAF.startTime + lastLoAF.duration
      : 0;
    if (lastLoAFEndTime >= interactionTime + inputDelay + processingDuration) {
      totalPaintDuration = attribution.nextPaintTime - lastLoAFEndTime;
    }

    if (longestScriptEntry && longestScriptSubpart) {
      attribution.longestScript = {
        entry: longestScriptEntry,
        subpart: longestScriptSubpart,
        intersectingDuration: longestScriptDuration,
      };
    }
    attribution.totalScriptDuration = totalScriptDuration;
    attribution.totalStyleAndLayoutDuration = totalStyleAndLayoutDuration;
    attribution.totalPaintDuration = totalPaintDuration;
    attribution.totalUnattributedDuration =
      attribution.nextPaintTime -
      interactionTime -
      totalScriptDuration -
      totalStyleAndLayoutDuration -
      totalPaintDuration;
  };

  const attributeINP = (metric: INPMetric): INPMetricWithAttribution => {
    const firstEntry = metric.entries[0];
    const group = entryToEntriesGroupMap.get(firstEntry)!;

    const processingStart = firstEntry.processingStart;

    // Due to the fact that durations can be rounded down to the nearest 8ms,
    // we have to clamp `nextPaintTime` so it doesn't appear to occur before
    // processing starts. Note: we can't use `processingEnd` since processing
    // can extend beyond the event duration in some cases (see next comment).
    const nextPaintTime = Math.max(
      firstEntry.startTime + firstEntry.duration,
      processingStart,
    );

    // For the purposes of attribution, clamp `processingEnd` to `nextPaintTime`,
    // so processing is never reported as taking longer than INP (which can
    // happen via the web APIs in the case of sync modals, e.g. `alert()`).
    // See: https://github.com/GoogleChrome/web-vitals/issues/492
    const processingEnd = Math.min(group.processingEnd, nextPaintTime);

    // Sort the entries in processing time order.
    const processedEventEntries = group.entries.sort((a, b) => {
      return a.processingStart - b.processingStart;
    });

    const longAnimationFrameEntries: PerformanceLongAnimationFrameTiming[] =
      getIntersectingLoAFs(firstEntry.startTime, processingEnd);

    const interaction = interactionManager._longestInteractionMap.get(
      firstEntry.interactionId,
    );

    const attribution: INPAttribution = {
      // TS flags the next line because `interactionTargetMap.get()` might
      // return `undefined`, but we ignore this assuming the user knows what
      // they are doing.
      interactionTarget: interactionTargetMap.get(interaction!)!,
      interactionType: firstEntry.name.startsWith('key')
        ? 'keyboard'
        : 'pointer',
      interactionTime: firstEntry.startTime,
      nextPaintTime: nextPaintTime,
      processedEventEntries: processedEventEntries,
      longAnimationFrameEntries: longAnimationFrameEntries,
      inputDelay: processingStart - firstEntry.startTime,
      processingDuration: processingEnd - processingStart,
      presentationDelay: nextPaintTime - processingEnd,
      loadState: getLoadState(firstEntry.startTime),
      longestScript: undefined,
      totalScriptDuration: undefined,
      totalStyleAndLayoutDuration: undefined,
      totalPaintDuration: undefined,
      totalUnattributedDuration: undefined,
    };

    attributeLoAFDetails(attribution);

    // Use `Object.assign()` to ensure the original metric object is returned.
    const metricWithAttribution: INPMetricWithAttribution = Object.assign(
      metric,
      {attribution},
    );
    return metricWithAttribution;
  };

  // Start observing LoAF entries for attribution.
  observe('long-animation-frame', handleLoAFEntries);

  unattributedOnINP((metric: INPMetric) => {
    const metricWithAttribution = attributeINP(metric);
    onReport(metricWithAttribution);
  }, opts);
};


================================================
FILE: src/attribution/onLCP.ts
================================================
/*
 * Copyright 2022 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 {getNavigationEntry} from '../lib/getNavigationEntry.js';
import {getSelector} from '../lib/getSelector.js';
import {initUnique} from '../lib/initUnique.js';
import {LCPEntryManager} from '../lib/LCPEntryManager.js';
import {onLCP as unattributedOnLCP} from '../onLCP.js';
import {
  LCPAttribution,
  LCPMetric,
  LCPMetricWithAttribution,
  AttributionReportOpts,
} from '../types.js';

/**
 * Calculates the [LCP](https://web.dev/articles/lcp) value for the current page and
 * calls the `callback` function once the value is ready (along with the
 * relevant `largest-contentful-paint` performance entry used to determine the
 * value). The reported value is a `DOMHighResTimeStamp`.
 *
 * If the `reportAllChanges` configuration option is set to `true`, the
 * `callback` function will be called any time a new `largest-contentful-paint`
 * performance entry is dispatched, or once the final value of the metric has
 * been determined.
 */
export const onLCP = (
  onReport: (metric: LCPMetricWithAttribution) => void,
  opts: AttributionReportOpts = {},
) => {
  // Clone the opts object to ensure it's unique, so we can initialize a
  // single instance of the `LCPEntryManager` class that's shared only with
  // this function invocation and the `unattributedOnLCP()` invocation below
  // (which is passed the same `opts` object).
  opts = Object.assign({}, opts);

  const lcpEntryManager = initUnique(opts, LCPEntryManager);
  const lcpTargetMap: WeakMap<LargestContentfulPaint, string> = new WeakMap();

  lcpEntryManager._onBeforeProcessingEntry = (
    entry: LargestContentfulPaint,
  ) => {
    const node = entry.element;
    if (node) {
      const customTarget = opts.generateTarget?.(node) ?? getSelector(node);
      lcpTargetMap.set(entry, customTarget);
    } else if (entry.id) {
      // Use the LargestContentfulPaint.id property when the element has been
      // removed from the DOM (and so node is null), but still has an ID.
      lcpTargetMap.set(entry, `#${entry.id}`);
    }
  };

  const attributeLCP = (metric: LCPMetric): LCPMetricWithAttribution => {
    // Use a default object if no other attribution has been set.
    let attribution: LCPAttribution = {
      timeToFirstByte: 0,
      resourceLoadDelay: 0,
      resourceLoadDuration: 0,
      elementRenderDelay: metric.value,
    };

    if (metric.entries.length) {
      // The `metric.entries.length` check ensures there will be an entry.
      const lcpEntry = metric.entries.at(-1)!;
      const lcpResourceEntry =
        lcpEntry.url &&
        performance
          .getEntriesByType('resource')
          .find((e) => e.name === lcpEntry.url);

      attribution.target = lcpTargetMap.get(lcpEntry);
      attribution.lcpEntry = lcpEntry;
      // Only attribute the URL and resource entry if they exist.
      if (lcpEntry.url) {
        attribution.url = lcpEntry.url;
      }
      if (lcpResourceEntry) {
        attribution.lcpResourceEntry = lcpResourceEntry;
      }

      // Get subparts from navigation entry. Do this last as occasionally
      // Safari seems to fail to find a navigation entry.
      const navigationEntry = getNavigationEntry();
      if (navigationEntry) {
        const activationStart = navigationEntry.activationStart || 0;

        const ttfb = Math.max(
          0,
          navigationEntry.responseStart - activationStart,
        );

        const lcpRequestStart = Math.max(
          ttfb,
          // Prefer `requestStart` (if TOA is set), otherwise use `startTime`.
          lcpResourceEntry
            ? (lcpResourceEntry.requestStart || lcpResourceEntry.startTime) -
                activationStart
            : 0,
        );
        const lcpResponseEnd = Math.min(
          // Cap at LCP time (videos continue downloading after LCP for example)
          metric.value,
          Math.max(
            lcpRequestStart,
            lcpResourceEntry
              ? lcpResourceEntry.responseEnd - activationStart
              : 0,
          ),
        );

        attribution = {
          ...attribution,
          timeToFirstByte: ttfb,
          resourceLoadDelay: lcpRequestStart - ttfb,
          resourceLoadDuration: lcpResponseEnd - lcpRequestStart,
          elementRenderDelay: metric.value - lcpResponseEnd,
          navigationEntry,
        };
      }
    }

    // Use `Object.assign()` to ensure the original metric object is returned.
    const metricWithAttribution: LCPMetricWithAttribution = Object.assign(
      metric,
      {attribution},
    );
    return metricWithAttribution;
  };

  unattributedOnLCP((metric: LCPMetric) => {
    const metricWithAttribution = attributeLCP(metric);
    onReport(metricWithAttribution);
  }, opts);
};


================================================
FILE: src/attribution/onTTFB.ts
================================================
/*
 * Copyright 2022 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 {onTTFB as unattributedOnTTFB} from '../onTTFB.js';
import {
  TTFBMetric,
  TTFBMetricWithAttribution,
  AttributionReportOpts,
  TTFBAttribution,
} from '../types.js';

const attributeTTFB = (metric: TTFBMetric): TTFBMetricWithAttribution => {
  // Use a default object if no other attribution has been set.
  let attribution: TTFBAttribution = {
    waitingDuration: 0,
    cacheDuration: 0,
    dnsDuration: 0,
    connectionDuration: 0,
    requestDuration: 0,
  };

  if (metric.entries.length) {
    const navigationEntry = metric.entries[0];
    const activationStart = navigationEntry.activationStart || 0;

    // Measure from workerStart or fetchStart so any service worker startup
    // time is included in cacheDuration (which also includes other sw time
    // anyway, that cannot be accurately split out cross-browser).
    const waitEnd = Math.max(
      (navigationEntry.workerStart || navigationEntry.fetchStart) -
        activationStart,
      0,
    );
    const dnsStart = Math.max(
      navigationEntry.domainLookupStart - activationStart,
      0,
    );
    const connectStart = Math.max(
      navigationEntry.connectStart - activationStart,
      0,
    );
    const connectEnd = Math.max(
      navigationEntry.connectEnd - activationStart,
      0,
    );

    attribution = {
      waitingDuration: waitEnd,
      cacheDuration: dnsStart - waitEnd,
      // dnsEnd usually equals connectStart but use connectStart over dnsEnd
      // for dnsDuration in case there ever is a gap.
      dnsDuration: connectStart - dnsStart,
      connectionDuration: connectEnd - connectStart,
      // There is often a gap between connectEnd and requestStart. Attribute
      // that to requestDuration so connectionDuration remains 0 for
      // service worker controlled requests were connectStart and connectEnd
      // are the same.
      requestDuration: metric.value - connectEnd,
      navigationEntry: navigationEntry,
    };
  }

  // Use `Object.assign()` to ensure the original metric object is returned.
  const metricWithAttribution: TTFBMetricWithAttribution = Object.assign(
    metric,
    {attribution},
  );
  return metricWithAttribution;
};

/**
 * Calculates the [TTFB](https://web.dev/articles/ttfb) value for the
 * current page and calls the `callback` function once the page has loaded,
 * along with the relevant `navigation` performance entry used to determine the
 * value. The reported value is a `DOMHighResTimeStamp`.
 *
 * Note, this function waits until after the page is loaded to call `callback`
 * in order to ensure all properties of the `navigation` entry are populated.
 * This is useful if you want to report on other metrics exposed by the
 * [Navigation Timing API](https://w3c.github.io/navigation-timing/). For
 * example, the TTFB metric starts from the page's [time
 * origin](https://www.w3.org/TR/hr-time-2/#sec-time-origin), which means it
 * includes time spent on DNS lookup, connection negotiation, network latency,
 * and server processing time.
 */
export const onTTFB = (
  onReport: (metric: TTFBMetricWithAttribution) => void,
  opts: AttributionReportOpts = {},
) => {
  unattributedOnTTFB((metric: TTFBMetric) => {
    const metricWithAttribution = attributeTTFB(metric);
    onReport(metricWithAttribution);
  }, opts);
};


================================================
FILE: src/index.ts
================================================
/*
 * Copyright 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.
 */

export {onCLS, CLSThresholds} from './onCLS.js';
export {onFCP, FCPThresholds} from './onFCP.js';
export {onINP, INPThresholds} from './onINP.js';
export {onLCP, LCPThresholds} from './onLCP.js';
export {onTTFB, TTFBThresholds} from './onTTFB.js';

export * from './types.js';


================================================
FILE: src/lib/InteractionManager.ts
================================================
/*
 * Copyright 2024 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 {getInteractionCount} from './polyfills/interactionCountPolyfill.js';

export interface Interaction {
  _latency: number;
  // While the `id` and `entries` properties are also internal and could be
  // mangled by prefixing with an underscore, since they correspond to public
  // symbols there is no need to mangle them as the library will compress
  // better if we reuse the existing names.
  id: number;
  entries: PerformanceEventTiming[];
}

// To prevent unnecessary memory usage on pages with lots of interactions,
// store at most 10 of the longest interactions to consider as INP candidates.
const MAX_INTERACTIONS_TO_CONSIDER = 10;

// Used to store the interaction count after a bfcache restore, since p98
// interaction latencies should only consider the current navigation.
let prevInteractionCount = 0;

/**
 * Returns the interaction count since the last bfcache restore (or for the
 * full page lifecycle if there were no bfcache restores).
 */
const getInteractionCountForNavigation = () => {
  return getInteractionCount() - prevInteractionCount;
};

export class InteractionManager {
  /**
   * A list of longest interactions on the page (by latency) sorted so the
   * longest one is first. The list is at most MAX_INTERACTIONS_TO_CONSIDER
   * long.
   */
  _longestInteractionList: Interaction[] = [];

  /**
   * A mapping of longest interactions by their interaction ID.
   * This is used for faster lookup.
   */
  _longestInteractionMap: Map<number, Interaction> = new Map();

  _onBeforeProcessingEntry?: (entry: PerformanceEventTiming) => void;

  _onAfterProcessingINPCandidate?: (interaction: Interaction) => void;

  _resetInteractions() {
    prevInteractionCount = getInteractionCount();
    this._longestInteractionList.length = 0;
    this._longestInteractionMap.clear();
  }

  /**
   * Returns the estimated p98 longest interaction based on the stored
   * interaction candidates and the interaction count for the current page.
   */
  _estimateP98LongestInteraction() {
    const candidateInteractionIndex = Math.min(
      this._longestInteractionList.length - 1,
      Math.floor(getInteractionCountForNavigation() / 50),
    );

    return this._longestInteractionList[candidateInteractionIndex];
  }

  /**
   * Takes a performance entry and adds it to the list of worst interactions
   * if its duration is long enough to make it among the worst. If the
   * entry is part of an existing interaction, it is merged and the latency
   * and entries list is updated as needed.
   */
  _processEntry(entry: PerformanceEventTiming) {
    this._onBeforeProcessingEntry?.(entry);

    // Skip further processing for entries that cannot be INP candidates.
    if (!(entry.interactionId || entry.entryType === 'first-input')) return;

    // The least-long of the 10 longest interactions.
    const minLongestInteraction = this._longestInteractionList.at(-1);

    let interaction = this._longestInteractionMap.get(entry.interactionId!);

    // Only process the entry if it's possibly one of the ten longest,
    // or if it's part of an existing interaction.
    if (
      interaction ||
      this._longestInteractionList.length < MAX_INTERACTIONS_TO_CONSIDER ||
      // If the above conditions are false, `minLongestInteraction` will be set.
      entry.duration > minLongestInteraction!._latency
    ) {
      // If the interaction already exists, update it. Otherwise create one.
      if (interaction) {
        // If the new entry has a longer duration, replace the old entries,
        // otherwise add to the array.
        if (entry.duration > interaction._latency) {
          interaction.entries = [entry];
          interaction._latency = entry.duration;
        } else if (
          entry.duration === interaction._latency &&
          entry.startTime === interaction.entries[0].startTime
        ) {
          interaction.entries.push(entry);
        }
      } else {
        interaction = {
          id: entry.interactionId!,
          entries: [entry],
          _latency: entry.duration,
        };
        this._longestInteractionMap.set(interaction.id, interaction);
        this._longestInteractionList.push(interaction);
      }

      // Sort the entries by latency (descending) and keep only the top ten.
      this._longestInteractionList.sort((a, b) => b._latency - a._latency);
      if (this._longestInteractionList.length > MAX_INTERACTIONS_TO_CONSIDER) {
        const removedInteractions = this._longestInteractionList.splice(
          MAX_INTERACTIONS_TO_CONSIDER,
        );

        for (const interaction of removedInteractions) {
          this._longestInteractionMap.delete(interaction.id);
        }
      }

      // Call any post-processing on the interaction
      this._onAfterProcessingINPCandidate?.(interaction);
    }
  }
}


================================================
FILE: src/lib/LCPEntryManager.ts
================================================
/*
 * Copyright 2024 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.
 */

export class LCPEntryManager {
  _onBeforeProcessingEntry?: (entry: LargestContentfulPaint) => void;

  _processEntry(entry: LargestContentfulPaint) {
    this._onBeforeProcessingEntry?.(entry);
  }
}


================================================
FILE: src/lib/LayoutShiftManager.ts
================================================
/*
 * Copyright 2024 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.
 */

export class LayoutShiftManager {
  _onAfterProcessingUnexpectedShift?: (entry: LayoutShift) => void;

  _sessionValue = 0;
  _sessionEntries: LayoutShift[] = [];

  _processEntry(entry: LayoutShift) {
    // Only count layout shifts without recent user input.
    if (entry.hadRecentInput) return;

    const firstSessionEntry = this._sessionEntries[0];
    const lastSessionEntry = this._sessionEntries.at(-1);

    // If the entry occurred less than 1 second after the previous entry
    // and less than 5 seconds after the first entry in the session,
    // include the entry in the current session. Otherwise, start a new
    // session.
    if (
      this._sessionValue &&
      firstSessionEntry &&
      lastSessionEntry &&
      entry.startTime - lastSessionEntry.startTime < 1000 &&
      entry.startTime - firstSessionEntry.startTime < 5000
    ) {
      this._sessionValue += entry.value;
      this._sessionEntries.push(entry);
    } else {
      this._sessionValue = entry.value;
      this._sessionEntries = [entry];
    }

    this._onAfterProcessingUnexpectedShift?.(entry);
  }
}


================================================
FILE: src/lib/bfcache.ts
================================================
/*
 * Copyright 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.
 */

interface onBFCacheRestoreCallback {
  (event: PageTransitionEvent): void;
}

let bfcacheRestoreTime = -1;

export const getBFCacheRestoreTime = () => bfcacheRestoreTime;

export const onBFCacheRestore = (cb: onBFCacheRestoreCallback) => {
  addEventListener(
    'pageshow',
    (event) => {
      if (event.persisted) {
        bfcacheRestoreTime = event.timeStamp;
        cb(event);
      }
    },
    true,
  );
};


================================================
FILE: src/lib/bindReporter.ts
================================================
/*
 * Copyright 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 {MetricType, MetricRatingThresholds} from '../types.js';

const getRating = (
  value: number,
  thresholds: MetricRatingThresholds,
): MetricType['rating'] => {
  if (value > thresholds[1]) {
    return 'poor';
  }
  if (value > thresholds[0]) {
    return 'needs-improvement';
  }
  return 'good';
};

export const bindReporter = <MetricName extends MetricType['name']>(
  callback: (metric: Extract<MetricType, {name: MetricName}>) => void,
  metric: Extract<MetricType, {name: MetricName}>,
  thresholds: MetricRatingThresholds,
  reportAllChanges?: boolean,
) => {
  let prevValue: number;
  let delta: number;
  return (forceReport?: boolean) => {
    if (metric.value >= 0) {
      if (forceReport || reportAllChanges) {
        delta = metric.value - (prevValue ?? 0);

        // Report the metric if there's a non-zero delta or if no previous
        // value exists (which can happen in the case of the document becoming
        // hidden when the metric value is 0).
        // See: https://github.com/GoogleChrome/web-vitals/issues/14
        if (delta || prevValue === undefined) {
          prevValue = metric.value;
          metric.delta = delta;
          metric.rating = getRating(metric.value, thresholds);
          callback(metric);
        }
      }
    }
  };
};


================================================
FILE: src/lib/doubleRAF.ts
================================================
/*
 * Copyright 2022 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.
 */

export const doubleRAF = (cb: () => unknown) => {
  requestAnimationFrame(() => requestAnimationFrame(() => cb()));
};


================================================
FILE: src/lib/generateUniqueID.ts
================================================
/*
 * Copyright 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.
 */

/**
 * Performantly generate a unique, 30-char string by combining a version
 * number, the current timestamp with a 13-digit number integer.
 * @return {string}
 */
export const generateUniqueID = () => {
  return `v5-${Date.now()}-${Math.floor(Math.random() * (9e12 - 1)) + 1e12}`;
};


================================================
FILE: src/lib/getActivationStart.ts
================================================
/*
 * Copyright 2022 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 {getNavigationEntry} from './getNavigationEntry.js';

export const getActivationStart = (): number => {
  const navEntry = getNavigationEntry();
  return navEntry?.activationStart ?? 0;
};


================================================
FILE: src/lib/getLoadState.ts
================================================
/*
 * Copyright 2022 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 {getNavigationEntry} from './getNavigationEntry.js';
import {LoadState} from '../types.js';

export const getLoadState = (timestamp: number): LoadState => {
  if (document.readyState === 'loading') {
    // If the `readyState` is 'loading' there's no need to look at timestamps
    // since the timestamp has to be the current time or earlier.
    return 'loading';
  } else {
    const navigationEntry = getNavigationEntry();
    if (navigationEntry) {
      if (timestamp < navigationEntry.domInteractive) {
        return 'loading';
      } else if (
        navigationEntry.domContentLoadedEventStart === 0 ||
        timestamp < navigationEntry.domContentLoadedEventStart
      ) {
        // If the `domContentLoadedEventStart` timestamp has not yet been
        // set, or if the given timestamp is less than that value.
        return 'dom-interactive';
      } else if (
        navigationEntry.domComplete === 0 ||
        timestamp < navigationEntry.domComplete
      ) {
        // If the `domComplete` timestamp has not yet been
        // set, or if the given timestamp is less than that value.
        return 'dom-content-loaded';
      }
    }
  }
  // If any of the above fail, default to loaded. This could really only
  // happy if the browser doesn't support the performance timeline, which
  // most likely means this code would never run anyway.
  return 'complete';
};


================================================
FILE: src/lib/getNavigationEntry.ts
================================================
/*
 * Copyright 2022 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.
 */

export const getNavigationEntry = (): PerformanceNavigationTiming | void => {
  const navigationEntry = performance.getEntriesByType('navigation')[0];

  // Check to ensure the `responseStart` property is present and valid.
  // In some cases a zero value is reported by the browser (for
  // privacy/security reasons), and in other cases (bugs) the value is
  // negative or is larger than the current page time. Ignore these cases:
  // - https://github.com/GoogleChrome/web-vitals/issues/137
  // - https://github.com/GoogleChrome/web-vitals/issues/162
  // - https://github.com/GoogleChrome/web-vitals/issues/275
  if (
    navigationEntry &&
    navigationEntry.responseStart > 0 &&
    navigationEntry.responseStart < performance.now()
  ) {
    return navigationEntry;
  }
};


================================================
FILE: src/lib/getSelector.ts
================================================
/*
 * Copyright 2022 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.
 */

const getName = (node: Node) => {
  const name = node.nodeName;
  return node.nodeType === 1
    ? name.toLowerCase()
    : name.toUpperCase().replace(/^#/, '');
};

const MAX_LEN = 100;

export const getSelector = (node: Node | null) => {
  let sel = '';

  try {
    while (node?.nodeType !== 9) {
      const el: Element = node as Element;
      const part = el.id
        ? '#' + el.id
        : [getName(el), ...Array.from(el.classList).sort()].join('.');
      if (sel.length + part.length > MAX_LEN - 1) {
        return sel || part;
      }
      sel = sel ? part + '>' + sel : part;
      if (el.id) {
        break;
      }
      node = el.parentNode;
    }
  } catch {
    // Do nothing...
  }
  return sel;
};


================================================
FILE: src/lib/getVisibilityWatcher.ts
================================================
/*
 * Copyright 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 {onBFCacheRestore} from './bfcache.js';
import {getActivationStart} from './getActivationStart.js';

let firstHiddenTime = -1;
const onHiddenFunctions: Set<() => void> = new Set();

const initHiddenTime = () => {
  // If the document is hidden when this code runs, assume it was always
  // hidden and the page was loaded in the background, with the one exception
  // that visibility state is always 'hidden' during prerendering, so we have
  // to ignore that case until prerendering finishes (see: `prerenderingchange`
  // event logic below).
  return document.visibilityState === 'hidden' && !document.prerendering
    ? 0
    : Infinity;
};

const onVisibilityUpdate = (event: Event) => {
  // Handle changes to hidden state
  if (document.visibilityState === 'hidden') {
    if (event.type === 'visibilitychange') {
      for (const onHiddenFunction of onHiddenFunctions) {
        onHiddenFunction();
      }
    }

    // If the document is 'hidden' and no previous hidden timestamp has been
    // set (so is infinity), update it based on the current event data.
    if (!isFinite(firstHiddenTime)) {
      // If the event is a 'visibilitychange' event, it means the page was
      // visible prior to this change, so the event timestamp is the first
      // hidden time.
      // However, if the event is not a 'visibilitychange' event, then it must
      // be a 'prerenderingchange' event, and the fact that the document is
      // still 'hidden' from the above check means the tab was activated
      // in a background state and so has always been hidden.
      firstHiddenTime = event.type === 'visibilitychange' ? event.timeStamp : 0;

      // We no longer need the `prerenderingchange` event listener now we've
      // set an initial init time so remove that
      // (we'll keep the visibilitychange one for onHiddenFunction above)
      removeEventListener('prerenderingchange', onVisibilityUpdate, true);
    }
  }
};

export const getVisibilityWatcher = () => {
  if (firstHiddenTime < 0) {
    // Check if we have a previous hidden `visibility-state` performance entry.
    const activationStart = getActivationStart();
    /* eslint-disable indent */
    const firstVisibilityStateHiddenTime = !document.prerendering
      ? globalThis.performance
          .getEntriesByType('visibility-state')
          .find((e) => e.name === 'hidden' && e.startTime >= activationStart)
          ?.startTime
      : undefined;
    /* eslint-enable indent */

    // Prefer that, but if it's not available and the document is hidden when
    // this code runs, assume it was hidden since navigation start. This isn't
    // a perfect heuristic, but it's the best we can do until the
    // `visibility-state` performance entry becomes available in all browsers.
    firstHiddenTime = firstVisibilityStateHiddenTime ?? initHiddenTime();

    // Listen for visibility changes so we can handle things like bfcache
    // restores and/or prerender without having to examine individual
    // timestamps in detail and also for onHidden function calls.
    addEventListener('visibilitychange', onVisibilityUpdate, true);
    // IMPORTANT: when a page is prerendering, its `visibilityState` is
    // 'hidden', so in order to account for cases where this module checks for
    // visibility during prerendering, an additional check after prerendering
    // completes is also required.
    addEventListener('prerenderingchange', onVisibilityUpdate, true);

    // Reset the time on bfcache restores.
    onBFCacheRestore(() => {
      // Schedule a task in order to track the `visibilityState` once it's
      // had an opportunity to change to visible in all browsers.
      // https://bugs.chromium.org/p/chromium/issues/detail?id=1133363
      setTimeout(() => {
        firstHiddenTime = initHiddenTime();
      });
    });
  }
  return {
    get firstHiddenTime() {
      return firstHiddenTime;
    },
    onHidden(cb: () => void) {
      onHiddenFunctions.add(cb);
    },
  };
};


================================================
FILE: src/lib/initMetric.ts
================================================
/*
 * Copyright 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 {getBFCacheRestoreTime} from './bfcache.js';
import {generateUniqueID} from './generateUniqueID.js';
import {getActivationStart} from './getActivationStart.js';
import {getNavigationEntry} from './getNavigationEntry.js';
import {MetricType} from '../types.js';

export const initMetric = <MetricName extends MetricType['name']>(
  name: MetricName,
  value: number = -1,
) => {
  const navEntry = getNavigationEntry();
  let navigationType: MetricType['navigationType'] = 'navigate';

  if (getBFCacheRestoreTime() >= 0) {
    navigationType = 'back-forward-cache';
  } else if (navEntry) {
    if (document.prerendering || getActivationStart() > 0) {
      navigationType = 'prerender';
    } else if (document.wasDiscarded) {
      navigationType = 'restore';
    } else if (navEntry.type) {
      navigationType = navEntry.type.replace(
        /_/g,
        '-',
      ) as MetricType['navigationType'];
    }
  }

  // Use `entries` type specific for the metric.
  const entries: Extract<MetricType, {name: MetricName}>['entries'] = [];

  return {
    name,
    value,
    rating: 'good' as const, // If needed, will be updated when reported. `const` to keep the type from widening to `string`.
    delta: 0,
    entries,
    id: generateUniqueID(),
    navigationType,
  };
};


================================================
FILE: src/lib/initUnique.ts
================================================
/*
 * Copyright 2024 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.
 */

const instanceMap: WeakMap<object, unknown> = new WeakMap();

/**
 * A function that accepts and identity object and a class object and returns
 * either a new instance of that class or an existing instance, if the
 * identity object was previously used.
 */
export function initUnique<T>(identityObj: object, ClassObj: new () => T): T {
  if (!instanceMap.get(identityObj)) {
    instanceMap.set(identityObj, new ClassObj());
  }
  return instanceMap.get(identityObj)! as T;
}


================================================
FILE: src/lib/observe.ts
================================================
/*
 * Copyright 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.
 */

interface PerformanceEntryMap {
  'event': PerformanceEventTiming[];
  'first-input': PerformanceEventTiming[];
  'layout-shift': LayoutShift[];
  'largest-contentful-paint': LargestContentfulPaint[];
  'long-animation-frame': PerformanceLongAnimationFrameTiming[];
  'paint': PerformancePaintTiming[];
  'navigation': PerformanceNavigationTiming[];
  'resource': PerformanceResourceTiming[];
}

/**
 * Takes a performance entry type and a callback function, and creates a
 * `PerformanceObserver` instance that will observe the specified entry type
 * with buffering enabled and call the callback _for each entry_.
 *
 * This function also feature-detects entry support and wraps the logic in a
 * try/catch to avoid errors in unsupporting browsers.
 */
export const observe = <K extends keyof PerformanceEntryMap>(
  type: K,
  callback: (entries: PerformanceEntryMap[K]) => void,
  opts: PerformanceObserverInit = {},
): PerformanceObserver | undefined => {
  try {
    if (PerformanceObserver.supportedEntryTypes.includes(type)) {
      const po = new PerformanceObserver((list) => {
        // Delay by a microtask to workaround a bug in Safari where the
        // callback is invoked immediately, rather than in a separate task.
        // See: https://github.com/GoogleChrome/web-vitals/issues/277
        queueMicrotask(() => {
          callback(list.getEntries() as PerformanceEntryMap[K]);
        });
      });
      po.observe({type, buffered: true, ...opts});
      return po;
    }
  } catch {
    // Do nothing.
  }
  return;
};


================================================
FILE: src/lib/polyfills/getFirstHiddenTimePolyfill.ts
================================================
/*
 * Copyright 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.
 */

let firstHiddenTime = document.visibilityState === 'hidden' ? 0 : Infinity;

const onVisibilityChange = (event: Event) => {
  if (document.visibilityState === 'hidden') {
    firstHiddenTime = event.timeStamp;
    removeEventListener('visibilitychange', onVisibilityChange, true);
  }
};

// Note: do not add event listeners unconditionally (outside of polyfills).
addEventListener('visibilitychange', onVisibilityChange, true);

export const getFirstHiddenTime = () => firstHiddenTime;


================================================
FILE: src/lib/polyfills/interactionCountPolyfill.ts
================================================
/*
 * Copyright 2022 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 {observe} from '../observe.js';

declare global {
  interface Performance {
    readonly interactionCount: number;
  }
}

let interactionCountEstimate = 0;
let minKnownInteractionId = Infinity;
let maxKnownInteractionId = 0;

const updateEstimate = (entries: PerformanceEventTiming[]) => {
  for (const entry of entries) {
    if (entry.interactionId) {
      minKnownInteractionId = Math.min(
        minKnownInteractionId,
        entry.interactionId,
      );
      maxKnownInteractionId = Math.max(
        maxKnownInteractionId,
        entry.interactionId,
      );

      interactionCountEstimate = maxKnownInteractionId
        ? (maxKnownInteractionId - minKnownInteractionId) / 7 + 1
        : 0;
    }
  }
};

let po: PerformanceObserver | undefined;

/**
 * Returns the `interactionCount` value using the native API (if available)
 * or the polyfill estimate in this module.
 */
export const getInteractionCount = () => {
  return po ? interactionCountEstimate : (performance.interactionCount ?? 0);
};

/**
 * Feature detects native support or initializes the polyfill if needed.
 */
export const initInteractionCountPolyfill = () => {
  if ('interactionCount' in performance || po) return;

  po = observe('event', updateEstimate, {
    type: 'event',
    buffered: true,
    durationThreshold: 0,
  } as PerformanceObserverInit);
};


================================================
FILE: src/lib/runOnce.ts
================================================
/*
 * Copyright 2022 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.
 */

export const runOnce = (cb: () => void) => {
  let called = false;
  return () => {
    if (!called) {
      cb();
      called = true;
    }
  };
};


================================================
FILE: src/lib/whenActivated.ts
================================================
/*
 * Copyright 2022 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.
 */

export const whenActivated = (callback: () => void) => {
  if (document.prerendering) {
    addEventListener('prerenderingchange', () => callback(), true);
  } else {
    callback();
  }
};


================================================
FILE: src/lib/whenIdleOrHidden.ts
================================================
/*
 * Copyright 2024 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 {runOnce} from './runOnce.js';

/**
 * Runs the passed callback during the next idle period, or immediately
 * if the browser's visibility state is (or becomes) hidden.
 */
export const whenIdleOrHidden = (cb: () => void) => {
  const rIC = globalThis.requestIdleCallback || setTimeout;
  const cIC = globalThis.cancelIdleCallback || clearTimeout;

  // If the document is hidden, run the callback immediately, otherwise
  // race an idle callback with the next `visibilitychange` event.
  if (document.visibilityState === 'hidden') {
    cb();
  } else {
    const wrappedCb = runOnce(cb);

    let idleHandle = -1;
    const onHidden = () => {
      cIC(idleHandle);
      wrappedCb();
    };

    addEventListener('visibilitychange', onHidden, {once: true, capture: true});
    idleHandle = rIC(() => {
      removeEventListener('visibilitychange', onHidden, {capture: true});
      wrappedCb();
    });
  }
};


================================================
FILE: src/onCLS.ts
================================================
/*
 * Copyright 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 {onBFCacheRestore} from './lib/bfcache.js';
import {bindReporter} from './lib/bindReporter.js';
import {doubleRAF} from './lib/doubleRAF.js';
import {initMetric} from './lib/initMetric.js';
import {initUnique} from './lib/initUnique.js';
import {LayoutShiftManager} from './lib/LayoutShiftManager.js';
import {observe} from './lib/observe.js';
import {runOnce} from './lib/runOnce.js';
import {onFCP} from './onFCP.js';
import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js';
import {CLSMetric, MetricRatingThresholds, ReportOpts} from './types.js';

/** Thresholds for CLS. See https://web.dev/articles/cls#what_is_a_good_cls_score */
export const CLSThresholds: MetricRatingThresholds = [0.1, 0.25];

/**
 * Calculates the [CLS](https://web.dev/articles/cls) value for the current page and
 * calls the `callback` function once the value is ready to be reported, along
 * with all `layout-shift` performance entries that were used in the metric
 * value calculation. The reported value is a `double` (corresponding to a
 * [layout shift score](https://web.dev/articles/cls#layout_shift_score)).
 *
 * If the `reportAllChanges` configuration option is set to `true`, the
 * `callback` function will be called as soon as the value is initially
 * determined as well as any time the value changes throughout the page
 * lifespan.
 *
 * _**Important:** CLS should be continually monitored for changes throughout
 * the entire lifespan of a page—including if the user returns to the page after
 * it's been hidden/backgrounded. However, since browsers often [will not fire
 * additional callbacks once the user has backgrounded a
 * page](https://developer.chrome.com/blog/page-lifecycle-api/#advice-hidden),
 * `callback` is always called when the page's visibility state changes to
 * hidden. As a result, the `callback` function might be called multiple times
 * during the same page load._
 */
export const onCLS = (
  onReport: (metric: CLSMetric) => void,
  opts: ReportOpts = {},
) => {
  const visibilityWatcher = getVisibilityWatcher();
  // Start monitoring FCP so we can only report CLS if FCP is also reported.
  // Note: this is done to match the current behavior of CrUX.
  onFCP(
    runOnce(() => {
      let metric = initMetric('CLS', 0);
      let report: ReturnType<typeof bindReporter>;

      const layoutShiftManager = initUnique(opts, LayoutShiftManager);

      const handleEntries = (entries: LayoutShift[]) => {
        for (const entry of entries) {
          layoutShiftManager._processEntry(entry);
        }

        // If the current session value is larger than the current CLS value,
        // update CLS and the entries contributing to it.
        if (layoutShiftManager._sessionValue > metric.value) {
          metric.value = layoutShiftManager._sessionValue;
          metric.entries = layoutShiftManager._sessionEntries;
          report();
        }
      };

      const po = observe('layout-shift', handleEntries);
      if (po) {
        report = bindReporter(
          onReport,
          metric,
          CLSThresholds,
          opts!.reportAllChanges,
        );

        visibilityWatcher.onHidden(() => {
          handleEntries(po.takeRecords() as CLSMetric['entries']);
          report(true);
        });

        // Only report after a bfcache restore if the `PerformanceObserver`
        // successfully registered.
        onBFCacheRestore(() => {
          layoutShiftManager._sessionValue = 0;
          metric = initMetric('CLS', 0);
          report = bindReporter(
            onReport,
            metric,
            CLSThresholds,
            opts!.reportAllChanges,
          );

          doubleRAF(() => report());
        });

        // Queue a task to report (if nothing else triggers a report first).
        // This allows CLS to be reported as soon as FCP fires when
        // `reportAllChanges` is true.
        setTimeout(report);
      }
    }),
  );
};


================================================
FILE: src/onFCP.ts
================================================
/*
 * Copyright 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 {onBFCacheRestore} from './lib/bfcache.js';
import {bindReporter} from './lib/bindReporter.js';
import {doubleRAF} from './lib/doubleRAF.js';
import {getActivationStart} from './lib/getActivationStart.js';
import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js';
import {initMetric} from './lib/initMetric.js';
import {observe} from './lib/observe.js';
import {whenActivated} from './lib/whenActivated.js';
import {FCPMetric, MetricRatingThresholds, ReportOpts} from './types.js';

/** Thresholds for FCP. See https://web.dev/articles/fcp#what_is_a_good_fcp_score */
export const FCPThresholds: MetricRatingThresholds = [1800, 3000];

/**
 * Calculates the [FCP](https://web.dev/articles/fcp) value for the current page and
 * calls the `callback` function once the value is ready, along with the
 * relevant `paint` performance entry used to determine the value. The reported
 * value is a `DOMHighResTimeStamp`.
 */
export const onFCP = (
  onReport: (metric: FCPMetric) => void,
  opts: ReportOpts = {},
) => {
  whenActivated(() => {
    const visibilityWatcher = getVisibilityWatcher();
    let metric = initMetric('FCP');
    let report: ReturnType<typeof bindReporter>;

    const handleEntries = (entries: FCPMetric['entries']) => {
      for (const entry of entries) {
        if (entry.name === 'first-contentful-paint') {
          po!.disconnect();

          // Only report if the page wasn't hidden prior to the first paint.
          if (entry.startTime < visibilityWatcher.firstHiddenTime) {
            // The activationStart reference is used because FCP should be
            // relative to page activation rather than navigation start if the
            // page was prerendered. But in cases where `activationStart` occurs
            // after the FCP, this time should be clamped at 0.
            metric.value = Math.max(entry.startTime - getActivationStart(), 0);
            metric.entries.push(entry);
            report(true);
          }
        }
      }
    };

    const po = observe('paint', handleEntries);

    if (po) {
      report = bindReporter(
        onReport,
        metric,
        FCPThresholds,
        opts!.reportAllChanges,
      );

      // Only report after a bfcache restore if the `PerformanceObserver`
      // successfully registered or the `paint` entry exists.
      onBFCacheRestore((event) => {
        metric = initMetric('FCP');
        report = bindReporter(
          onReport,
          metric,
          FCPThresholds,
          opts!.reportAllChanges,
        );

        doubleRAF(() => {
          metric.value = performance.now() - event.timeStamp;
          report(true);
        });
      });
    }
  });
};


================================================
FILE: src/onINP.ts
================================================
/*
 * Copyright 2022 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 {onBFCacheRestore} from './lib/bfcache.js';
import {bindReporter} from './lib/bindReporter.js';
import {initMetric} from './lib/initMetric.js';
import {initUnique} from './lib/initUnique.js';
import {InteractionManager} from './lib/InteractionManager.js';
import {observe} from './lib/observe.js';
import {initInteractionCountPolyfill} from './lib/polyfills/interactionCountPolyfill.js';
import {whenActivated} from './lib/whenActivated.js';
import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js';
import {whenIdleOrHidden} from './lib/whenIdleOrHidden.js';

import {INPMetric, MetricRatingThresholds, INPReportOpts} from './types.js';

/** Thresholds for INP. See https://web.dev/articles/inp#what_is_a_good_inp_score */
export const INPThresholds: MetricRatingThresholds = [200, 500];

// The default `durationThreshold` used across this library for observing
// `event` entries via PerformanceObserver.
const DEFAULT_DURATION_THRESHOLD = 40;

/**
 * Calculates the [INP](https://web.dev/articles/inp) value for the current
 * page and calls the `callback` function once the value is ready, along with
 * the `event` performance entries reported for that interaction. The reported
 * value is a `DOMHighResTimeStamp`.
 *
 * A custom `durationThreshold` configuration option can optionally be passed
 * to control what `event-timing` entries are considered for INP reporting. The
 * default threshold is `40`, which means INP scores of less than 40 will not
 * be reported. To avoid reporting no interactions in these cases, the library
 * will fall back to the input delay of the first interaction. Note that this
 * will not affect your 75th percentile INP value unless that value is also
 * less than 40 (well below the recommended
 * [good](https://web.dev/articles/inp#what_is_a_good_inp_score) threshold).
 *
 * If the `reportAllChanges` configuration option is set to `true`, the
 * `callback` function will be called as soon as the value is initially
 * determined as well as any time the value changes throughout the page
 * lifespan.
 *
 * _**Important:** INP should be continually monitored for changes throughout
 * the entire lifespan of a page—including if the user returns to the page after
 * it's been hidden/backgrounded. However, since browsers often [will not fire
 * additional callbacks once the user has backgrounded a
 * page](https://developer.chrome.com/blog/page-lifecycle-api/#advice-hidden),
 * `callback` is always called when the page's visibility state changes to
 * hidden. As a result, the `callback` function might be called multiple times
 * during the same page load._
 */
export const onINP = (
  onReport: (metric: INPMetric) => void,
  opts: INPReportOpts = {},
) => {
  // Return if the browser doesn't support all APIs needed to measure INP.
  if (
    !(
      globalThis.PerformanceEventTiming &&
      'interactionId' in PerformanceEventTiming.prototype
    )
  ) {
    return;
  }

  const visibilityWatcher = getVisibilityWatcher();

  whenActivated(() => {
    // TODO(philipwalton): remove once the polyfill is no longer needed.
    initInteractionCountPolyfill();

    let metric = initMetric('INP');
    let report: ReturnType<typeof bindReporter>;

    const interactionManager = initUnique(opts, InteractionManager);

    const handleEntries = (entries: INPMetric['entries']) => {
      // Queue the `handleEntries()` callback in the next idle task.
      // This is needed to increase the chances that all event entries that
      // occurred between the user interaction and the next paint
      // have been dispatched. Note: there is currently an experiment
      // running in Chrome (EventTimingKeypressAndCompositionInteractionId)
      // 123+ that if rolled out fully may make this no longer necessary.
      whenIdleOrHidden(() => {
        for (const entry of entries) {
          interactionManager._processEntry(entry);
        }

        const inp = interactionManager._estimateP98LongestInteraction();

        if (inp && inp._latency !== metric.value) {
          metric.value = inp._latency;
          metric.entries = inp.entries;
          report();
        }
      });
    };

    const po = observe('event', handleEntries, {
      // Event Timing entries have their durations rounded to the nearest 8ms,
      // so a duration of 40ms would be any event that spans 2.5 or more frames
      // at 60Hz. This threshold is chosen to strike a balance between usefulness
      // and performance. Running this callback for any interaction that spans
      // just one or two frames is likely not worth the insight that could be
      // gained.
      durationThreshold: opts.durationThreshold ?? DEFAULT_DURATION_THRESHOLD,
    });

    report = bindReporter(
      onReport,
      metric,
      INPThresholds,
      opts.reportAllChanges,
    );

    if (po) {
      // Also observe entries of type `first-input`. This is useful in cases
      // where the first interaction is less than the `durationThreshold`.
      po.observe({type: 'first-input', buffered: true});

      visibilityWatcher.onHidden(() => {
        handleEntries(po.takeRecords() as INPMetric['entries']);
        report(true);
      });

      // Only report after a bfcache restore if the `PerformanceObserver`
      // successfully registered.
      onBFCacheRestore(() => {
        interactionManager._resetInteractions();

        metric = initMetric('INP');
        report = bindReporter(
          onReport,
          metric,
          INPThresholds,
          opts.reportAllChanges,
        );
      });
    }
  });
};


================================================
FILE: src/onLCP.ts
================================================
/*
 * Copyright 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 {LCPEntryManager} from './lib/LCPEntryManager.js';
import {onBFCacheRestore} from './lib/bfcache.js';
import {bindReporter} from './lib/bindReporter.js';
import {doubleRAF} from './lib/doubleRAF.js';
import {getActivationStart} from './lib/getActivationStart.js';
import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js';
import {initMetric} from './lib/initMetric.js';
import {initUnique} from './lib/initUnique.js';
import {observe} from './lib/observe.js';
import {runOnce} from './lib/runOnce.js';
import {whenActivated} from './lib/whenActivated.js';
import {whenIdleOrHidden} from './lib/whenIdleOrHidden.js';
import {LCPMetric, MetricRatingThresholds, ReportOpts} from './types.js';

/** Thresholds for LCP. See https://web.dev/articles/lcp#what_is_a_good_lcp_score */
export const LCPThresholds: MetricRatingThresholds = [2500, 4000];

/**
 * Calculates the [LCP](https://web.dev/articles/lcp) value for the current page and
 * calls the `callback` function once the value is ready (along with the
 * relevant `largest-contentful-paint` performance entry used to determine the
 * value). The reported value is a `DOMHighResTimeStamp`.
 *
 * If the `reportAllChanges` configuration option is set to `true`, the
 * `callback` function will be called any time a new `largest-contentful-paint`
 * performance entry is dispatched, or once the final value of the metric has
 * been determined.
 */
export const onLCP = (
  onReport: (metric: LCPMetric) => void,
  opts: ReportOpts = {},
) => {
  whenActivated(() => {
    const visibilityWatcher = getVisibilityWatcher();
    let metric = initMetric('LCP');
    let report: ReturnType<typeof bindReporter>;

    const lcpEntryManager = initUnique(opts, LCPEntryManager);

    const handleEntries = (entries: LCPMetric['entries']) => {
      // If reportAllChanges is set then call this function for each entry,
      // otherwise only consider the last one.
      if (!opts!.reportAllChanges) {
        entries = entries.slice(-1);
      }

      for (const entry of entries) {
        lcpEntryManager._processEntry(entry);

        // Only report if the page wasn't hidden prior to LCP.
        if (entry.startTime < visibilityWatcher.firstHiddenTime) {
          // The startTime attribute returns the value of the renderTime if it is
          // not 0, and the value of the loadTime otherwise. The activationStart
          // reference is used because LCP should be relative to page activation
          // rather than navigation start if the page was prerendered. But in cases
          // where `activationStart` occurs after the LCP, this time should be
          // clamped at 0.
          metric.value = Math.max(entry.startTime - getActivationStart(), 0);
          metric.entries = [entry];
          report();
        }
      }
    };

    const po = observe('largest-contentful-paint', handleEntries);

    if (po) {
      report = bindReporter(
        onReport,
        metric,
        LCPThresholds,
        opts!.reportAllChanges,
      );

      // Ensure this logic only runs once, since it can be triggered from
      // any of three different event listeners below.
      const stopListening = runOnce(() => {
        handleEntries(po!.takeRecords() as LCPMetric['entries']);
        po!.disconnect();
        report(true);
      });

      // Need a separate wrapper to ensure the `runOnce` function above is
      // common for all three functions
      const stopListeningWrapper = (event: Event) => {
        if (event.isTrusted) {
          // Wrap the listener in an idle callback so it's run in a separate
          // task to reduce potential INP impact.
          // https://github.com/GoogleChrome/web-vitals/issues/383
          whenIdleOrHidden(stopListening);
          removeEventListener(event.type, stopListeningWrapper, {
            capture: true,
          });
        }
      };

      // S
Download .txt
gitextract_ttgfkz8n/

├── .editorconfig
├── .github/
│   └── workflows/
│       ├── lint.yml
│       └── tests.yml
├── .gitignore
├── .husky/
│   ├── .gitignore
│   └── pre-commit
├── .nvmrc
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── attribution.d.ts
├── attribution.js
├── docs/
│   ├── upgrading-to-v4.md
│   └── upgrading-to-v5.md
├── eslint.config.js
├── package.json
├── rollup.config.js
├── src/
│   ├── attribution/
│   │   ├── index.ts
│   │   ├── onCLS.ts
│   │   ├── onFCP.ts
│   │   ├── onINP.ts
│   │   ├── onLCP.ts
│   │   └── onTTFB.ts
│   ├── index.ts
│   ├── lib/
│   │   ├── InteractionManager.ts
│   │   ├── LCPEntryManager.ts
│   │   ├── LayoutShiftManager.ts
│   │   ├── bfcache.ts
│   │   ├── bindReporter.ts
│   │   ├── doubleRAF.ts
│   │   ├── generateUniqueID.ts
│   │   ├── getActivationStart.ts
│   │   ├── getLoadState.ts
│   │   ├── getNavigationEntry.ts
│   │   ├── getSelector.ts
│   │   ├── getVisibilityWatcher.ts
│   │   ├── initMetric.ts
│   │   ├── initUnique.ts
│   │   ├── observe.ts
│   │   ├── polyfills/
│   │   │   ├── getFirstHiddenTimePolyfill.ts
│   │   │   └── interactionCountPolyfill.ts
│   │   ├── runOnce.ts
│   │   ├── whenActivated.ts
│   │   └── whenIdleOrHidden.ts
│   ├── onCLS.ts
│   ├── onFCP.ts
│   ├── onINP.ts
│   ├── onLCP.ts
│   ├── onTTFB.ts
│   ├── types/
│   │   ├── base.ts
│   │   ├── cls.ts
│   │   ├── fcp.ts
│   │   ├── inp.ts
│   │   ├── lcp.ts
│   │   └── ttfb.ts
│   └── types.ts
├── test/
│   ├── css/
│   │   └── styles.css
│   ├── e2e/
│   │   ├── onCLS-test.js
│   │   ├── onFCP-test.js
│   │   ├── onINP-test.js
│   │   ├── onLCP-test.js
│   │   └── onTTFB-test.js
│   ├── script/
│   │   ├── async.js
│   │   └── defer.js
│   ├── server.js
│   ├── tsconfig.json
│   ├── unit/
│   │   ├── attribution-test.js
│   │   ├── bindReporter-test.js
│   │   └── index-test.js
│   ├── utils/
│   │   ├── assertIsCloseTo.js
│   │   ├── beacons.js
│   │   ├── browserSupportsEntry.js
│   │   ├── domReadyState.js
│   │   ├── firstContentfulPaint.js
│   │   ├── imagesPainted.js
│   │   ├── navigateTo.js
│   │   ├── nextFrame.js
│   │   ├── stubForwardBack.js
│   │   ├── stubVisibilityChange.js
│   │   ├── waitUntilIdle.js
│   │   └── webVitalsLoaded.js
│   └── views/
│       ├── cls.njk
│       ├── fcp.njk
│       ├── inp.njk
│       ├── layout.njk
│       ├── lcp.njk
│       └── ttfb.njk
├── tsconfig.json
└── wdio.conf.js
Download .txt
SYMBOL INDEX (82 symbols across 34 files)

FILE: src/attribution/onINP.ts
  type pendingEntriesGroup (line 32) | interface pendingEntriesGroup {
  constant MAX_PENDING_FRAMES (line 47) | const MAX_PENDING_FRAMES = 10;

FILE: src/lib/InteractionManager.ts
  type Interaction (line 19) | interface Interaction {
  constant MAX_INTERACTIONS_TO_CONSIDER (line 31) | const MAX_INTERACTIONS_TO_CONSIDER = 10;
  class InteractionManager (line 45) | class InteractionManager {
    method _resetInteractions (line 63) | _resetInteractions() {
    method _estimateP98LongestInteraction (line 73) | _estimateP98LongestInteraction() {
    method _processEntry (line 88) | _processEntry(entry: PerformanceEventTiming) {

FILE: src/lib/LCPEntryManager.ts
  class LCPEntryManager (line 17) | class LCPEntryManager {
    method _processEntry (line 20) | _processEntry(entry: LargestContentfulPaint) {

FILE: src/lib/LayoutShiftManager.ts
  class LayoutShiftManager (line 17) | class LayoutShiftManager {
    method _processEntry (line 23) | _processEntry(entry: LayoutShift) {

FILE: src/lib/bfcache.ts
  type onBFCacheRestoreCallback (line 17) | interface onBFCacheRestoreCallback {

FILE: src/lib/getSelector.ts
  constant MAX_LEN (line 24) | const MAX_LEN = 100;

FILE: src/lib/getVisibilityWatcher.ts
  method firstHiddenTime (line 103) | get firstHiddenTime() {
  method onHidden (line 106) | onHidden(cb: () => void) {

FILE: src/lib/initUnique.ts
  function initUnique (line 24) | function initUnique<T>(identityObj: object, ClassObj: new () => T): T {

FILE: src/lib/observe.ts
  type PerformanceEntryMap (line 17) | interface PerformanceEntryMap {

FILE: src/lib/polyfills/interactionCountPolyfill.ts
  type Performance (line 20) | interface Performance {

FILE: src/onINP.ts
  constant DEFAULT_DURATION_THRESHOLD (line 35) | const DEFAULT_DURATION_THRESHOLD = 40;

FILE: src/types.ts
  type PerformanceEntryMap (line 29) | interface PerformanceEntryMap {
  type Document (line 37) | interface Document {
  type Performance (line 44) | interface Performance {
  type PerformanceObserverInit (line 51) | interface PerformanceObserverInit {
  type PerformanceNavigationTiming (line 56) | interface PerformanceNavigationTiming {
  type PerformanceEventTiming (line 61) | interface PerformanceEventTiming extends PerformanceEntry {
  type LayoutShiftAttribution (line 67) | interface LayoutShiftAttribution {
  type LayoutShift (line 74) | interface LayoutShift extends PerformanceEntry {
  type LargestContentfulPaint (line 81) | interface LargestContentfulPaint extends PerformanceEntry {
  type ScriptInvokerType (line 91) | type ScriptInvokerType =
  type ScriptWindowAttribution (line 100) | type ScriptWindowAttribution =
  type PerformanceScriptTiming (line 108) | interface PerformanceScriptTiming extends PerformanceEntry {
  type PerformanceLongAnimationFrameTiming (line 128) | interface PerformanceLongAnimationFrameTiming extends PerformanceEntry {

FILE: src/types/base.ts
  type Metric (line 23) | interface Metric {
  type MetricType (line 86) | type MetricType =
  type MetricWithAttribution (line 94) | type MetricWithAttribution =
  type MetricRatingThresholds (line 114) | type MetricRatingThresholds = [number, number];
  type ReportCallback (line 121) | interface ReportCallback {
  type ReportOpts (line 125) | interface ReportOpts {
  type AttributionReportOpts (line 129) | interface AttributionReportOpts extends ReportOpts {
  type LoadState (line 148) | type LoadState =

FILE: src/types/cls.ts
  type CLSMetric (line 22) | interface CLSMetric extends Metric {
  type CLSAttribution (line 32) | interface CLSAttribution {
  type CLSMetricWithAttribution (line 74) | interface CLSMetricWithAttribution extends CLSMetric {

FILE: src/types/fcp.ts
  type FCPMetric (line 22) | interface FCPMetric extends Metric {
  type FCPAttribution (line 32) | interface FCPAttribution {
  type FCPMetricWithAttribution (line 63) | interface FCPMetricWithAttribution extends FCPMetric {

FILE: src/types/inp.ts
  type INPReportOpts (line 24) | interface INPReportOpts extends ReportOpts {
  type INPAttributionReportOpts (line 28) | interface INPAttributionReportOpts extends AttributionReportOpts {
  type INPMetric (line 35) | interface INPMetric extends Metric {
  type INPLongestScriptSummary (line 40) | interface INPLongestScriptSummary {
  type INPAttribution (line 61) | interface INPAttribution {
  type INPMetricWithAttribution (line 175) | interface INPMetricWithAttribution extends INPMetric {

FILE: src/types/lcp.ts
  type LCPMetric (line 22) | interface LCPMetric extends Metric {
  type LCPAttribution (line 32) | interface LCPAttribution {
  type LCPMetricWithAttribution (line 90) | interface LCPMetricWithAttribution extends LCPMetric {

FILE: src/types/ttfb.ts
  type TTFBMetric (line 22) | interface TTFBMetric extends Metric {
  type TTFBAttribution (line 36) | interface TTFBAttribution {
  type TTFBMetricWithAttribution (line 76) | interface TTFBMetricWithAttribution extends TTFBMetric {

FILE: test/e2e/onCLS-test.js
  function triggerLayoutShift (line 1064) | async function triggerLayoutShift() {
  function getAttribution (line 1077) | function getAttribution(entries) {

FILE: test/e2e/onINP-test.js
  constant ROUNDING_ERROR (line 29) | const ROUNDING_ERROR = 8;

FILE: test/e2e/onTTFB-test.js
  function assertValidEntry (line 27) | function assertValidEntry(entry) {

FILE: test/server.js
  constant BEACON_FILE (line 21) | const BEACON_FILE = 'test/beacons.log';
  constant MIME_TYPES (line 23) | const MIME_TYPES = {
  function readBody (line 32) | function readBody(req) {
  function sleep (line 41) | function sleep(ms) {

FILE: test/utils/assertIsCloseTo.js
  function assertIsCloseTo (line 23) | function assertIsCloseTo(actual, expected, maxDelta) {

FILE: test/utils/beacons.js
  constant BEACON_FILE (line 19) | const BEACON_FILE = './test/beacons.log';
  function beaconCountIs (line 25) | async function beaconCountIs(count, opts = {}) {
  function getBeacons (line 38) | async function getBeacons(opts = {}) {
  function clearBeacons (line 68) | async function clearBeacons() {

FILE: test/utils/browserSupportsEntry.js
  function browserSupportsEntry (line 23) | function browserSupportsEntry(type) {

FILE: test/utils/domReadyState.js
  function domReadyState (line 23) | function domReadyState(state) {

FILE: test/utils/firstContentfulPaint.js
  function firstContentfulPaint (line 22) | function firstContentfulPaint() {

FILE: test/utils/imagesPainted.js
  function imagesPainted (line 22) | function imagesPainted() {

FILE: test/utils/navigateTo.js
  function navigateTo (line 24) | async function navigateTo(urlPath, opts) {

FILE: test/utils/nextFrame.js
  function nextFrame (line 22) | function nextFrame() {

FILE: test/utils/stubForwardBack.js
  function stubForwardBack (line 22) | function stubForwardBack(visibilityStateAfterRestore) {

FILE: test/utils/stubVisibilityChange.js
  function stubVisibilityChange (line 22) | function stubVisibilityChange(visibilityState) {

FILE: test/utils/waitUntilIdle.js
  function waitUntilIdle (line 22) | function waitUntilIdle() {

FILE: test/utils/webVitalsLoaded.js
  function webVitalsLoaded (line 22) | function webVitalsLoaded() {
Condensed preview — 91 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (459K chars).
[
  {
    "path": ".editorconfig",
    "chars": 498,
    "preview": "# editorconfig.org\n# For Visual Studio code use this extension to enforce below rules:\n# https://marketplace.visualstudi"
  },
  {
    "path": ".github/workflows/lint.yml",
    "chars": 429,
    "preview": "name: Lint Code Base\npermissions:\n  contents: read\non:\n  pull_request:\n  push:\n    branches:\n      - main\n  workflow_dis"
  },
  {
    "path": ".github/workflows/tests.yml",
    "chars": 1840,
    "preview": "name: Run tests\npermissions:\n  contents: read\non:\n  pull_request:\n  push:\n    branches:\n      - main\n  workflow_dispatch"
  },
  {
    "path": ".gitignore",
    "chars": 135,
    "preview": ".DS_Store\n.vscode\nnode_modules\n\n# Log files\n*.log\n\n# Generated TypeScript files and build data\ntsconfig.tsbuildinfo\n\n# D"
  },
  {
    "path": ".husky/.gitignore",
    "chars": 2,
    "preview": "_\n"
  },
  {
    "path": ".husky/pre-commit",
    "chars": 206,
    "preview": "lint-staged\n\ngrep -r \"\\.only(\" test/e2e \\\n  && echo \"ERROR: found .only() use in test\" && exit 1\n\ngrep -r \"browser\\.debu"
  },
  {
    "path": ".nvmrc",
    "chars": 13,
    "preview": "lts/Hydrogen\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 13799,
    "preview": "# Changelog\n\n### v5.1.0 (2025-07-31)\n\n- Register `visibility-change` early ([#637](https://github.com/GoogleChrome/web-v"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 3167,
    "preview": "# Google Open Source Community Guidelines\n\nAt Google, we recognize and celebrate the creativity and collaboration of ope"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 1797,
    "preview": "# How to Contribute\n\nWe'd love to accept your patches and contributions to this project. There are\njust a few small guid"
  },
  {
    "path": "LICENSE",
    "chars": 11342,
    "preview": "\n                                 Apache License\n                           Version 2.0, January 2004\n                  "
  },
  {
    "path": "README.md",
    "chars": 51430,
    "preview": "# `web-vitals`\n\n- [Overview](#overview)\n- [Install and load the library](#installation)\n  - [From npm](#import-web-vital"
  },
  {
    "path": "attribution.d.ts",
    "chars": 621,
    "preview": "/*\n Copyright 2022 Google LLC\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this fil"
  },
  {
    "path": "attribution.js",
    "chars": 755,
    "preview": "/*\n Copyright 2022 Google LLC\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this fil"
  },
  {
    "path": "docs/upgrading-to-v4.md",
    "chars": 4983,
    "preview": "# Upgrading to v4\n\nThis document lists the full set of changes between version 3 and version 4 that are relevant to anyo"
  },
  {
    "path": "docs/upgrading-to-v5.md",
    "chars": 3655,
    "preview": "# Upgrading to v5\n\nThis document lists the full set of changes between version 4 and version 5 that are relevant to anyo"
  },
  {
    "path": "eslint.config.js",
    "chars": 2249,
    "preview": "import {defineConfig} from 'eslint/config';\nimport globals from 'globals';\nimport tsParser from '@typescript-eslint/pars"
  },
  {
    "path": "package.json",
    "chars": 5297,
    "preview": "{\n  \"name\": \"web-vitals\",\n  \"version\": \"5.1.0\",\n  \"description\": \"Easily measure performance metrics in JavaScript\",\n  \""
  },
  {
    "path": "rollup.config.js",
    "chars": 2633,
    "preview": "/*\n Copyright 2020 Google LLC\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this fil"
  },
  {
    "path": "src/attribution/index.ts",
    "chars": 1016,
    "preview": "/*\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/attribution/onCLS.ts",
    "chars": 4693,
    "preview": "/*\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/attribution/onFCP.ts",
    "chars": 2541,
    "preview": "/*\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/attribution/onINP.ts",
    "chars": 18230,
    "preview": "/*\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/attribution/onLCP.ts",
    "chars": 5318,
    "preview": "/*\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/attribution/onTTFB.ts",
    "chars": 3904,
    "preview": "/*\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/index.ts",
    "chars": 873,
    "preview": "/*\n * Copyright 2020 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/lib/InteractionManager.ts",
    "chars": 5414,
    "preview": "/*\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/lib/LCPEntryManager.ts",
    "chars": 797,
    "preview": "/*\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/lib/LayoutShiftManager.ts",
    "chars": 1696,
    "preview": "/*\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/lib/bfcache.ts",
    "chars": 1016,
    "preview": "/*\n * Copyright 2020 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/lib/bindReporter.ts",
    "chars": 1890,
    "preview": "/*\n * Copyright 2020 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/lib/doubleRAF.ts",
    "chars": 715,
    "preview": "/*\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/lib/generateUniqueID.ts",
    "chars": 883,
    "preview": "/*\n * Copyright 2020 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/lib/getActivationStart.ts",
    "chars": 792,
    "preview": "/*\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/lib/getLoadState.ts",
    "chars": 1995,
    "preview": "/*\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/lib/getNavigationEntry.ts",
    "chars": 1379,
    "preview": "/*\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/lib/getSelector.ts",
    "chars": 1318,
    "preview": "/*\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/lib/getVisibilityWatcher.ts",
    "chars": 4598,
    "preview": "/*\n * Copyright 2020 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/lib/initMetric.ts",
    "chars": 1887,
    "preview": "/*\n * Copyright 2020 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/lib/initUnique.ts",
    "chars": 1074,
    "preview": "/*\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/lib/observe.ts",
    "chars": 2142,
    "preview": "/*\n * Copyright 2020 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/lib/polyfills/getFirstHiddenTimePolyfill.ts",
    "chars": 1083,
    "preview": "/*\n * Copyright 2020 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/lib/polyfills/interactionCountPolyfill.ts",
    "chars": 1951,
    "preview": "/*\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/lib/runOnce.ts",
    "chars": 746,
    "preview": "/*\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/lib/whenActivated.ts",
    "chars": 786,
    "preview": "/*\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/lib/whenIdleOrHidden.ts",
    "chars": 1517,
    "preview": "/*\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/onCLS.ts",
    "chars": 4544,
    "preview": "/*\n * Copyright 2020 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/onFCP.ts",
    "chars": 3293,
    "preview": "/*\n * Copyright 2020 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/onINP.ts",
    "chars": 6184,
    "preview": "/*\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/onLCP.ts",
    "chars": 5390,
    "preview": "/*\n * Copyright 2020 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/onTTFB.ts",
    "chars": 3638,
    "preview": "/*\n * Copyright 2020 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/types/base.ts",
    "chars": 5208,
    "preview": "/*\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/types/cls.ts",
    "chars": 2711,
    "preview": "/*\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/types/fcp.ts",
    "chars": 2141,
    "preview": "/*\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/types/inp.ts",
    "chars": 7103,
    "preview": "/*\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/types/lcp.ts",
    "chars": 3206,
    "preview": "/*\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/types/ttfb.ts",
    "chars": 2838,
    "preview": "/*\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "src/types.ts",
    "chars": 4738,
    "preview": "/*\n * Copyright 2020 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "test/css/styles.css",
    "chars": 37,
    "preview": "body {\n  background-color: yellow;\n}\n"
  },
  {
    "path": "test/e2e/onCLS-test.js",
    "chars": 37956,
    "preview": "/*\n * Copyright 2020 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "test/e2e/onFCP-test.js",
    "chars": 15238,
    "preview": "/*\n * Copyright 2020 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "test/e2e/onINP-test.js",
    "chars": 41856,
    "preview": "/*\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "test/e2e/onLCP-test.js",
    "chars": 33665,
    "preview": "/*\n * Copyright 2020 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "test/e2e/onTTFB-test.js",
    "chars": 15373,
    "preview": "/*\n * Copyright 2020 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "test/script/async.js",
    "chars": 654,
    "preview": "/*\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "test/script/defer.js",
    "chars": 654,
    "preview": "/*\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "test/server.js",
    "chars": 3834,
    "preview": "/*\n Copyright 2019 Google Inc. All Rights Reserved.\n Licensed under the Apache License, Version 2.0 (the \"License\");\n yo"
  },
  {
    "path": "test/tsconfig.json",
    "chars": 494,
    "preview": "{\n  \"compilerOptions\": {\n    \"allowJs\": true,\n    \"composite\": true,\n    \"declaration\": true,\n    \"lib\": [\"es2017\", \"DOM"
  },
  {
    "path": "test/unit/attribution-test.js",
    "chars": 753,
    "preview": "import {describe, it} from 'node:test';\nimport assert from 'assert';\nimport {\n  onCLS,\n  onFCP,\n  onINP,\n  onLCP,\n  onTT"
  },
  {
    "path": "test/unit/bindReporter-test.js",
    "chars": 6287,
    "preview": "import {describe, it} from 'node:test';\nimport assert from 'assert';\nimport {bindReporter} from '../../dist/modules/lib/"
  },
  {
    "path": "test/unit/index-test.js",
    "chars": 741,
    "preview": "import {describe, it} from 'node:test';\nimport assert from 'assert';\nimport {\n  onCLS,\n  onFCP,\n  onINP,\n  onLCP,\n  onTT"
  },
  {
    "path": "test/utils/assertIsCloseTo.js",
    "chars": 837,
    "preview": "/*\n * Copyright 2025 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "test/utils/beacons.js",
    "chars": 2012,
    "preview": "/*\n * Copyright 2020 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "test/utils/browserSupportsEntry.js",
    "chars": 1336,
    "preview": "/*\n * Copyright 2020 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "test/utils/domReadyState.js",
    "chars": 1507,
    "preview": "/*\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "test/utils/firstContentfulPaint.js",
    "chars": 1051,
    "preview": "/*\n * Copyright 2023 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "test/utils/imagesPainted.js",
    "chars": 2108,
    "preview": "/*\n * Copyright 2020 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "test/utils/navigateTo.js",
    "chars": 1722,
    "preview": "/*\n * Copyright 2023 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "test/utils/nextFrame.js",
    "chars": 904,
    "preview": "/*\n * Copyright 2022 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "test/utils/stubForwardBack.js",
    "chars": 1027,
    "preview": "/*\n * Copyright 2020 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "test/utils/stubVisibilityChange.js",
    "chars": 967,
    "preview": "/*\n * Copyright 2020 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "test/utils/waitUntilIdle.js",
    "chars": 1002,
    "preview": "/*\n * Copyright 2025 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "test/utils/webVitalsLoaded.js",
    "chars": 939,
    "preview": "/*\n * Copyright 2025 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use"
  },
  {
    "path": "test/views/cls.njk",
    "chars": 3262,
    "preview": "<!--\n Copyright 2020 Google LLC\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this f"
  },
  {
    "path": "test/views/fcp.njk",
    "chars": 2162,
    "preview": "<!--\n Copyright 2020 Google LLC\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this f"
  },
  {
    "path": "test/views/inp.njk",
    "chars": 6232,
    "preview": "<!--\n Copyright 2022 Google LLC\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this f"
  },
  {
    "path": "test/views/layout.njk",
    "chars": 9325,
    "preview": "<!DOCTYPE html>\n<!--\n Copyright 2020 Google LLC\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you ma"
  },
  {
    "path": "test/views/lcp.njk",
    "chars": 4025,
    "preview": "<!--\n Copyright 2020 Google LLC\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this f"
  },
  {
    "path": "test/views/ttfb.njk",
    "chars": 2440,
    "preview": "<!--\n Copyright 2020 Google LLC\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this f"
  },
  {
    "path": "tsconfig.json",
    "chars": 529,
    "preview": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"declaration\": true,\n    \"lib\": [\"esnext\", \"DOM\"],\n    \"module\": \"no"
  },
  {
    "path": "wdio.conf.js",
    "chars": 14008,
    "preview": "/*\n Copyright 2020 Google LLC\n Licensed under the Apache License, Version 2.0 (the \"License\");\n you may not use this fil"
  }
]

About this extraction

This page contains the full source code of the GoogleChrome/web-vitals GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 91 files (430.7 KB), approximately 108.7k tokens, and a symbol index with 82 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!