Your production bundle ships anywhere from 200KB to 800KB of JavaScript, and a surprising portion of it solves problems the browser already handles natively. The date-fns library that formats a few timestamps. The lodash functions that do what Array.prototype methods have done since ES2015. The core-js polyfill that patches features every supported browser already ships. Each dependency felt like a reasonable choice when someone added it. Most have not been re-evaluated since.
Browser Baseline, a joint initiative by Google, Mozilla, Apple and Microsoft, gives you the tool to change that. It tracks exactly which web platform features are available across all major browsers and has done so long enough that the "can I use this natively?" question finally has a definitive answer.
This guide walks through how to audit your JavaScript dependencies against Baseline, identify what can be replaced with native browser APIs, and measure the real-world impact on bundle size, load time, and maintenance cost.
What Browser Baseline Actually Tracks
Baselinetakes the familiar "can I use" question and formalizes it into two tiers:
- Baseline 2023 (and similar year-stamped milestones): features that became available in all four major browser engines, Chrome, Edge, Safari, and Firefox, within that calendar year.
- "Widely available": features that have been in all four engines for at least 30 months. This is the safe default for any company that supports "current and previous version" of each browser.
The data lives in the web-features repository on GitHub, because data and is structured as a real dataset, not a blog post, not a forum thread. You can query it programmatically, integrate it into your CI pipeline, and automate dependency audits. That is the shift from "I hope this works" to "I can prove this works on every browser we support."
Why This Matters for Bundle Size
The median JavaScript payload for a mobile web page is around 530 KB uncompressed (HTTP Archive, mid-2025 data). Core-js polyfills alone account for 40-90 KB in many bundles because teams include them via @babel/preset-env without explicitly defining their browser targets. Meanwhile, the features those polyfills cover, Array.prototype.flat, Object.fromEntries, String.prototype.replaceAll, have been Baseline "widely available" for over two years.
Removing a single outdated polyfill config does not feel heroic. But when you audit the full dependency tree and replace what the browser now provides, you routinely cut 30-35% of total JavaScript weight. Not from clever code splitting or tree shaking, from not shipping code the browser already includes.
A Step-by-Step Dependency Audit Against Baseline
Here is the process we use at ProjectMakers when advising teams on bundle optimization. It works whether you run it yourself or bring in outside help.
Step 1: Generate Your Dependency Inventory
Start by listing everything your bundle actually ships. npm ls --all shows the full tree, but for bundle-focused auditing, tools like Bundlephobia or npx bundlephobia-deps <package> are more useful, they show the minified + gzipped weight of each dependency including sub-dependencies.
# Full dependency tree with install size
npm ls --all --long 2>/dev/null | grep -E "^\|"
# Quick bundle weight analysis for your top-level deps
npx bundlephobia-deps date-fns lodash-es zod
Typical output for a mid-size SaaS product's front end:
date-fns 12.3 kB gzip (tree-shakeable, but only if imported correctly)
lodash-es 4.2 kB per function (12-70 kB depending on usage)
core-js 45-90 kB (varies by config)
whatwg-fetch 3.3 kB
Write these numbers down. They are your baseline, pun intended, for measuring improvement.
Step 2: Map Dependencies to Browser-native Replacements
This is where the real savings are. Here are the most common categories and their native equivalents:
Date Formatting & Manipulation
Library: date-fns, moment.js, dayjs → Native: Intl.DateTimeFormat, Intl.RelativeTimeFormat, Temporal (stage 3, shipping in Chrome 128+/Firefox 132+)
Before (with date-fns):
import { format, formatDistanceToNow } from 'date-fns';
import { de } from 'date-fns/locale';
const formatted = format(new Date(), 'dd.MM.yyyy HH:mm', { locale: de });
const relative = formatDistanceToNow(new Date(2026, 2, 1), { addSuffix: true, locale: de });
After (native Intl API, Baseline widely available since 2024):
const formatter = new Intl.DateTimeFormat('de-DE', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit'
});
const formatted = formatter.format(new Date());
const rtf = new Intl.RelativeTimeFormat('de-DE', { numeric: 'auto' });
const relative = rtf.format(
Math.round((new Date(2026, 2, 1) - Date.now()) / (1000 * 60 * 60 * 24)),
'day'
);
Savings: removing date-fns from a tree-shaken bundle typically saves 12-30 KB gzipped, depending on which functions you used. If your project still uses moment.js, congratulations, you are about to drop between 65 and 230 KB unminified.
Utility Functions (lodash, underscore)
Library: lodash-es/debounce, lodash-es/throttle, lodash-es/cloneDeep, lodash-es/merge → Native: vanilla JavaScript
Most commonly used lodash functions have one- or two-line native equivalents:
// debounce, replaces lodash/debounce
function debounce(fn, ms = 300) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), ms);
};
}
// Deep clone, replaces lodash/cloneDeep (Baseline widely available 2024)
const clone = structuredClone(original);
// Object merging, replaces lodash/merge for shallow cases
const merged = { ...defaults, ...overrides };
// Array chunking, replaces lodash/chunk
function chunk(arr, size) {
return Array.from({ length: Math.ceil(arr.length / size) }, (_, i) =>
arr.slice(i * size, i * size + size)
);
}
structuredClone is particularly important: it handles circular references, Date, Map, Set, RegExp, ArrayBuffer, and Blob, edge cases that trip up hand-rolled clone implementations. It has been Baseline "widely available" since early 2024.
Savings: even importing individual lodash-es functions adds up to 15-40 KB in a typical project. Each native replacement is free.
Polyfills and Transpilation
Library: core-js, whatwg-fetch, abortcontroller-polyfill → Native: ships in every modern browser
If your browserslist or @babel/preset-env config looks like this:
{
"browserslist": "> 0.25%, not dead"
}
You are likely polyfilling features that every browser matching that query already supports. Run npx browserslist to see the actual resolved browser list. Then check each polyfill against Baseline.
Better yet, use the Baseline browser configuration to update your targets:
{
"browserslist": "baseline widely available"
}
Or for feature-based targeting with Vite:
// vite.config.js, Vite 6+ supports Baseline targeting
import { defineConfig } from 'vite';
export default defineConfig({
build: {
target: 'baseline-widely-available' // or 'baseline-2024'
}
});
Savings: the most dramatic single change. Teams that move from a generic browserslist to explicit Baseline targeting typically drop 50-120 KB from their polyfill payload. One editorial tool we audited went from 340 KB to 210 KB JavaScript just by tightening browser targets and removing core-js imports that were no longer needed.
URL and Routing Libraries
Library: url-parse, qs (for simple cases) → Native: URL, URLSearchParams, Baseline widely available since 2020
// Instead of url-parse
const url = new URL('https://example.com/path?key=value');
// Instead of qs for simple query parsing (Baseline widely available)
const params = new URLSearchParams(window.location.search);
const value = params.get('key');
Step 3: Enforce Baseline Compliance in Your CI Pipeline
Auditing once is good. Preventing regression is better. Two approaches:
Option A: ESLint plugin
npm install --save-dev eslint-plugin-compat
// .eslintrc.json
{
"plugins": ["compat"],
"env": {
"browser": true
},
"rules": {
"compat/compat": "warn"
},
"settings": {
"browserslist": ["baseline widely available"]
}
}
This will flag any API call that is not available in your target browsers, before it reaches production.
Option B: Build-time baseline check with Vite
// vite.config.js
import { defineConfig } from 'vite';
import baseline from 'vite-plugin-baseline';
export default defineConfig({
plugins: [
baseline({
target: 'widely-available',
// warns on features not yet in baseline, blocks on removed features
violations: 'error'
})
]
});
Step 4: Measure the Impact
After replacing dependencies with native equivalents, re-run your bundle analysis:
# Compare before/after bundle sizes
npx source-map-explorer dist/assets/*.js
# Or with Vite
npx vite-bundle-visualizer
Realistic numbers from a typical audit:
That is 138-330 KB less JavaScript the browser must download, parse, and execute on every page load. On a 3G connection (1.5 Mbps effective), that is a 0.7-1.8 second faster time-to-interactive. On fast connections, the savings in parse time alone are measurable, JavaScript parse cost is roughly 0.25-0.5 ms per KB on a mid-range mobile device.
When Native APIs Are Not Enough
Baseline tells you what works everywhere. It does not always tell you what works the same everywhere. Edge cases to watch:
Intl.DateTimeFormatoutput differences: trailing spaces and non-breaking space characters vary between Safari and Chromium. If you compare formatted strings in tests, normalize whitespace.structuredClonelimitations: cannot clone functions, DOM nodes, or certain Error subclasses. If your data model includes those, you still need a specialized library.TemporalAPI: not yet Baseline "widely available" (expected 2026-2027). If you needTemporalspecifically, you may still need a polyfill, but a much thinner one.URLclass in non-browser environments: Node.js hasURLbuilt in, but edge runtimes like Cloudflare Workers had gaps until recently. Verify for your deployment target.
For teams that need to support older browsers (corporate environments stuck on Chrome 96, for example), the Baseline "widely available" target may not fit. In those cases, a team experienced in browser compatibility work can help you set the right boundaries between polyfilled features and the Baseline-safe core.
Five Best Practices for Shipping Less JavaScript
-
Set a Baseline browser target explicitly, do not rely on the default
> 0.25%, not deadbrowserslist. Writebaseline widely availableorbaseline 2024and review it every 6 months as browsers ship new features. -
Audit before you optimize, run
npx bundlephobia-depson yourpackage.json. Sort by gzip size. Attack the biggest dependencies first. Often 3-4 libraries account for 70% of replaceable bytes. -
Replace one dependency per sprint, do not attempt a "big bang" migration. Swap
moment.jsthis sprint, auditlodashnext sprint, tighten the browserslist the sprint after. Each change is independently testable and deployable. -
Block new polyfill-heavy PRs at CI, adding
eslint-plugin-compatto your pipeline surfaces incompatible API usage and unnecessary polyfill imports in pull requests, not in production performance reports. -
Measure parse time, not just download size, a 100 KB JavaScript file costs far more than a 100 KB image because the browser must parse, compile, and execute it. Use Lighthouse's "Reduce JavaScript execution time" audit to track the real cost of what you ship.
This kind of dependency audit fits naturally into a broader front-end performance strategy. If your team is building or modernizing a web application, establishing these defaults early costs a fraction of retrofitting them later, and every sprint builds on a smaller, faster foundation.
The Bottom Line
The web platform has quietly absorbed what used to require third-party libraries. Baseline makes that progress measurable and actionable. A dependency audit against Baseline is not a rewrite, it is removing code that the browser now ships for free. For most mid-size SaaS products, that means 150-300 KB less JavaScript per page load, faster time-to-interactive, fewer transitive dependencies, and a smaller supply chain attack surface.
Start with the dependency inventory. Run the numbers. Ship less.