npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nytka/plugin-pagespeed

v0.1.1

Published

PageSpeed Insights connector for nytka projects. Collects Lighthouse lab scores, Core Web Vitals lab metrics and the non-passing audits with their fix lists for any public URL into datasets/ and registers them with provenance.

Readme

@nytka/plugin-pagespeed

PageSpeed Insights connector for nytka projects. Runs Google's hosted Lighthouse against any public URL — your own or a competitor's — and collects the scores, the lab Core Web Vitals and every non-passing audit with its fix list into the project's datasets/, registered in datasets/index.json with provenance.

Read-only, by nature. The API has one method, runPagespeed, and it is a GET. There is no write surface to keep out of reach, so there is no allowlist here to admire: the client builds one URL, sends one verb, and nothing about a page can be changed through it.

Lab data, not field data. What this collects is a Lighthouse run in Google's data centre under a throttled, emulated device — a repeatable measurement of the page as built, not what Chrome users experienced. Field data is a different measurement from a different API, and a different connector: see Lab, not field and decision 0013.

Why this source

Every other connector in the line reports on a property the project owns — a Search Console grant, a GA4 property, a CMS token. PageSpeed Insights answers for any URL on the public web, on one API key and no OAuth. That makes it the first source here that reads a competitor: the same measurement, the same scale, for a page you do not control.

What it gives that nothing else in the line does:

  • The four Lighthouse scores — performance, accessibility, best practices, SEO — per URL and per device, from Google's own runner, so the number matches what a client sees on pagespeed.web.dev.
  • The fix list. Every audit the page fails, with Lighthouse's estimated savings and the actual resources behind it — the render-blocking stylesheet, the unsized image, the low-contrast span. This is the part a task gets written from.

Install

npm install @nytka/plugin-pagespeed

One dependency, @nytka/core, which has none. The API is JSON over HTTP and Node's built-in fetch speaks it; there is no client library to carry.

Setup

About five minutes. There is no key file and nothing to put in private/. This is not the service account the other Google connectors share (GOOGLE_SERVICE_ACCOUNT_KEY): PageSpeed Insights takes a plain API key as a query parameter.

1. Enable the API

Google Cloud console → pick a project (the one holding the service account is fine) → APIs & Services → Library → search PageSpeed Insights API → Enable.

2. Create the key

APIs & Services → Credentials → Create credentials → API key. Then edit the key:

  • Application restrictions: None. A referrer restriction blocks a command line, which sends no referrer.
  • API restrictions: Restrict key → PageSpeed Insights API — and Chrome UX Report API if the same key is to serve @nytka/plugin-crux, which accepts PAGESPEED_API_KEY in place of its own. The key can then do nothing else if it leaks.

Calls without a key are refused outright: the anonymous project's daily quota is zero (observed 2026-09-19). A key of your own is the whole setup.

3. Configure

Append the package's .env.example to the project's .env and fill it in:

cat node_modules/@nytka/plugin-pagespeed/.env.example >> .env
PAGESPEED_API_KEY=AIza…                          # the key value from step 2
PAGESPEED_URLS=https://example.com/, https://example.com/pricing/

PAGESPEED_URLS is every page to measure, in full — scheme, host, path — comma-separated. Each origin in the list gets its own pair of datasets; several pages of one origin become rows of the same pair. Every entry costs two calls per run and ten seconds to a minute each, so keep it to the pages whose speed you act on.

Then name the system in references/systems.yaml, pointers only:

pageSpeed:                       # @nytka/plugin-pagespeed
  urls:
    - https://example.com/
    - https://example.com/pricing/
  authEnvVar: PAGESPEED_API_KEY  # the key VALUE, not a path; a Cloud API key restricted to this API

4. Verify

npx nytka-pagespeed check

One mobile, performance-only run of the first URL. Prints the score and the three headline metrics, writes nothing. It spends one call of the quota, because the API has no free call.

