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

tldkit

v1.0.2

Published

A zero-dependency, ESM-first drop-in replacement for tldts and psl. Same API, same Public Suffix List, a fraction of the install size.

Readme

tldkit

A drop-in replacement for tldts and psl — same API, same Public Suffix List, a fraction of the install size.

npm unpacked size zero dependencies CI

npm install tldkit
- import { getDomain, parse } from "tldts";
+ import { getDomain, parse } from "tldkit";

That is the entire migration. Every export keeps its name, its signature and its option defaults — allowPrivateDomains, validHosts, mixedInputs, detectSpecialUse, and the full IResult field set. If you use psl instead, the equivalent import is tldkit/psl.

Why

One reason, and it is install size.

tldts unpacks to 3,187 KB across tldts + tldts-core. Measured against that tree: 1,809 KB of it is sourcemaps — 33 files — and the Public Suffix List trie ships several times over, as TypeScript source (114 KB), as a .d.ts (70 KB), once per module format under dist/es6 and dist/cjs (135 KB each), and inlined again into three prebuilt bundles (index.umd.min.js, index.esm.min.js, index.cjs.min.js). The list is not what makes the package big; the packaging is.

tldkit ships the same list, encoded once, stored once, with no sourcemaps and no prebuilt bundles.

Install size — bytes on disk after npm install

The whole transitive tree, which is what a CI cache, a Docker layer, a Lambda bundle or a laptop actually holds. Reproduce with npm run size.

| package | transitive tree | on disk | | --- | --- | --- | | tldts | tldts + tldts-core | 3,186.9 KB | | psl | psl + punycode | 728.0 KB | | tldkit | tldkit | 140.9 KB |

22.6× smaller than tldts, 5.2× smaller than psl, and zero runtime dependencies either way. Warm lookup throughput is at parity with tldts, so this is not a size-for-speed trade — see What this is not for the axes where it does cost you.

An earlier draft of this package targeted "under 100 KB". It is not reachable and the target was dropped rather than fudged: the encoded Public Suffix List is 69 KB on its own, and the rest is the dual ESM/CJS output and its type declarations. Dropping the CommonJS tree would get there, at the cost of every CommonJS consumer. 22.6× is measured; there is no rounder number behind it.

Bundled size — what a browser downloads

A separate claim, and a much smaller one. esbuild, bundled, minified, tree-shaken, then gzipped. Reproduce with npm run size.

| import shape | baseline | gzip | tldkit entry | gzip | delta | | --- | --- | --- | --- | --- | --- | | named getDomain | tldts | 44.6 KB | tldkit | 41.6 KB | −3.0 KB | | named parse | tldts | 44.6 KB | tldkit | 41.6 KB | −3.0 KB | | whole namespace | tldts | 44.8 KB | tldkit | 41.7 KB | −3.2 KB | | named getDomain | tldts | 44.6 KB | tldkit/icann | 28.5 KB | −16.2 KB | | parse (psl API) | psl | 43.7 KB | tldkit/psl | 40.7 KB | −3.0 KB |

On the full dataset that is about 7% smaller — real, but thin, and not a reason on its own to change a dependency. Both libraries have to ship the same Public Suffix List, and the list is most of the bytes.

That margin is deliberately narrower than it once was. The dataset is stored in a denser encoding than it used to be, which is what shrank the install tree and the resident heap below — but denser bytes have higher entropy, so they gzip less well. The trade was taken knowingly: install size and memory are where this package wins, and the bundled column is where it pays for them.

The large bundled win needs the tldkit/icann entry point: 36% smaller, 44.6 KB down to 28.5 KB, because a bundler following that entry never reaches the PRIVATE section of the list. tldts cannot offer the same thing — its trie is one monolithic module covering both sections. The tradeoff is that tldkit/icann cannot resolve private suffixes at all, so it is only the right choice if you never pass allowPrivateDomains: true. npm run size probes the emitted bundle for a PRIVATE-only rule and fails if one leaks in, so that row is checked rather than asserted.

Runtime memory — heap once the list is loaded

A third claim, kept separate from the other two like they are from each other. Both libraries hold the whole Public Suffix List resident and both keep it in typed arrays; what differs is what that costs. tldkit expands a compact encoded string into a flat radix trie — Uint8Array and Uint16Array edge, flag and offset tables over a single run of label text — building only the sections your options actually enable, and filling in its per-node lookup index lazily as hostnames arrive rather than all at once.

Measured as heapUsed after parsing the whole benchmark corpus, against a baseline taken before the import, under --expose-gc so both readings follow a collection. Reproduce with npm run bench.

| | tldts | tldkit | | | --- | --- | --- | --- | | default, ICANN only | 1,140 KB | 645 KB | 0.57× | | with the private section | 1,150 KB | 683 KB | 0.59× | | tldkit/icann | 1,140 KB | 544 KB | 0.48× |

Roughly 500 KB less resident per process on the default path. That matters most where processes are many and short — a serverless worker per request, a test runner forking per file, a crawler with a pool of workers.

Entry points

| entry | dataset | API | | --- | --- | --- | | tldkit | ICANN + PRIVATE | tldts | | tldkit/icann | ICANN only | tldts | | tldkit/psl | ICANN + PRIVATE | psl |

tldkit — the default. Use it to replace tldts. Exports parse, getHostname, getPublicSuffix, getDomain, getFullDomain, getSubdomain and getDomainWithoutSuffix, plus a default export carrying all seven so import tldts from "tldkit" works. Types IResult and IOptions are exported too. allowPrivateDomains defaults to false, as in tldts.

