Site icon Filestack Blog

Browser Freezes When Uploading Files and Finding the Main-Thread Bottleneck

Browser Freezes When Uploading Files: Finding the Main-Thread Bottleneck

Upload works. The file lands, the progress bar moves, everything’s fine, except the tab locks up while it happens. Clicks queue. The spinner stops spinning. Support tickets start with “the page froze” instead of “the upload failed,” and that distinction matters more than it sounds like it should.

Here’s the thing: uploading is I/O, and I/O never freezes a browser. Waiting for bytes to travel over a network doesn’t block a single frame. What does block frames is computation, and when the browser freezes when uploading files, there’s almost always a chunk of synchronous JavaScript running on the main thread at the exact moment the UI needed to breathe.

When a browser freezes while uploading files, the cause is almost always main-thread work: synchronous file reads, client-side hashing or image processing, or progress events flooding the render loop. The fix is moving file work off the main thread with Web Workers, streaming reads, and throttled UI updates. Managed uploaders like Filestack run chunking and transfer off the critical path so the page stays responsive.

The rest of this article walks through finding that bottleneck with DevTools, the usual suspects behind it, and the two fixes that actually resolve it.

Key Takeaways

Before touching any code, get proof of what’s actually blocking the thread; DevTools will show it to you directly.

Reproduce It, Then Read the Flame Chart

Open the Performance panel, hit record, trigger the upload that freezes, and stop recording once the page comes back to life. You’re looking for a task with a red flag in the corner; that’s a “long task,” anything over 50ms, and Chrome marks it precisely because 50ms is roughly where users start perceiving lag.

Click into it. The call stack underneath tells you exactly which function is holding the thread hostage. This one question usually starts the whole investigation: what are the best practices for handling file uploads with JavaScript? The honest answer is that best practice #1 is knowing where your current implementation spends its main-thread time, because guessing at a fix without this step wastes hours.

In a typical trace, you’ll see a single wide bar, sometimes 300-800ms, sitting right under the click that started the upload. That bar is your answer.

The Usual Suspects

Three patterns account for nearly every case of upload-related jank, and they’re all avoidable once you know to look for them.

Suspect 1: Whole-file reads. FileReader.readAsDataURL() and readAsArrayBuffer() both read the entire file into memory synchronously relative to the render loop. On a 500MB file, readAsDataURL allocates a base64 string roughly 1.37x the file’s size, and building that string is exactly the kind of CPU-bound work that shows up as your long task.

Suspect 2: Synchronous thumbnail generation. Drawing a preview to a canvas, resizing it, and re-encoding it right after file selection is common, and expensive. This is usually the moment someone asks how do I generate image thumbnails automatically after upload? The answer matters here specifically because doing it synchronously, in the same tick as file selection, is what turns a nice-to-have preview into a frozen tab.

Suspect 3: Unthrottled progress events. If a progress handler updates React state on every progress event, and the browser is firing that event thousands of times per second on a fast connection, you’re triggering thousands of re-renders of a potentially large component tree. Individually cheap, collectively brutal.

Fix 1: Slice and Stream Instead of Read

Once you know reading is the problem, the fix is straightforward: stop reading the whole file into memory at all.

Blob.slice() creates a reference to a byte range without copying any data; it’s essentially free. That’s the whole trick behind chunked uploads staying cheap on the main thread: you’re never holding the full file in memory, just streaming references to pieces of it.

If you’re working with multi-gigabyte files, this same approach also prevents excessive server memory usage and makes uploads far more resilient. We cover the architecture, trade-offs, and implementation patterns in our guide to handling large file uploads.

This connects directly to how multipart transfer works, a question worth answering plainly: how does multipart upload work in web applications? The client slices the file into fixed-size pieces, uploads them (often in parallel), and the server reassembles them by sequence once every piece has arrived. It’s also the mechanism behind resumable uploads, since each chunk is tracked independently; a dropped connection only needs to retry the missing pieces, not the whole file.