Use

npx nytka-pagespeed check                        # auth smoke test — one run, nothing written
npx nytka-pagespeed measure                      # every URL × mobile + desktop → two datasets per origin
npx nytka-pagespeed measure --strategy mobile    # one device only
npx nytka-pagespeed measure --url https://example.org/   # a one-off list, the env var ignored
npx nytka-pagespeed measure --snapshot           # key today's run to the day, not the month
npx nytka-pagespeed measure --no-register        # write the payloads, leave the registry alone

measure calls the API once per URL and device, sequentially, and prints a progress line per run on stderr. Four categories are requested in every call — the performance run carries the other three at no extra cost — so one call yields both datasets.

The surface, and the question each part answers

| Command | Calls | Question | |---|---|---| | check | 1 | Does the key work, and roughly how fast is the first page on a phone? | | measure → pagespeed-scores-… | URLs × devices | How does each page score on performance, accessibility, best practices and SEO, on mobile and desktop, and what are its lab Core Web Vitals? | | measure → pagespeed-audits-… | (same calls) | What exactly does each page fail, how much would fixing it save, and on which resources? |

Why two datasets from one run. A Lighthouse score varies run to run — network jitter, the runner's load, an A/B variant served — so the audits that explain a score must come from the run that produced it. Two commands making two sets of calls would produce a fix list for a score nobody recorded.

What a query script is asked

  • Which URL sits below 90 on mobile? → scores rows, strategy === 'mobile', sort by performance.
  • Which audit would save the most LCP, and on which resources? → audits rows, sort by savings.LCP, read items.
  • Did mobile performance move since last month? → this month's and last month's scores entries, same URL, same strategy.
  • Is the competitor faster? → the same three questions over their origin's pair.

The two datasets

pagespeed-scores-<origin>-<period> — operation lab-scores

One row per URL × device.

| Column | Type | From | |---|---|---| | url | string | the URL as configured — what was asked for | | strategy | mobile | desktop | the emulated device | | date | YYYY-MM-DD | the UTC day of lighthouseResult.fetchTime | | performance, accessibility, bestPractices, seo | integer 0–100 or null | the category score × 100, rounded; null when Lighthouse could not score it | | fcpMs, lcpMs, tbtMs, speedIndexMs | integer ms or null | the metric audits' numericValue, rounded | | cls | number to 3 dp or null | cumulative-layout-shift, unitless | | ttfbMs | integer ms or null | server-response-time — the root document's time to first byte | | finalUrl | string or null | where the page landed after redirects (finalDisplayedUrl) | | lighthouseVersion | string | which Lighthouse produced the numbers |

The performance score is a weighted blend of five of these — FCP 10 %, Speed Index 10 %, LCP 25 %, TBT 30 %, CLS 25 % in Lighthouse 10 and later. TTFB is not scored; it is kept because it is the number a hosting conversation turns on.

pagespeed-audits-<origin>-<period> — operation lab-audits

One row per URL × device × non-passing audit.

| Column | Type | From | |---|---|---| | url, strategy | as above | | | audit | string | the audit id, e.g. unused-javascript, color-contrast | | title | string | Lighthouse's wording for the failing state | | category | performance | accessibility | best-practices | seo | null | which category's auditRefs names it | | group | string or null | the report section (metrics, insights, diagnostics, a11y-color-contrast, …) | | score | 0–1 or null | as Lighthouse gives it | | mode | binary | numeric | metricSavings | error | scoreDisplayMode | | displayValue | string or null | the report's short reading, e.g. Root document took 610 ms | | numericValue, numericUnit | number, string, or null | the unscored measurement behind it | | weight | number | the audit's weight in its category score — 0 for every opportunity and insight, so sort on it to see what moved the score | | savings | object or null | metricSavings as Lighthouse gives it: { LCP: 750, FCP: 0 } — milliseconds, except CLS | | itemCount | integer | how many items the audit listed | | items | array | details.items as Lighthouse gives it, first 100 — the resources, nodes and bytes behind the failure |

