DEV Community

Cover image for 10 Browser APIs That Can Replace npm Packages (and When They Can't)
Loknath Patel
Loknath Patel

Posted on

10 Browser APIs That Can Replace npm Packages (and When They Can't)

Many of us type npm install without thinking twice. But browsers keep adding built-in APIs that can handle tasks we once needed a package for. Sometimes, those packages stay in our projects simply because nobody stopped to check whether we still need them.

I checked these 10 APIs against MDN and the web-platform-dx web-features data. I also tested the key parts of each example in the Chrome DevTools console on desktop. For each API, you'll see what it can replace, how to use it, where it falls short, and its Baseline support status.

A quick note on Baseline: "Newly available" means a feature works in the latest versions of all major browsers. "Widely available" means it's been supported across those browsers for a while.

This isn't an argument against using dependencies. It's about knowing what's already available before adding another package to your project.

Check the platform first, then decide.


1. structuredClone() replaces lodash cloneDeep

Deep cloning JavaScript objects used to mean either reaching for JSON.parse(JSON.stringify(obj)) or installing lodash. The JSON approach has its own problems: Dates become strings, Maps and Sets lose their contents, and circular references cause errors.

For supported data types, structuredClone() offers a built-in alternative.

// Before
import cloneDeep from 'lodash/cloneDeep';
const copy = cloneDeep(user);

// After
const copy = structuredClone(user);
Enter fullscreen mode Exit fullscreen mode

It can clone Dates, Maps, Sets, Blobs, ArrayBuffers, typed arrays, and circular references.

Caveat: Functions and DOM nodes can't be cloned. If a value can't be structured-cloned, the method throws a DataCloneError. Class instances also lose their custom prototype, so their prototype methods won't be preserved. If your code depends on those behaviors, keep cloneDeep.

Support: Baseline since March 2022 (Chrome 98, Firefox 94, Safari 15.4).


2. crypto.randomUUID() replaces uuid

The uuid package is useful, but if all you need is a random v4 UUID, the browser already has a method for that.

// Before
import { v4 as uuidv4 } from 'uuid';
const id = uuidv4();

// After
const id = crypto.randomUUID();
// e.g. '36b8f84d-df4e-4d49-b662-bcde71a8764f'
Enter fullscreen mode Exit fullscreen mode

That's all you need for a standard v4 UUID.

Caveat: crypto.randomUUID() only generates v4 UUIDs and requires a secure context, such as HTTPS or localhost. If you need other versions, such as v1, v5, or v7, the uuid package may still be the better choice.

Support: Baseline since March 2022.


3. fetch() + AbortSignal.timeout() replaces axios for simple requests

For straightforward HTTP requests, the built-in fetch() API can do the job without axios. You can also use AbortSignal.timeout() to set a timeout without creating your own timer.

Here's what the difference looks like:

// Before
const { data } = await axios.get('/api/users', { timeout: 5000 });

// After
try {
  const res = await fetch('/api/users', {
    signal: AbortSignal.timeout(5000),
  });
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const data = await res.json();
} catch (err) {
  if (err.name === 'TimeoutError') {
    // the request took longer than 5 seconds
  } else {
    throw err;
  }
}
Enter fullscreen mode Exit fullscreen mode

There are a few differences worth knowing before switching.

Caveats:

  • fetch() doesn't reject its promise just because the server returns a 404 or 500 response. Check res.ok to handle HTTP errors yourself.
  • A timeout normally produces a TimeoutError. In Chrome 103 through 123, however, it produced an AbortError, so the TimeoutError check above won't match those versions.
  • If your application relies on axios interceptors, replacing axios with fetch() means implementing that behavior yourself.

Support: AbortSignal.timeout() is Baseline Newly available since April 2024 (Chrome 124, Firefox 100, Safari 16). The web-features data expects it to become Widely available on 18 October 2026.


4. Intl.RelativeTimeFormat replaces Moment for "time ago"

Moment is a large date library, and its own website describes it as a legacy project in maintenance mode. If you only need to display text such as "3 minutes ago", a browser API can handle the formatting.

The catch is that Intl.RelativeTimeFormat doesn't calculate the time difference or choose the right unit for you. You'll need a small helper for that.

const DIVISIONS = [
  { amount: 60, unit: 'second' },
  { amount: 60, unit: 'minute' },
  { amount: 24, unit: 'hour' },
  { amount: 7, unit: 'day' },
  { amount: 4.34524, unit: 'week' },
  { amount: 12, unit: 'month' },
  { amount: Infinity, unit: 'year' },
];

const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });

