Toolium

Five browser APIs you probably should be using

Hemanth Gedda12 min read

Last year I shipped a base64 encoder that broke for anyone with non-ASCII characters in their input. The bug was that I had used btoa() directly, and btoa() only accepts strings where every character codepoint is in the 0-255 range. A Japanese product name has characters outside that range, so btoa throws an exception. A user reported it on a Wednesday morning. By that afternoon I had the fix in production: encode the string to UTF-8 bytes via TextEncoder first, then run btoa on the bytes via String.fromCharCode.

The fix has been possible in browsers since 2018. I just had not reached for it. The legacy pattern (charCodeAt loop, manually handle multi-byte chars) was still in my head from years of doing it that way.

This is a generalizable problem. Browsers have shipped excellent APIs for solving real problems, and most JavaScript code still does not use them, partly because of legacy habits and partly because of poor discoverability. Here are five I think more code should reach for.

1. TextEncoder and TextDecoder

The fix for the btoa story. TextEncoder encodes a JavaScript string to UTF-8 bytes; TextDecoder decodes UTF-8 bytes back to a string. Available in all browsers since 2018.

Use cases I keep hitting:

  • Converting strings to ArrayBuffer for hashing, encryption, or transmission.
  • Round-tripping Unicode strings through any byte-oriented API.
  • Replacing String.fromCharCode plus charCodeAt loops with something that handles multi-byte characters correctly.
const bytes = new TextEncoder().encode("Hello, 世界");
// bytes is a Uint8Array of UTF-8 byte values
const text = new TextDecoder().decode(bytes);
// text is "Hello, 世界" again

The bug if you do not use it: any code that assumes one character equals one byte breaks for any non-ASCII input. This is the bug that hit Toolium's base64 encoder, JWT decoder, and word counter in the first six months.

2. Intl.Segmenter

Splits text on word, sentence, or grapheme boundaries using locale-aware logic. Available in all browsers since 2022 (Safari 14.1 in March 2021, then everyone else within a year).

Use cases:

  • Counting words in Chinese, Japanese, or Thai. These languages do not use spaces between words, so splitting on whitespace gives the wrong answer.
  • Counting characters where compound emojis should count as one grapheme.
  • Wrapping text at safe boundaries for layout or truncation.
const segmenter = new Intl.Segmenter("ja", { granularity: "word" });
const segments = [...segmenter.segment("日本語のテキスト")];
const wordCount = segments.filter(s => s.isWordLike).length;
// wordCount is around 3, which matches what a native speaker would count

The bug if you do not use it: word counts for CJK languages come out wrong by a factor of 3-5 because each character gets counted as a word. The first version of Toolium's Word Counter had this exact bug.

Grapheme segmentation is the other underused part of Intl.Segmenter. Compound emojis like the family-of-four glyph are technically four codepoints with zero-width joiners; without grapheme segmentation, they count as four characters when a user would count them as one.

3. SubtleCrypto

Cryptographic primitives in the browser, exposed via crypto.subtle. Available in all modern browsers, with stable SHA-256 and SHA-512 since around 2017.

Use cases:

  • SHA-256 and SHA-512 hashing of strings or files.
  • HMAC for signature verification on incoming webhooks.
  • Random number generation via crypto.getRandomValues (technically separate but related).
  • Public-key crypto (RSA-OAEP, ECDSA, ECDH) for end-to-end encryption use cases.
async function sha256Hex(text) {
  const bytes = new TextEncoder().encode(text);
  const hash = await crypto.subtle.digest("SHA-256", bytes);
  return [...new Uint8Array(hash)]
    .map(b => b.toString(16).padStart(2, "0"))
    .join("");
}
const result = await sha256Hex("hello");
// result is "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"

What people reach for instead and shouldn't: third-party crypto libraries that ship 50KB of JavaScript for what SubtleCrypto handles in 50 bytes of code. The classic offender is js-sha256, which is fine but completely redundant with the native API.

For password hashing specifically, SubtleCrypto is the wrong choice. SHA-256 is too fast, which makes brute-force attacks feasible. Use bcrypt, scrypt, or Argon2 via a real library (or, better, do password hashing on the server).

4. Web Workers

Off-main-thread JavaScript execution. Available since the dawn of time. Underused for non-obvious reasons.

The main reason developers skip workers is that the message-passing API is awkward. You post a message in, you get a message out, you have no easy way to share state. Modern wrappers (Comlink from Google, for example) hide the message-passing behind a Promise-based API that feels normal.

Even without a wrapper, the bare API solves real problems:

  • Anything that takes more than 16ms to compute. Otherwise it drops a frame.
  • CPU-bound operations like regex, parsing, hashing of large files.
  • Operations that might hang and need a timeout. This is the pattern I use most.
// regex-worker.js
self.onmessage = (e) => {
  const { pattern, input, flags } = e.data;
  const regex = new RegExp(pattern, flags);
  const matches = [...input.matchAll(regex)];
  self.postMessage({ matches });
};

// main.js
const worker = new Worker("/regex-worker.js");
const timer = setTimeout(() => {
  worker.terminate();
  console.warn("regex timed out");
}, 2000);
worker.onmessage = (e) => {
  clearTimeout(timer);
  // handle e.data.matches
};
worker.postMessage({ pattern, input, flags: "g" });

The catastrophic-backtracking pattern (the regex (a+)+b against a long string of a's) hangs the JavaScript engine indefinitely. Without a worker, that hangs your page and the user has to close the tab. With a worker, the main thread stays responsive and you can terminate the worker after a timeout.

5. File System Access API

Lets the user pick a file or directory from disk and read/write to it directly. Available in Chrome since 2020, but Safari and Firefox do not support it yet.

Use cases:

  • Editing files in place without the download-edit-reupload cycle.
  • Batch operations across a directory (process every file, write the output back to a sibling directory).
  • Persistent file handles across sessions, so a workflow can resume where it left off.
// User picks a file
const [handle] = await window.showOpenFilePicker();
const file = await handle.getFile();
const text = await file.text();
// modify text...
const writable = await handle.createWritable();
await writable.write(newText);
await writable.close();

This is the one I have not built into Toolium yet but probably should. The use case for Toolium: drag a folder of images onto Image Compressor, write the compressed versions back to the same folder. Right now you have to download each one. With File System Access, the round-trip is one click.

The reason I have not is the Safari and Firefox gap. About 40% of Toolium users are not on Chrome, and a feature that silently does not work for them is worse than a feature that does not exist. When Safari ships support (the progress on this in 2025 was encouraging but inconclusive), the in-place workflow becomes plausible.

The bigger point

The pattern across all five: each replaces 50 to 200 lines of legacy code or third-party library with 5 to 10 lines of native API. The APIs work. They are documented on MDN. They are supported in every major browser.

What stops adoption is mostly habit. JavaScript developers learned patterns in the 2010s that involved a lot of third-party libraries for things the platform now handles. The default I would argue for in 2026: before reaching for a library, check if there is a native API for it. There usually is.

The list above is not exhaustive. Other underused APIs worth knowing: IntersectionObserver for scroll-triggered behavior, requestIdleCallback for non-critical work, the Cache API for offline-capable apps, BroadcastChannel for cross-tab messaging. Each one closes a real gap that used to need third-party code.