Performance & Threading

How to Prevent UI Freezing During Cryptography

May 2, 2026
8 Min Read

You have perfectly implemented client-side encryption using the Web Crypto API. Your user selects a 10MB file, types in their secure PIN, and clicks "Encrypt." Suddenly, your beautiful CSS loading spinner freezes completely. The browser tab becomes unresponsive for two solid seconds.

What went wrong? JavaScript is single-threaded. It has one main thread to handle DOM updates, CSS animations, click events, and JavaScript execution. When you force that single thread to compute 100,000 iterations of PBKDF2 hashing, everything else grinds to a halt. To build a premium user experience, we must utilize Web Workers.

The Solution: Background Threads

A Web Worker allows you to spawn a completely separate background thread in the user's browser. This background thread has its own CPU allocation. It cannot access the DOM directly, but it *can* access the window.crypto.subtle API.

By offloading the heavy cryptographic math to the Worker, your Main Thread remains completely free to render smooth 60fps animations and handle user interactions.

Step 1: Create the Worker File

First, we create a separate JavaScript file that will run in the background. It listens for messages from the Main Thread, does the heavy lifting, and sends the result back.

// crypto-worker.js

// Listen for messages from the main UI thread
self.addEventListener('message', async (event) => {
    const { action, payload, key } = event.data;

    if (action === 'ENCRYPT_FILE') {
        try {
            // Generate Initialization Vector
            const iv = self.crypto.getRandomValues(new Uint8Array(12));

            // Perform the heavy AES-GCM encryption in the background
            const ciphertext = await self.crypto.subtle.encrypt(
                { name: "AES-GCM", iv: iv },
                key,
                payload
            );

            // Send the result back to the main thread
            self.postMessage({ status: 'SUCCESS', ciphertext, iv });
            
        } catch (error) {
            self.postMessage({ status: 'ERROR', error: error.message });
        }
    }
});

Step 2: Communicate with the Worker

In your main application file (the one connected to your HTML), you instantiate the Web Worker and set up a communication bridge using the postMessage API.

// main.js

// 1. Initialize the background worker
const worker = new Worker('crypto-worker.js');

function encryptFileAsync(fileBuffer, cryptoKey) {
    return new Promise((resolve, reject) => {
        
        // 2. Set up the listener for when the worker finishes
        worker.onmessage = (event) => {
            const { status, ciphertext, iv, error } = event.data;
            if (status === 'SUCCESS') {
                resolve({ ciphertext, iv });
            } else {
                reject(error);
            }
        };

        // 3. Send the heavy data to the background thread
        worker.postMessage({ 
            action: 'ENCRYPT_FILE', 
            payload: fileBuffer, 
            key: cryptoKey 
        });
        
        console.log("Encryption started in background. UI is still responsive!");
    });
}

Transferable Objects: The Performance Secret

When you use postMessage() to send a 10MB file buffer to a Web Worker, the browser creates a complete copy (clone) of that 10MB file by default. This consumes extra RAM and slows down the transfer.

To achieve maximum performance, you should use Transferable Objects. This allows you to transfer *ownership* of the memory from the Main Thread to the Worker Thread instantly, with zero copying overhead.

// Optimized Main Thread transfer
worker.postMessage(
    { action: 'ENCRYPT_FILE', payload: fileBuffer, key: cryptoKey },
    [fileBuffer] // <-- This array passes ownership instantly!
);

Experience Zero UI Blocking

When building ZeroKey, we knew that waiting for a payload to encrypt on a mobile device could feel sluggish. We heavily utilized Web Workers for both PBKDF2 key derivation and AES file encryption.

Because the math happens in a background thread, our matrix-style decryption animations render at a flawless 60fps, completely uninterrupted by the heavy cryptography happening under the hood.

Conclusion

Building a secure application is only half the battle; the other half is making it usable. If your security features degrade the user experience by freezing the browser, users will find less secure ways to share their data. By mastering Web Workers and Transferable Objects, you can deliver military-grade cryptography with native-app smoothness.