The problem: polyfills never leave
A polyfill is a deal you make with the past. A browser doesn't support some
feature yet, so you npm install a shim, ship it, and move on. The deal is
supposed to be temporary — but nobody ever collects the other half.
Years pass. The feature ships in every browser. The polyfill is now pure dead
weight: it bloats your bundle, slows your installs, and adds one more link to
your supply-chain attack surface. And nothing tells you to delete it.
- Renovate / Dependabot dutifully bump the polyfill's version forever. They keep the corpse fresh; they never tell you it's a corpse.
- Linters flag some redundant dependencies, but they don't act, and they don't know whether the underlying feature is actually safe to rely on yet.
So @oddbird/css-anchor-positioning, @ungap/structured-clone,
array.prototype.flat, and a dozen friends quietly live in your package.json
for years after they stopped doing anything.
The signal: Web Platform Baseline
The missing piece is a trustworthy, dated answer to "is this feature safe to
use natively yet?" That signal now exists: Baseline.
The web-features dataset
tags each web feature with a Baseline status. The one that matters here is
Widely available (status.baseline === "high") — roughly 30 months after
the feature reached every major browser engine. Once a feature is Widely
available, the polyfill that shimmed it is, by definition, redundant.
That's a calendar-driven trigger: a polyfill becomes removable on a specific
day, whether or not anyone is paying attention.
The tool: baseline-polyfill-pruner
baseline-polyfill-pruner watches that signal for you and removes the dead
polyfills it finds. It runs as a CLI, or — better — as a GitHub Action that
opens a pull request the moment a polyfill becomes removable.
How it decides
A dependency becomes a removal candidate only when both of these are true:
baseline-high(feature) AND browserslist-targets-all-support(feature)
→ polyfill is dead weight → removal candidate
-
Baseline says so. The feature the polyfill maps to is Baseline Widely
available in the
web-featuresdataset. -
Your targets agree. Your project's own
browserslisttargets all support that feature natively.
Step 2 is the important one. "Widely available" is a global statement about
the web. But if your browserslist still targets ie 11 or an ancient
Safari, the polyfill is still doing real work — so the tool keeps it.
The guiding principle is "when in doubt, don't remove." A missed removal is
harmless (you keep a dependency a little longer). A wrong removal breaks
someone's build. The whole design is biased toward the safe mistake.
How it maps polyfills to features
A hand-curated registry maps ~30 well-known polyfills (the es-shims
ECMAScript ponyfills, plus DOM/CSS shims like intersection-observer,
dialog-polyfill, whatwg-fetch, and @ungap/structured-clone) to their
web-features ids.
Crucially, every id in the registry is checked against the live dataset in
CI. If a feature gets renamed or someone fat-fingers an id, the build fails —
instead of silently mis-flagging your dependency. It's quality over quantity by
design.
Using it
As a CLI
baseline-prune --diff # report removable polyfills (default, no writes)
baseline-prune --fix # remove them from package.json + list import sites
baseline-prune --json # machine-readable report
baseline-prune --cwd ./app # target a specific project root
A dry run looks like this:
would remove array.prototype.flat — array-flat is Baseline Widely available (since 2022-07-15)
✓ 1 removable polyfill(s) found.
Run with --fix to apply.
--fix removes the dependency from package.json — preserving your
indentation, key order, and trailing newline — and then hands you a checklist of
every import site that still references it:
removed array.prototype.flat — array-flat is Baseline Widely available (since 2022-07-15)
⚠ 2 import site(s) still reference the removed package(s).
Remove these manually, then run your build/tests:
- src/list.js:2 (array.prototype.flat)
- src/list.js:8 (array.prototype.flat)
Note what --fix doesn't do: it never rewrites your source code. Automatic
import-site rewriting (an AST codemod) is deliberately out of scope. A wrong
edit to your source breaks builds and burns trust — so the tool edits the
manifest and lets you handle the import lines.
As a GitHub Action (the point)
The real value is automation. A polyfill becomes removable on the day Baseline
flips, and no human remembers to re-check. So put it on a schedule:
# .github/workflows/baseline-prune.yml
name: Baseline polyfill prune
on:
schedule:
- cron: "0 9 * * 1" # Mondays
workflow_dispatch: {}
permissions:
contents: write
pull-requests: write
jobs:
prune:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: mk668a/baseline-polyfill-pruner@v1
Every Monday, the Action runs the engine, applies --fix, and — if it finds
anything — opens a PR titled "Drop N polyfills Baseline reports as widely
available" with a rationale table and the import-site checklist in the body.
You review it and merge with confidence.
Why this didn't already exist
| Tool | What it does | The gap |
|---|---|---|
eslint-plugin-depend |
Lints for redundant deps | Lint-only, no autofix, no Baseline |
module-replacements-codemods |
Codemods deps → native | Doesn't key off Baseline status |
| Renovate / Dependabot | Bump versions | Never say "this dep is removable" |
Each piece existed. The combination — a live Baseline trigger plus an
automatic removal PR — is the thing none of them do. That combination is the
whole idea.
One design note worth stealing
baseline-polyfill-pruner uses no LLM at runtime. web-features is a
static, versioned dataset, and the removal decision is deterministic date
arithmetic plus a browserslist check. AI helped build the tool, but the
shipped binary is boring, predictable, and offline — exactly what you want from
something that edits your package.json on a cron schedule.
baseline-polyfill-pruner is MIT-licensed. Source:
github.com/mk668a/baseline-polyfill-pruner.
Top comments (0)