async function uploadInChunks(file, chunkSize = 5 * 1024 * 1024) {

const totalChunks = Math.ceil(file.size / chunkSize);

for (let i = 0; i < totalChunks; i++) {

const start = i * chunkSize;

const end = Math.min(start + chunkSize, file.size);

const chunk = file.slice(start, end); // no byte copy, cheap

await fetch(`/upload?part=${i}`, {

method: 'POST',

body: chunk,

});

}

}

// Cheap, async preview — no main-thread decode blocking

async function generatePreview(file) {

const bitmap = await createImageBitmap(file, { resizeWidth: 200 });

const canvas = document.createElement('canvas');

canvas.width = bitmap.width;

canvas.height = bitmap.height;

canvas.getContext('2d').drawImage(bitmap, 0, 0);

return canvas.toDataURL();

}

createImageBitmap decodes off the main thread by design, which is why it’s a better default than drawing straight to a canvas from a FileReader result.

Fix 2: Workers and Throttled Progress

Slicing solves the reading problem. The remaining CPU-bound work, hashing and compression, needs somewhere else to run entirely.

If you need a checksum before upload, or you’re compressing client-side, that work belongs in a Web Worker. Workers run on a separate thread, and passing large binary data to them via transferable ArrayBuffers avoids the copy cost that would otherwise defeat the purpose.

This is also where how can I improve user experience in file upload UI? gets a concrete answer, beyond generic advice: keep the main thread free so the progress bar, cancel button, and rest of the page stay interactive for the full duration of the upload, not just at the start and end of it.

// main.js

const worker = new Worker('hash-worker.js');

const buffer = await file.arrayBuffer();

worker.postMessage({ buffer }, [buffer]); // transferred, not copied

worker.onmessage = (e) => {

console.log('File hash:', e.data.hash);

};

// hash-worker.js

self.onmessage = async ({ data }) => {

const digest = await crypto.subtle.digest('SHA-256', data.buffer);

const hash = Array.from(new Uint8Array(digest))

.map((b) => b.toString(16).padStart(2, '0'))

.join('');

self.postMessage({ hash });

};

Progress updates deserve the same discipline. Instead of setting React state on every progress event, throttle updates to animation-frame cadence with requestAnimationFrame, so the UI reflects progress smoothly without re-rendering thousands of times a second.

The Managed Route: Responsiveness by Default

Every fix above is something you can build, but it’s also exactly what a dedicated upload tool is built to handle already.

If you’d rather not own this class of bug long-term, a production file uploader performs slicing, hashing, and transfer off the critical path by design, rather than as something bolted on after a jank report. Filestack’s picker slices files with Blob.slice and uploads chunks asynchronously with throttled progress callbacks, which is the same pattern this article just walked through, just already implemented and tested against real-world network conditions.

Its Intelligent Ingestion layer chunks and uploads adaptively, adjusting chunk size to the connection without any of that logic touching your render loop. Drop a large file into it, and the UI stays interactive the entire time, no frozen scroll, no delayed clicks, which is the practical test anyone searching for a JavaScript file uploader is really running.

Conclusion: Keep File Work Off the Main Thread

Freezing during upload is a main-thread problem wearing a network costume. Measure first with the Performance panel, slice instead of reading whole files, delegate hashing and compression to a worker, and throttle your progress updates. Do those four things and the freeze disappears regardless of file size.

If you want to see the pattern already built and battle-tested, explore Filestack’s File Picker and see how it handles large uploads while keeping the main thread responsive.

FAQ

Why does my browser freeze when uploading files?

Main-thread file work – synchronous reads, hashing, or unthrottled progress updates – is blocking the render loop while the upload runs.

Do large files always freeze the page?

No. Chunked, asynchronous transfer keeps the UI responsive regardless of file size; freezing comes from synchronous processing, not size alone.

Does a managed uploader fix this?

Yes, if it slices and transfers off the critical path by design, as Filestack’s picker does.

Exit mobile version