function timeAgo(date) {
  let duration = (date - Date.now()) / 1000;
  for (const { amount, unit } of DIVISIONS) {
    if (Math.abs(duration) < amount) {
      return rtf.format(Math.round(duration), unit);
    }
    duration /= amount;
  }
}

timeAgo(new Date(Date.now() - 3 * 60 * 1000)); // '3 minutes ago'
Enter fullscreen mode Exit fullscreen mode

Setting numeric: 'auto' gives you natural phrases such as "yesterday" instead of "1 day ago". The formatter also supports localization.

Caveat: This only handles relative-time formatting. If you need date parsing, date arithmetic, or time-zone handling, a dedicated library is still useful. Moment's own team recommends considering alternatives such as Luxon, Day.js, and date-fns.

Support: Baseline Widely available since March 2023.


5. URLSearchParams replaces qs and query-string for flat query strings

If you're building or reading simple URL query strings, you may not need a separate package.

// Before
import qs from 'qs';
const query = qs.stringify({ q: 'web apis', page: 2 });

// After
const params = new URLSearchParams({ q: 'web apis', page: 2 });
params.toString(); // 'q=web+apis&page=2'

const url = new URL('https://example.com/search?q=web+apis&page=2');
url.searchParams.get('q'); // 'web apis'
Enter fullscreen mode Exit fullscreen mode

URLSearchParams handles encoding and decoding query parameters for you. You can also use it through a URL object's searchParams property to read or update an existing URL.

Caveat: It doesn't serialize nested objects or arrays in the same way as qs. If your API expects bracket notation or a particular nested-query format, keep the package that handles it.

Support: Baseline Widely available.


6. IntersectionObserver replaces scroll listeners for visibility checks

You don't always need to listen for every scroll event just to find out whether an element is visible. IntersectionObserver notifies you when an element enters or leaves a viewport, or another observation area, without requiring you to calculate its position on every scroll.

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      entry.target.classList.add('visible');
      observer.unobserve(entry.target);
    }
  });
}, { threshold: 0.2 });

document.querySelectorAll('.card').forEach((el) => observer.observe(el));
Enter fullscreen mode Exit fullscreen mode

This example adds a visible class when a card intersects the viewport, then stops observing that card.

The threshold: 0.2 option controls when the callback runs, and entry.isIntersecting only tells you the element intersects the viewport. If you need "at least 20% visible", check entry.intersectionRatio >= 0.2 instead.

You can use the same API for infinite scrolling, scroll-triggered animations, lazy loading, and visibility analytics.

Caveat: If all you need is lazy-loaded images, the native loading="lazy" attribute on an <img> element may be enough. And if you're working with React or another framework, a wrapper library can still save you some setup.

Support: Baseline Widely available since September 2021 (Chrome 58, Firefox 55, Safari 12.1).


7. ResizeObserver replaces window resize listeners for element sizes

The window's resize event tells you when the window changes size. It doesn't tell you whenever a particular element changes size.

That's where ResizeObserver comes in. It watches an element directly, making it useful for charts, dashboards, and responsive components whose dimensions can change even when the window stays the same.

const ro = new ResizeObserver((entries) => {
  for (const entry of entries) {
    console.log(entry.contentRect.width);
  }
});

ro.observe(element);
Enter fullscreen mode Exit fullscreen mode

Here, the observer logs the content width whenever the observed element's size changes.

Caveat: Be careful when changing an element's dimensions inside its own resize callback. That can trigger further observations and cause a "ResizeObserver loop" warning. Handle layout updates carefully to avoid repeated changes.