Non-passing is the Lighthouse report's own rule, not a stricter one: an audit fails when its score is below 0.9 in any scored mode (binary, numeric, metricSavings), an error always counts, and informative, manual and notApplicable never do. One rule is this package's: an audit the category files under the hidden group is skipped, because Lighthouse 13 hides an audit exactly when an insight has superseded it (interactive, server-response-time, redirects), and listing both would count one finding twice.

metricSavings is the mode Lighthouse 11 introduced for opportunities and insights — the older references do not list it. It is where the fix list lives; a filter written from the documented modes alone would drop it entirely.

Several origins

PAGESPEED_URLS=https://example.com/, https://example.com/pricing/, https://competitor.example/

Two origins, so two pairs: pagespeed-scores-example-com-… with two rows per device, and pagespeed-scores-competitor-example-… with one. The registry entry's subject is the origin, not the URL, and it is subject rather than property because the thing measured need not be one the project owns.

Lab, not field

Lighthouse in Google's data centre loads the page once, on an emulated Moto G Power over a throttled 4G connection (mobile) or a desktop profile, and reports what it saw. That is lab data: repeatable, comparable across pages and sites, and available for any URL. It is not what your visitors experienced — a page can score 100 in the lab and fail Core Web Vitals in the field because real users are on worse phones, or pass in the field because they are mostly on fast desktops.

The same API response still carries field data (loadingExperience, from the Chrome UX Report), and this package does not read it. Google's own page, 2026-09-19: "We plan to discontinue including real-world data from the Chrome User Experience Report in this API. We recommend the CrUX API or the CrUX History API instead." Field data belongs to a later @nytka/plugin-crux on that API; 0013 has the four reasons. Every summary this package writes ends with lab data, not what users experienced so the two are never read as one.

Not collected, and why. The full Lighthouse result — several hundred KB per run, a full-page screenshot and every translation string included — stays in the response; the rows keep what a query needs. The pwa category is gone from Lighthouse 12, and the discovery document's agentic_browsing category is new and undocumented; neither is asked for. Items of passing and informative audits are not kept: a fix list of things that are fine is noise.

Troubleshooting

| What you see | What it means | |---|---| | PAGESPEED_API_KEY is not set … and exit 0 | The project has not configured PageSpeed Insights — step 3. Not an error: capability is configuration presence | | no URL to measure | PAGESPEED_URLS is empty and no --url was given — step 3 | | PageSpeed Insights rejected the API key | The key is wrong, or was deleted in the console — step 2 | | the PageSpeed Insights API is not enabled in the Cloud project that owns this key | Step 1 was skipped for the project the key belongs to | | the API key is restricted to HTTP referrers | Step 2's application restriction is set. Change it to None and keep the API restriction | | plugin-crux says the key's restriction does not allow its API | Step 2's API restriction lists only PageSpeed Insights. Add Chrome UX Report API beside it | | the daily quota for this caller is zero | No key reached Google — the value is blank or not loaded — or the API is not enabled in the key's project | | rate-limited the call (HTTP 429) | The per-100-seconds or per-day quota. Retry later; the figures are in the Cloud console for the key's project | | Lighthouse could not measure https://… (ERRORED_DOCUMENT_REQUEST / NO_FCP / …) | Google's runner could not load that page — down, blocked, behind a login, or painted nothing. The other URLs still collect; this one is in the entry's summary and the payload's failures | | Google asked for a captcha before measuring … | Google's FAQ names a shared key as the usual cause. A key of your own avoids it | | did not answer for https://… within 120s | A slow page or a busy runner; that run is recorded as failed and the rest continue | | no URL could be measured — N runs failed | Every run failed; nothing was written. The list says why, per URL | | no project.yaml found walking up from cwd | Not inside a nytka project. cd to the project root |

