Skip to content

Bits of .NET

Daily micro-tips for C#, SQL, performance, and scalable backend engineering.

  • Asp.Net Core
  • C#
  • SQL
  • JavaScript
  • CSS
  • About
  • ErcanOPAK.com
  • No Access
  • Privacy Policy
HTML

HTML5 Web Workers: Run JavaScript in Background Thread Without Freezing UI

- 01.02.26 - ErcanOPAK

Heavy JavaScript computation freezing your webpage? Web Workers run code in a separate thread, keeping your UI responsive.

The UI Freezing Problem:

// ❌ BAD: Blocks UI for 5 seconds
function processLargeData() {
    const data = Array.from({ length: 10000000 }, (_, i) => i);
    
    const result = data
        .filter(n => n % 2 === 0)
        .map(n => n * 2)
        .reduce((sum, n) => sum + n, 0);
    
    console.log('Result:', result);
}

button.addEventListener('click', processLargeData);
// User clicks button → UI freezes → can't scroll, click, or type
// Page appears broken for 5 seconds

Web Worker Solution:

// Create worker.js file:
// worker.js
self.addEventListener('message', (e) => {
    const data = e.data;
    
    // Heavy computation runs in background thread
    const result = data
        .filter(n => n % 2 === 0)
        .map(n => n * 2)
        .reduce((sum, n) => sum + n, 0);
    
    // Send result back to main thread
    self.postMessage(result);
});

// Main thread (index.html):
const worker = new Worker('worker.js');

worker.addEventListener('message', (e) => {
    console.log('Result from worker:', e.data);
    updateUI(e.data);  // Update UI with result
});

button.addEventListener('click', () => {
    const data = Array.from({ length: 10000000 }, (_, i) => i);
    worker.postMessage(data);  // Send data to worker
    // UI stays responsive! User can still interact with page
});

How Web Workers Actually Work:

// Main thread and worker thread run in parallel:

// Main Thread                     Worker Thread
// ↓                               ↓
// Create worker                   (starts running worker.js)
// ↓                               ↓  
// postMessage(data) →→→→→→→→→→→→→ receives data
// ↓                               ↓
// UI stays responsive             processes data (heavy work)
// User can scroll, click          ↓
// ↓                               ↓
// ←←←←←←←←←←←←←←←←←←←←← postMessage(result)
// receives result                 ↓
// ↓                               continues listening
// updateUI(result)                
// ↓
// Done

// Key point: Main thread never blocks!

Worker Limitations:

// ❌ Workers CANNOT access:
// - DOM (document, window)
// - localStorage, sessionStorage
// - Parent page variables directly

// ✅ Workers CAN access:
// - fetch() for network requests
// - setTimeout, setInterval
// - Import other scripts
// - IndexedDB for storage
// - Math, Date, JSON, etc.

// Example of what you can't do:
// worker.js
self.addEventListener('message', () => {
    document.getElementById('result').textContent = 'Done';  // ❌ ERROR!
    // Workers have no access to DOM
});

// Instead, send message to main thread:
// worker.js
self.addEventListener('message', () => {
    self.postMessage({ type: 'done', message: 'Processing complete' });
});

// main.js  
worker.addEventListener('message', (e) => {
    if (e.data.type === 'done') {
        document.getElementById('result').textContent = e.data.message;  // ✅ Works
    }
});

Transferable Objects (Zero-Copy):

// By default, postMessage copies data
const largeArray = new Uint8Array(10000000);  // 10MB array

worker.postMessage(largeArray);  
// Copies 10MB → main thread still has array
// Worker gets copy → uses 20MB total RAM

// ✅ Use Transferable for zero-copy
worker.postMessage(largeArray, [largeArray.buffer]);
// Transfers ownership → main thread loses array
// Worker gets array → uses only 10MB RAM
// 50% memory savings!

// After transfer:
console.log(largeArray.byteLength);  // 0 (empty, transferred away)

// Transferable types:
// - ArrayBuffer
// - MessagePort
// - ImageBitmap
// - OffscreenCanvas

Shared Workers (Multiple Tabs):

// Regular Worker: Each tab gets its own instance
const worker = new Worker('worker.js');  // Tab 1 has separate worker
const worker = new Worker('worker.js');  // Tab 2 has separate worker

// Shared Worker: All tabs share single instance
const worker = new SharedWorker('shared-worker.js');

// shared-worker.js
self.addEventListener('connect', (e) => {
    const port = e.ports[0];
    
    port.addEventListener('message', (msg) => {
        // Broadcast to all connected tabs
        self.ports.forEach(p => p.postMessage(msg.data));
    });
    
    port.start();
});

// Use case: Real-time chat shared across multiple tabs
// One WebSocket connection in Shared Worker
// All tabs receive messages