Support: Baseline Widely available since January 2023 (Chrome 64, Firefox 69, Safari 13.1).


8. Page Visibility API replaces small tab-visibility helper libraries

People often leave a tab open while working somewhere else. If your application keeps polling an API or running unnecessary tasks in the background, you may be wasting resources.

The Page Visibility API lets you respond when a document becomes hidden or visible.

const video = document.querySelector('video');

document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    video.pause();
    // also stop polling here
  } else {
    // the tab is visible again: resume or refresh data
  }
});
Enter fullscreen mode Exit fullscreen mode

You can use this event to pause videos, stop polling, and reduce unnecessary CPU and battery use while the page isn't visible.

Caveat: A hidden tab doesn't necessarily mean the user has left your application. They might simply be looking at another tab. Use this API to manage background work, not to decide whether a session has ended.

Support: Long established and supported in all major browsers.


9. BroadcastChannel replaces the localStorage event trick for cross-tab messages

One way to synchronize browser tabs is to write data to localStorage and listen for the storage event. It works, but there's a built-in API designed specifically for messaging between contexts: BroadcastChannel.

// Tab 1
const channel = new BroadcastChannel('app-sync');
channel.postMessage({ type: 'theme-changed', theme: 'dark' });

// Tab 2
const channel = new BroadcastChannel('app-sync');
channel.onmessage = (event) => {
  console.log(event.data); // { type: 'theme-changed', theme: 'dark' }
};
Enter fullscreen mode Exit fullscreen mode

Contexts using the same channel name can exchange messages. This works across tabs, windows, iframes, and workers, provided they share the relevant origin and storage partition.

One practical example is logging a user out across all open tabs when their session ends.

Caveat: Messages only reach contexts that share the same storage partition. For example, an iframe from another site embedded in your page can't communicate through this API with a regular tab of that other site, even if both have the same origin.

Support: Baseline since March 2022 (Chrome 54, Firefox 38, Safari 15.4).


10. Clipboard API replaces document.execCommand('copy') and clipboard.js for copying text

If your application needs a simple "Copy" button, the modern Clipboard API can handle it without a separate library.

copyButton.addEventListener('click', async () => {
  try {
    await navigator.clipboard.writeText('Copied!');
  } catch (err) {
    console.error('Copy failed:', err);
  }
});
Enter fullscreen mode Exit fullscreen mode

The writeText() method copies the supplied string to the clipboard and returns a promise, so you can handle success or failure asynchronously.

Caveat: The API requires a secure context, such as HTTPS or localhost. Clipboard access is also subject to browser permission rules and user-activation requirements. Calling it from a click handler, as shown above, is a practical approach.

Support: writeText() is Baseline Widely available since March 2020.


Quick summary

You may be installing Built-in alternative Keep the package if
lodash cloneDeep structuredClone() You need to clone functions or preserve class prototypes
uuid crypto.randomUUID() You need UUID versions other than v4
axios fetch() + AbortSignal.timeout() You rely on interceptors
Moment (time ago) Intl.RelativeTimeFormat You need parsing, date math, or time-zone support
qs URLSearchParams You need nested objects or arrays
Scroll listeners IntersectionObserver You want a ready-made framework wrapper
Window resize hacks ResizeObserver You need to support very old browsers
Visibility helper libraries Page Visibility API You need richer idle or activity detection
localStorage event trick BroadcastChannel You need to synchronize across devices, which requires a server
clipboard.js Clipboard API You must support non-HTTPS pages

These APIs aren't always direct replacements for the packages listed here. But for many everyday tasks, the browser already provides what you need. Removing an unnecessary dependency also means one less package to maintain and audit.


What this doesn't cover

  • Older browsers: I've included Baseline support information, but if your project targets older browsers, check caniuse.com against your actual browser requirements.
  • Other browsers: I tested the examples in Chrome on desktop only. Test them in the browsers your project supports before shipping.
  • Performance and bundle size: I haven't benchmarked these alternatives. Check Bundlephobia or your own build output if you want actual bundle-size numbers.
  • Why these packages exist: qs supports nested query strings, axios offers interceptors, and date libraries handle parsing and time zones. The point isn't to remove every dependency. It's to understand what you're installing and why.