tldkit/icann — the same API restricted to the ICANN section. allowPrivateDomains is accepted and ignored. Worth using when you are bundling for a browser and never need private suffixes; not worth using anywhere the install-size argument is the one that matters, since both entries ship in the same package.

tldkit/psl — the psl API: parse, get, isValid, errorCodes, and a default export. It follows psl's own algorithm rather than the tldts-compatible one, because the two genuinely disagree — on unlisted TLDs, on .local, and on how much of a hostname counts as the subdomain. Matching psl is the point of the entry point.

import { getDomain } from "tldkit";
import { getDomain as icannOnly } from "tldkit/icann";
import psl from "tldkit/psl";

getDomain("https://www.bbc.co.uk/news");                              // "bbc.co.uk"
getDomain("foo.blogspot.com", { allowPrivateDomains: true });         // "foo.blogspot.com"
icannOnly("foo.blogspot.com");                                        // "blogspot.com"
psl.get("www.example.co.uk");                                         // "example.co.uk"

Both ESM and CommonJS are shipped, with types for each, so require("tldkit") works as well.

Migration

From tldts

npm install tldkit && npm uninstall tldts
- import { getDomain } from "tldts";
+ import { getDomain } from "tldkit";
- const { getDomain } = require("tldts");
+ const { getDomain } = require("tldkit");

From psl

- import psl from "psl";
+ import psl from "tldkit/psl";

Without changing any code

If the import sites are in a dependency you do not control, or there are too many of them to touch, alias the specifier at build time:

// vite.config.js
export default { resolve: { alias: { tldts: "tldkit", psl: "tldkit/psl" } } };
// webpack.config.js
module.exports = { resolve: { alias: { tldts: "tldkit", psl: "tldkit/psl" } } };
// esbuild
await build({ alias: { tldts: "tldkit", psl: "tldkit/psl" } });

To reach a dependency's own import "tldts" rather than your bundle, redirect it at install time instead — npm overrides, yarn resolutions, pnpm pnpm.overrides:

{ "overrides": { "tldts": "npm:tldkit@^1" } }

That route works for tldts only. psl's replacement lives at the tldkit/psl subpath, and a package alias cannot point at a subpath.

An alias is worth verifying rather than assuming: it silently does nothing if the specifier is resolved through a path your configuration does not cover.

Correctness

The official publicsuffix.org conformance suite is vendored at test/fixtures/psl-tests.txt and runs with allowPrivateDomains: true:

tldkit  76/78
tldts    76/78

The two failures are .example.com and .example.example — a hostname with a single leading dot, which the suite says must yield null and which both libraries resolve as though the dot were absent. tldts fails the identical two cases with the identical values. For a drop-in that is the correct outcome: match the incumbent, and say where the incumbent deviates.

Beyond the suite, tldkit and tldts are run side by side over every rule in the list — each with subdomain, uppercase and trailing-dot mutations, under both allowPrivateDomains settings — and every field of the returned result must match, not only domain. The two diverge on a small number of nested-wildcard rules; the differential suite pins every one of them and asserts which behaviour the publicsuffix.org algorithm calls for, so a divergence that is merely different — rather than justified — fails the build.

The dataset itself is generated by scripts/build-data.mjs straight from publicsuffix.org, committed rather than downloaded at install time, and checked in CI: npm run check:data regenerates it and fails if what is committed differs. The current snapshot is dated 2026-08-05 and carries 10,239 rules (6,949 ICANN, 3,290 private).

What this is not

It is not faster than tldts, and this README will not claim that it is. Warm throughput lands within a few percent of tldts, on the low side of it as often as the high, while the spread across the benchmark's own reps is 17–40% — an order of magnitude wider than the difference. The supportable claim is parity; anything finer is noise, and the benchmark prints that spread as a column so you can check the reading. Node 26, [email protected], a fixed corpus of 140 hostnames and URLs; cold start is the median of 15 fresh processes, throughput the median of 45 reps. Reproduce with npm run bench.

| | tldts | tldkit | | | --- | --- | --- | --- | | getDomain, warm | 6.8 M/s | 6.7 M/s | parity (0.99×) | | parse, warm | 8.3 M/s | 8.2 M/s | parity (0.98×) | | getDomain, warm, private section | 5.6 M/s | 5.4 M/s | parity (0.96×) | | parse, warm, private section | 6.7 M/s | 6.4 M/s | parity (0.94×) |

tldkit expands its packed dataset on the first lookup rather than at module evaluation, so the first call costs more than the ones after it. npm run bench reports that alongside everything else.

The dataset is a snapshot, not a live fetch. The Public Suffix List changes continuously; this package pins the copy it was built with. A scheduled CI job regenerates it weekly, runs the full suite and opens a pull request when the list has moved, so the lag is a release cycle rather than an install. Nothing runs on install, and nothing reaches the network at runtime. If you need the list as of this minute, no npm package will give you that.

tldkit/psl covers psl's core API, not every corner of it. parse, get, isValid and errorCodes are there and are differentially tested against psl. If you depend on something outside that set, check before you switch.

It is not a validator. getDomain returning null means "no registrable domain under the Public Suffix List", which is not the same question as "is this a hostname I should accept".

Requirements

Node 18 or newer. No Node built-ins are used at runtime, so it works in browsers and edge runtimes. Zero runtime dependencies.

Security

Nothing executes on install and no network access happens at any point — the Public Suffix List is committed, not fetched. Hostname validation is character-class scanning rather than a regex, so there is no backtracking behaviour to trigger on hostile input.

See SECURITY.md for the full posture and threat model.

License

MIT © Damin3927

tldts and psl are separate projects, used here as devDependencies for differential testing only. tldkit is an independent implementation and is not affiliated with or endorsed by either.