Real-World Example – Image Processing:

// image-worker.js
self.addEventListener('message', (e) => {
    const { imageData, filter } = e.data;
    const data = imageData.data;
    
    if (filter === 'grayscale') {
        for (let i = 0; i < data.length; i += 4) {
            const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
            data[i] = avg;      // R
            data[i + 1] = avg;  // G
            data[i + 2] = avg;  // B
        }
    } else if (filter === 'invert') {
        for (let i = 0; i < data.length; i += 4) {
            data[i] = 255 - data[i];          // R
            data[i + 1] = 255 - data[i + 1];  // G
            data[i + 2] = 255 - data[i + 2];  // B
        }
    }
    
    self.postMessage({ imageData }, [imageData.data.buffer]);
});

// main.js
const worker = new Worker('image-worker.js');
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

document.getElementById('grayscale-btn').addEventListener('click', () => {
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    
    worker.postMessage(
        { imageData, filter: 'grayscale' },
        [imageData.data.buffer]  // Transfer for performance
    );
});

worker.addEventListener('message', (e) => {
    const { imageData } = e.data;
    ctx.putImageData(imageData, 0, 0);
    // UI stayed responsive during processing!
});

Error Handling:

worker.addEventListener('error', (e) => {
    console.error('Worker error:', e.message);
    console.error('File:', e.filename);
    console.error('Line:', e.lineno);
});

// In worker, unhandled errors bubble to main thread
// worker.js
self.addEventListener('message', () => {
    throw new Error('Something went wrong!');  
    // Main thread's error listener catches this
});

Terminate Worker:

// From main thread
worker.terminate();  // Immediately stops worker

// From worker (self-termination)
self.close();  // Worker stops itself

Related posts:

HTML5: Use loading='lazy' Attribute to Defer Offscreen Images

The loading="lazy" Attribute That Saves Bandwidth Instantly

HTML5 Forms Submitting Twice

Post Views: 6

Post navigation

SQL: When CTEs Kill Performance and You Should Use Temp Tables Instead
Visual Studio: Stop Debugger from Stepping Into .NET Framework Source Code

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

February 2026
M T W T F S S
 1
2345678
9101112131415
16171819202122
232425262728  
« Jan    

Most Viewed Posts

  • Get the User Name and Domain Name from an Email Address in SQL (935)
  • How to add default value for Entity Framework migrations for DateTime and Bool (831)
  • Get the First and Last Word from a String or Sentence in SQL (825)
  • How to select distinct rows in a datatable in C# (799)
  • How to make theater mode the default for Youtube (714)
  • Add Constraint to SQL Table to ensure email contains @ (573)
  • How to enable, disable and check if Service Broker is enabled on a database in SQL Server (552)
  • Average of all values in a column that are not zero in SQL (519)
  • How to use Map Mode for Vertical Scroll Mode in Visual Studio (474)
  • Find numbers with more than two decimal places in SQL (436)

Recent Posts

  • C#: Use ArgumentNullException.ThrowIfNull for Cleaner Validation
  • C#: Use Discard Pattern to Ignore Unwanted Values
  • C#: Use Deconstruction with Tuples for Cleaner Multiple Returns
  • C#: Use File-Scoped Types to Limit Class Visibility
  • SQL: Use PIVOT to Transform Rows into Columns
  • SQL: Use MERGE OUTPUT to Track What Changed During Upsert
  • .NET Core: Use Polly for Resilient HTTP Requests with Retry Logic
  • .NET Core: Use Dapper for Lightweight ORM Alternative to Entity Framework
  • Git: Use git sparse-checkout to Clone Only Specific Folders
  • Git: Use git switch and git restore Instead of Confusing git checkout

Most Viewed Posts

  • Get the User Name and Domain Name from an Email Address in SQL (935)
  • How to add default value for Entity Framework migrations for DateTime and Bool (831)
  • Get the First and Last Word from a String or Sentence in SQL (825)
  • How to select distinct rows in a datatable in C# (799)
  • How to make theater mode the default for Youtube (714)

Recent Posts

  • C#: Use ArgumentNullException.ThrowIfNull for Cleaner Validation
  • C#: Use Discard Pattern to Ignore Unwanted Values
  • C#: Use Deconstruction with Tuples for Cleaner Multiple Returns
  • C#: Use File-Scoped Types to Limit Class Visibility
  • SQL: Use PIVOT to Transform Rows into Columns

Social

  • ErcanOPAK.com
  • GoodReads
  • LetterBoxD
  • Linkedin
  • The Blog
  • Twitter
© 2026 Bits of .NET | Built with Xblog Plus free WordPress theme by wpthemespace.com