Which package have you replaced with a browser API recently? And did the native alternative cover everything you needed, or did you run into a limitation? Let me know in the comments.

Top comments (6)

Collapse
 
koda2026 profile image
Harun - solo dev •

lokhnath, this is exactly the constraint-driven philosophy that allowed me to build koda (a mobile-first ai code mentor) in just 142kb! 🐯

when you're building for users on budget phones and spotty 3g connections, every single npm package is a liability. bloated dependencies drain battery, increase load times, and introduce supply chain risks.

we rely heavily on native browser apis for this exact reason. for example, using crypto.randomUUID() for session ids, the native Clipboard API for saving code snippets to the library, and BroadcastChannel to sync state in our live study rooms without pulling in heavy external websocket libraries.

your point about "check the platform first, then decide" should be the golden rule of modern frontend development. removing unnecessary dependencies isn't just about bundle size; it's about respecting the user's hardware, data caps, and privacy.

fantastic, highly practical breakdown! which of these 10 have you successfully removed from your own stack recently? πŸš€

Collapse
 
loknath_patel_aafc07267d5 profile image
Loknath Patel •

Thanks Harun, and 142kb for a mobile-first app is impressive.

To answer your question honestly: I haven't removed these from a production project yet. For this article I checked each API against MDN and tested the examples in Chrome DevTools, so I can only speak for that.

Your BroadcastChannel use case is interesting. Since it only reaches tabs in the same browser, how do you sync the live study rooms across different devices? Do you use a server or WebSocket for that part?

Collapse
 
koda2026 profile image
Harun - solo dev •

thanks for the kind words, loknath! honestly, keeping it at 142kb was the hardest but most rewarding constraint.

great catch on the broadcastchannel nuance! you are exactly right: broadcastchannel is strictly for same-browser, same-origin tab sync (like keeping the ui state consistent if a user accidentally opens two tabs of koda).

for actual cross-device study rooms, we rely on lightweight websockets. to keep it strictly mobile-first and 3g-friendly, we don't sync heavy state or dom elements. we only transmit the raw text payload, the room id, and basic typing indicators.

by keeping the websocket payloads as lean as possible (just the essential json), we ensure that the live study rooms don't eat up a student's data cap, while still giving that real-time "pair programming" feel.

it’s all about using the right tool for the right boundary: native apis for local tab state, and lean websockets for cross-device sync!

Thread Thread
 
loknath_patel_aafc07267d5 profile image
Loknath Patel • • Edited

Thanks for the detailed breakdown, Harun! The "right tool for the right boundary" approach makes perfect sense. Using native APIs for local tab state and lean WebSockets for cross-device sync is a solid architecture.

I really like the focus on keeping payloads minimal (just essential JSON and typing indicators) for mobile-first and 3G users. It shows you're genuinely prioritizing the user experience over just shipping features.

Appreciate you sharing these insights. Best of luck with Koda and the live study rooms!

By the way, I'd love to check out Koda if there's a public link or beta version available. No pressure though, just curious to see the live study rooms in action!

Collapse
 
idanshalem profile image
Idan Shalem •

Good call putting BroadcastChannel in this list - the localStorage + storage event trick deserves retirement. Two caveats worth knowing before swapping it in for shared state, though. BroadcastChannel is fire-and-forget: a tab that opens after a message was sent sees nothing (with localStorage the value at least persisted for late joiners), so you need an explicit sync-on-join handshake if tabs share state. And the posting tab never receives its own message, so you update local state separately from broadcasting it. Both are easy to handle once you know about them and painful to debug when you don't. I maintain a small React hook library (react-broadcast-sync) for cross-tab messaging; its docs show the explicit late-tab handshake and local-state update patterns.

Some comments may only be visible to logged-in visitors. Sign in to view all comments.