What it writes

| Path | Committed? | |---|---| | datasets/payloads/<id>.json | no — the directory ships its own .gitignore | | datasets/index.json | yes — two entries per origin, added or replaced |

The dataset id

scores    pagespeed-scores-example-com-2026-09          keyed to the month (default)
audits    pagespeed-audits-example-com-2026-09
scores    pagespeed-scores-example-com-2026-09-19       keyed to the day (--snapshot)

A lab run has no date range — it is a measurement taken now — so the id is keyed to the reporting period, not to the run (PLG-007): a month of daily runs replaces one entry rather than laying down thirty. The entry's period records exactly what the id is keyed to, and dateRange is explicitly null. --snapshot keys the id to the day instead, for whoever wants a dated series — the same opt-in @nytka/plugin-sanity offers.

The row's date is the run's UTC day; the entry's collectedAt and period are the local calendar. Around midnight the two can differ by a day, and both are right.

Rules it follows

  • Payloads never enter agent context. Query them with a script; write conclusions to research/. The CLI prints row counts and paths for this reason; check prints four numbers, which is the smoke test's job.
  • The key never appears in output. It travels as a query parameter, which is how the API takes it; no URL is printed, and any response body is redacted before it can reach an error message.
  • One run's failure is that run's. A page Google cannot load is recorded and skipped; the collection carries on. A key or quota refusal stops it at once, because every further call would fail the same way.
  • Calls are sequential, on purpose: the quota is per 100 seconds and a run takes up to a minute.
  • The project is found by walking up for project.yaml, so it works at any install depth.
  • No YAML parsing, no config file. Secrets from .env, everything else from flags.

Quotas and limits worth knowing

  • Verified 2026-09-19: a call with no key answers HTTP 429, Queries per day, with a limit of 0 for the anonymous project. There is no keyless mode any more.
  • Quoted, not verified here: 25,000 queries per day and 100 per 100 seconds per project (PLG-017, from Google's older documentation). The Cloud console shows the figures that apply to your project; the connector claims no number in its messages.
  • What has and has not been run against the live API. Confirmed without a credential on 2026-09-19: the URL format, the query-parameter auth, Google's error envelope, the keyless quota refusal and the API_KEY_INVALID fault for a made-up key. Not yet confirmed: a successful response. Its shape here is a Lighthouse 13.5.0 result produced locally and wrapped in the envelope the reference documents. Two spellings of the enums exist in Google's own pages — the reference and Lighthouse CI's client use mobile and best-practices; the discovery document says MOBILE and BEST_PRACTICES — and the lowercase form is what goes on the wire. The first keyed check is the check.
  • Lighthouse moves. PageSpeed Insights ran 13.0 from 2025-10-20 and later 13.x since; scores are comparable within a version and not always across one. Every row carries lighthouseVersion so a jump can be read for what it is.
  • Every run is a fresh page load. A run is not cached: the same URL measured twice in a minute costs two calls and may score differently.

Tests

npm test

node --test against recorded responses. No credential, no network. Sixty-two tests, including: the scores rounding and the CLS precision, the 0.9 threshold at its boundary in every mode Lighthouse has including the one the reference omits, the hidden group skipped with the real interactive and server-response-time audits as witnesses, the id holding still across a month of runs and moving with --snapshot, a captcha, a runtimeError, a Lighthouse 500 and a timeout each failing one run and not the collection, the observed keyless 429 and the live invalid-key 400 pinned byte for byte, the key absent from every error the package can produce, and the CLI's exit codes at the process boundary.

The fixtures are Lighthouse 13.5.0 results run locally on 2026-09-19 against two real pages — one that scores 100 across the board, one at 66 with a dozen failures — anonymised to example.com and example.org and trimmed to the audits the tests need, then wrapped in the pagespeedonline#result envelope from the reference. error-429-keyless.json and error-400-key-invalid.json are live bodies, verbatim.