Why Large Files Don't Work Well with Base64?
And When Should You Use It?
Base64 is a convenient way to turn a file into a text string, but it comes with real costs: larger size, doubled memory, and browser freezes. This article explains why, and helps you decide when to use Base64 and when to switch to a better option.
Why Base64 Makes Files Larger
The core idea behind Base64 is: take 3 bytes (24 bits) of raw data at a time, split them into four 6-bit groups, and represent each group with one of 64 printable ASCII characters (A–Z, a–z, 0–9, +, /).
The result: 3 bytes become 4 characters. Each character takes 1 byte, so the size grows to 4/3 ≈ 133% of the original.
Encoding Example
+33%
Size Increase
If the original data length is not a multiple of 3, = padding is added to make it a multiple of 4, so the actual increase is slightly more than 33%.
For example: a 5 MB image becomes about 6.7 MB in Base64; a 100 MB video becomes about 133 MB — 33 MB more than the original.
Why Browsers Freeze When Converting Large Files
The most common approach is FileReader.readAsDataURL(file). The problem lies in how this API works:
- 1Reads the entire file into memory:The whole file is loaded into an ArrayBuffer at once. A 50 MB file occupies 50 MB of memory immediately.
- 2Encodes on the main thread:Base64 encoding runs on the JavaScript main thread. During this time, the browser cannot respond to user interaction — the page freezes.
- 3Builds a massive string:The result is an enormous string (a 50 MB file produces ~67 MB of Base64). JavaScript strings are immutable, so constructing one involves heavy memory allocation and copying.
- 4Peak memory doubles:The original ArrayBuffer and the Base64 string both exist in memory at the same time, so peak usage is about 2.3x the file size. A 50 MB file can instantly consume ~115 MB of memory.
How to Avoid Freezing
Move the Base64 encoding into a Web Worker. The main thread passes the ArrayBuffer via postMessage(buffer, [buffer]) using a zero-copy transfer, the Worker encodes it in the background and sends the result back. The entire process does not block the UI — the page stays responsive.
Three Approaches: Which One Should You Use?
Different scenarios have different optimal solutions. Here are typical use cases for each of the three file handling approaches:
- Small CSS background images (< 5 KB)
- Inline favicon / app icon
- Embedded images in HTML email
- Inline small fonts with @font-face
- Small attachments in JSON API (signatures, thumbnails)
- Generate browser-side download links with Data URLs
- User uploads avatars or documents to your server
- Server-side validation of file type and size is needed
- Submitted together with form fields
- Uploaded via XMLHttpRequest or Fetch
- File size is unknown (anywhere from a few KB to hundreds of MB)
- Upload progress bar is needed (XHR progress event)
- Images, videos, or audio that need long-term storage
- CDN acceleration or global access is required
- Frontend presigned URL direct upload (bypasses the server)
- Large file (> 10 MB) multipart upload
- Multiple users share the same file
- Pay-as-you-go cost control is needed
File Size Reference Boundaries
There are no hard rules, but these boundaries are followed by most developers:
| File Size | Recommended Approach | Notes |
|---|---|---|
| < 5 KB | Inline Base64 | CSS backgrounds, icons, email images — saves an HTTP request |
| 5 KB – 1 MB | Base64 or multipart | Depends on whether inline is needed; multipart saves bandwidth for larger images |
| 1 MB – 10 MB | multipart/form-data | Base64 overhead becomes significant; longer upload time, more browser memory pressure |
| > 10 MB | Direct object storage upload | Presigned URL direct upload to S3/OSS from the frontend, no app server bandwidth used |
| > 100 MB | Multipart object storage upload | Resumable upload with concurrent parts — avoids single-request timeouts |
Handling Large-File Base64 with a Web Worker
If you really need to Base64-encode a large file in the browser, offloading the work to a Worker prevents UI freezes:
self.onmessage = function(e) {
const buffer = e.data // ArrayBuffer (zero-copy transfer)
const bytes = new Uint8Array(buffer)
let binary = ''
const CHUNK = 8192
for (let i = 0; i < bytes.length; i += CHUNK) {
binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK))
}
const base64 = btoa(binary)
self.postMessage(base64)
}const worker = new Worker('/base64.worker.js')
input.addEventListener('change', async (e) => {
const file = e.target.files[0]
const buffer = await file.arrayBuffer()
// Transferable: buffer ownership transferred to Worker (zero-copy)
worker.postMessage(buffer, [buffer])
})
worker.onmessage = (e) => {
const base64 = e.data // encoded result
console.log('data:...;base64,' + base64)
}The second argument to postMessage(buffer, [buffer]) is the Transferable list. The ArrayBuffer ownership is transferred zero-copy to the Worker, and the main thread's buffer becomes empty (byteLength 0), avoiding doubled memory usage.
Quick Decision: One-line Guide
Related Tools
Base64 Size Calculator
Enter the original file size to instantly estimate the encoded size increase and avoid unexpectedly large payloads.
File to Base64
Convert any file to Base64 with output formats including Data URL, HTML, CSS, and JSON.
Base64 to File
Decode a Base64 string back to the original file and download it.
Frequently Asked Questions
Why does a file get larger after Base64 encoding?
Base64 encodes every 3 bytes as 4 ASCII characters, making the output 4/3 the original size — about 33% larger. If the data length is not a multiple of 3, = padding is added, making the actual increase slightly over 33%.
Why does the browser freeze when converting a large file to Base64?
FileReader.readAsDataURL() reads and encodes the entire file on the main thread, blocking all UI updates. Combined with both the raw data and the Base64 string in memory at once, peak usage is about 2.3x the file size. Use a Web Worker to fix this.
What is the difference between a Blob URL and a Base64 Data URL?
A Blob URL (URL.createObjectURL) is a temporary browser-internal reference that does not copy data, so memory usage is minimal — ideal for displaying or downloading files within the browser. A Base64 Data URL is a real text string you can embed in HTML, CSS, or JSON. It is suitable for inline scenarios but much larger.
When is multipart/form-data better than Base64?
When uploading to a server, multipart sends raw binary data as a stream with no size overhead. The server can write to disk while receiving, it supports progress events, and it handles any file size. Base64 is only suitable for small inline attachments or JSON API payloads.
What are Transferables in Web Workers?
When you call postMessage(buffer, [buffer]), the ArrayBuffer ownership is zero-copy transferred to the Worker — the main thread's variable becomes empty (byteLength 0) immediately. Compared to copying, Transferables are both faster and more memory-efficient for large files.
How do presigned URLs for object storage work?
The backend generates a time-limited, signed upload URL using the AWS SDK or OSS SDK. The frontend uses that URL to PUT the file directly to the storage service, bypassing the app server entirely. The file does not consume server bandwidth or memory — ideal for large files.