DEV Community

jiebang-tools
jiebang-tools

Posted on

Base64 Encoding in JavaScript: A Practical Guide with Real Examples

Base64 is everywhere in web development — JWT tokens, image data URLs, API payloads, HTTP Basic auth headers. But if you've ever tried to btoa() a Chinese string and gotten an error, you know it's not as straightforward as it looks.

This guide covers the common pitfalls and practical patterns I've learned from working with Base64 in production JavaScript applications.

What Base64 Actually Does

Base64 converts binary data into ASCII text using 64 printable characters (A-Z, a-z, 0-9, +, /). Every 3 bytes of input becomes 4 characters of output, which means the encoded result is about 33% larger than the original.

This encoding exists because many protocols (email, HTTP, JSON) were designed for text, not binary. Base64 bridges that gap.

The btoa() Trap

The native btoa() function works perfectly for ASCII:

btoa("Hello World"); // "SGVsbG8gV29ybGQ="
atob("SGVsbG8gV29ybGQ="); // "Hello World"
Enter fullscreen mode Exit fullscreen mode

But try encoding anything with non-Latin1 characters:

btoa("你好世界");
// Uncaught DOMException: Failed to execute 'btoa' on 'Window':
// The string to be encoded contains characters outside of the Latin1 range.
Enter fullscreen mode Exit fullscreen mode

This trips up anyone working with internationalized content. The fix depends on your JavaScript environment.

Encoding UTF-8 Strings (Browser)

Method 1: encodeURIComponent Trick

The classic approach — escape Unicode characters first, then encode:

function encodeBase64(str) {
  return btoa(encodeURIComponent(str).replace(
    /%([0-9A-F]{2})/g,
    (_, p1) => String.fromCharCode('0x' + p1)
  ));
}

function decodeBase64(b64) {
  return decodeURIComponent(
    atob(b64).split('').map(
      c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
    ).join('')
  );
}

encodeBase64("你好世界"); // "5L2g5aW977yM5LiW55WM"
decodeBase64("5L2g5aW977yM5LiW55WM"); // "你好世界"
Enter fullscreen mode Exit fullscreen mode

Method 2: TextEncoder (Modern)

Cleaner and more readable:

function encodeBase64Unicode(str) {
  const bytes = new TextEncoder().encode(str);
  let binary = '';
  bytes.forEach(b => binary += String.fromCharCode(b));
  return btoa(binary);
}

function decodeBase64Unicode(b64) {
  const binary = atob(b64);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }
  return new TextDecoder().decode(bytes);
}
Enter fullscreen mode Exit fullscreen mode

Both methods work. I prefer TextEncoder for readability, but the encodeURIComponent approach has wider browser support if you're targeting older environments.

Converting Images to Base64 (Data URLs)

Embedding small images as Base64 data URLs eliminates HTTP requests:

function fileToDataUrl(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(reader.result);
    reader.onerror = reject;
    reader.readAsDataURL(file);
  });
}

// Usage: preview an uploaded image
input.addEventListener('change', async (e) => {
  const dataUrl = await fileToDataUrl(e.target.files[0]);
  previewImg.src = dataUrl; // data:image/jpeg;base64,/9j/4AAQ...
});
Enter fullscreen mode Exit fullscreen mode

When to use this: Icons under 4KB, single-use graphics, or when you need a self-contained HTML file.

When NOT to use this: Large images. A 2MB photo becomes a ~2.6MB string in your HTML, blocking rendering and bloating memory.

Base64 in Node.js

Node.js handles UTF-8 natively via Buffer:

// Encode
const encoded = Buffer.from('你好世界', 'utf-8').toString('base64');
// "5L2g5aW977yM5LiW55WM"

// Decode
const decoded = Buffer.from(encoded, 'base64').toString('utf-8');
// "你好世界"
Enter fullscreen mode Exit fullscreen mode

Much simpler than the browser. If you're doing Base64 work on the server, Node.js is painless.

Base64URL: The JWT Variant

JWT tokens use Base64URL, not standard Base64. The differences:

Standard Base64 Base64URL
+ -
/ _
= padding No padding
// Convert standard Base64 to Base64URL
function toBase64Url(b64) {
  return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}

// Convert back
function fromBase64Url(b64url) {
  let b64 = b64url.replace(/-/g, '+').replace(/_/g, '/');
  while (b64.length % 4) b64 += '=';
  return b64;
}
Enter fullscreen mode Exit fullscreen mode

If you're building or parsing JWTs, getting this wrong will cause silent authentication failures that are incredibly hard to debug.

Common Pitfalls

1. Base64 Is Not Encryption

This bears repeating: Base64 is encoding, not encryption. Anyone can decode it. Never use it to "protect" passwords, API keys, or sensitive data. Use proper encryption (AES, RSA) for that.

2. Memory Issues with Large Files

A 10MB video file becomes a ~13MB string. In the browser, this string lives in memory as a JavaScript string (UTF-16), so actual memory usage could be ~26MB. Processing multiple large files this way can crash tabs.

Solution: Use Blob and URL.createObjectURL() for large files instead of Base64.

3. Blocking the Main Thread

Encoding a large file synchronously will freeze the UI. Move it to a Web Worker:

// worker.js
self.onmessage = (e) => {
  const reader = new FileReaderSync();
  const result = reader.readAsDataURL(e.data.file);
  self.postMessage(result);
};

// main.js
const worker = new Worker('worker.js');
worker.postMessage({ file: largeFile });
worker.onmessage = (e) => {
  console.log('Done:', e.data.substring(0, 50) + '...');
};
Enter fullscreen mode Exit fullscreen mode

Quick Reference

// Browser: ASCII only
btoa("Hello");           // "SGVsbG8="
atob("SGVsbG8=");        // "Hello"

// Browser: UTF-8 (use TextEncoder)
const bytes = new TextEncoder().encode("你好");
btoa(String.fromCharCode(...bytes)); // "5L2g5aW9"

// Node.js: UTF-8 native
Buffer.from('你好', 'utf-8').toString('base64'); // "5L2g5aW9"

// Base64URL (for JWT)
btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');

// File to Data URL
new FileReader().readAsDataURL(file);

// HTTP Basic Auth header
'Basic ' + btoa('user:pass');
Enter fullscreen mode Exit fullscreen mode

Testing Base64 Quickly

When debugging, you often need to quickly encode or decode a Base64 string without writing code. An online Base64 encoder/decoder handles UTF-8 correctly and shows both standard and URL-safe variants — useful when you're comparing JWT payloads or debugging API responses.

The key takeaway: Base64 is simple in concept but has enough edge cases (UTF-8, URL-safe variants, memory) that it's worth understanding the underlying mechanics rather than just copy-pasting btoa() calls.


If you found this helpful, I maintain a collection of 170+ free browser-based developer tools — all processing happens client-side, no data uploaded to servers. Check out the Base64 encoder/decoder or browse the full tools directory.

Top comments (0)