Rajnish Noonia

Category: Javascript

  • The JavaScript Execution Engine: Event Loop, Microtasks, Macrotasks, Web Workers and Service Workers

    JavaScript is single-threaded. There is one call stack, one thread of execution, and at any given moment exactly one thing is running. Yet it handles timers, network requests, user events, animations, background computation, and offline support without blocking. Understanding how it works changes how you write async code, debug race conditions, and reason about performance. This post covers the full picture: the event loop, the queues, Web Workers, and Service Workers.

    JS engine (single thread) Call stack Executes synchronous code console.log(‘hello’) main() Heap Object memory allocation { name: ‘obj’, val: 42 } Web APIs / Node.js setTimeout Timer (0ms+ delay) fetch / XHR Network I/O DOM events click, scroll, resize rAF / Promises Async coordination Microtask queue Drains completely before next task Promise.then() / .catch() / .finally() queueMicrotask() / MutationObserver Macrotask queue One task per loop iteration setTimeout / setInterval I/O callbacks / UI events Event loop offload Priority: (1) sync (2) microtask – drains fully (3) macrotask – one per iteration
    JavaScript execution engine: call stack, Web APIs, microtask queue, macrotask queue and event loop

    The Moving Parts

    Four components interact to make async JavaScript work on the main thread.

    The call stack

    The call stack is where synchronous code executes. When you call a function, a frame is pushed onto the stack. When it returns, the frame is popped. The engine can only execute code at the top of the stack. If a function takes 500ms of CPU work, nothing else can happen during those 500ms – the stack is blocked. This is why long-running synchronous operations freeze the browser: the event loop cannot process anything while the stack is occupied.

    Web APIs (or Node.js equivalents)

    The browser provides APIs that live outside the JavaScript engine: timers, network I/O, DOM events, file system access. When you call setTimeout, the JS engine hands the callback and delay to the browser’s timer mechanism and immediately returns. The actual waiting happens in a separate system thread managed by the browser. When the timer fires, the browser posts the callback into a queue.

    This is the key insight: JavaScript itself never does I/O. It delegates to the runtime, registers a callback, and moves on. The runtime notifies JavaScript when the work is done by placing callbacks into one of two queues.

    The microtask queue

    Microtasks are high-priority callbacks that run immediately after the current task completes, before the event loop picks up any macrotask and before the browser renders the next frame. Sources of microtasks include:

    • Promise.then(), .catch(), .finally()
    • queueMicrotask()
    • MutationObserver callbacks
    • await continuations (which desugar to Promise .then())

    The critical rule: the microtask queue drains completely before the event loop moves on. If a microtask callback queues another microtask, that microtask also runs before any macrotask. This can theoretically starve the macrotask queue – and the rendering pipeline – if microtasks keep spawning more microtasks.

    The macrotask queue (task queue)

    Macrotasks are lower-priority callbacks scheduled by the runtime. Sources include:

    • setTimeout and setInterval callbacks
    • I/O callbacks (file reads, network in Node.js)
    • UI event callbacks (click, keydown, scroll)
    • setImmediate in Node.js
    • MessageChannel port messages

    The event loop picks exactly one macrotask per iteration. After that one macrotask runs, it drains the entire microtask queue again before picking the next macrotask.

    The event loop algorithm

    while (true) {
      executeCurrentTask();
      while (microtaskQueue.length > 0) {
        const task = microtaskQueue.shift();
        task();
      }
      maybeRender();
      if (macrotaskQueue.length > 0) {
        const task = macrotaskQueue.shift();
        task();
      }
    }

    Why Promise.then() always beats setTimeout(fn, 0)

    setTimeout(fn, 0) does not mean “run immediately”. It means “run as soon as possible, but only after the current task and all microtasks have finished, and only as a macrotask”. A Promise.then() registered at the same time will always run first because it goes into the microtask queue, which drains before any macrotask is picked.

    setTimeout(() => console.log('timeout'), 0);
    Promise.resolve().then(() => console.log('promise'));
    
    // Output:
    // promise  (microtask - runs first)
    // timeout  (macrotask - runs after all microtasks)

    How fetch actually works

    When you call fetch('/data'):

    1. fetch() returns a Promise immediately. The browser’s networking layer starts the HTTP request in a separate thread.
    2. JavaScript continues executing synchronously – nothing waits.
    3. When the response arrives, the browser resolves the promise, queuing any .then() handlers as microtasks.
    4. The next time the microtask queue drains, those handlers run.
    5. Each .then() in a chain is a separate microtask, queued lazily when the previous one resolves.

    How async/await desugars

    async/await is syntactic sugar over promises. Every await suspends the async function (pops its frame off the call stack) and queues the continuation as a microtask when the awaited promise resolves.

    // async/await
    async function load() {
      const res = await fetch('/data');
      const data = await res.json();
      console.log(data);
    }
    
    // Is approximately:
    function load() {
      return fetch('/data')
        .then(res => res.json())
        .then(data => { console.log(data); });
    }

    A worked example: what does this log?

    console.log('A');
    setTimeout(() => console.log('B'), 0);
    Promise.resolve()
      .then(() => console.log('C'))
      .then(() => console.log('D'));
    console.log('E');
    
    // Output: A E C D B

    Sync runs first: A, then E. Microtask queue drains: C resolves and queues D, so C then D. Finally the macrotask runs: B. The second .then() was not enqueued until the first one ran – promise chains are lazy microtask sequences, not a pre-loaded batch.

    Main thread UI + event loop Call stack Sync execution DOM access Microtask queue Promise callbacks Macrotask queue setTimeout, events postMessage() Send / receive data Render pipeline Layout – Paint Blocked by JS Workers never block this Web Worker Separate OS thread Own call stack CPU-heavy work No DOM access Own microtask queue Promises work here Own macrotask queue setTimeout works here postMessage() Send results back Service Worker Network proxy thread Lifecycle events install – activate idle / terminated fetch event Intercepts all requests respondWith(promise) Cache API Serve offline / stale Cache-then-network Push + sync events Background wakeup No DOM / no window postMessage to page send result fetch() intercepted by SW cached / network response All three have their own event loop – only the main thread can access the DOM
    Main thread, Web Worker, and Service Worker – three independent event loops with distinct responsibilities

    Web Workers: true parallelism for JavaScript

    The event loop handles async waiting by delegating I/O to the browser and resuming when ready. But it does not solve CPU-intensive computation. If you need to parse a 50MB JSON file, run a physics simulation, encrypt a large payload, or process an image, doing that on the main thread blocks the call stack and freezes the UI regardless of how cleverly you structure your promises.

    Web Workers give you a genuine second OS thread. A Worker runs in a completely separate execution context with its own call stack, its own microtask and macrotask queues, its own memory heap, and its own event loop. The main thread and workers run in parallel on multi-core hardware.

    What Workers can and cannot do

    A Worker has access to: fetch, setTimeout, setInterval, Promise, IndexedDB, WebSockets, Cache API, crypto, console, and most Web APIs that don’t require a rendering context. What it does not have access to is the DOM – no document, no window, no direct manipulation of page elements. The rendering pipeline lives exclusively on the main thread, and the browser enforces this hard boundary.

    Communication via postMessage

    The main thread and workers communicate exclusively through postMessage(). Data is serialised (structured clone algorithm) and deserialised on the other side; each side gets its own copy, not a shared reference. For large data (images, audio buffers, typed arrays) you can use Transferable Objects to hand ownership to the other thread without copying, which is zero-copy and fast.

    // main.js
    const worker = new Worker('worker.js');
    worker.postMessage({ action: 'process', data: largeArray });
    worker.onmessage = (event) => {
      console.log('Result:', event.data);
    };
    
    // worker.js
    self.onmessage = (event) => {
      const result = heavyComputation(event.data.data);
      self.postMessage(result);
    };

    When a worker calls postMessage(), the message arrives on the main thread as a macrotask – it queues a message event on the worker object. The main thread’s event loop picks it up in the normal way: after all current microtasks have drained. This means worker communication is non-blocking in both directions.

    Transferable Objects for zero-copy transfer

    const buffer = new ArrayBuffer(100 * 1024 * 1024); // 100MB
    worker.postMessage({ buffer }, [buffer]);
    // buffer is now detached in main - worker owns it
    
    self.onmessage = (e) => {
      processBuffer(e.data.buffer);
      self.postMessage({ buffer: e.data.buffer }, [e.data.buffer]);
    };

    Shared memory with SharedArrayBuffer

    For high-frequency communication (game engines, audio processing, real-time data pipelines), copying data on every message is too expensive. SharedArrayBuffer creates a memory region that both the main thread and workers can read and write without copying. You coordinate access using Atomics – atomic operations that prevent race conditions by guaranteeing visibility and mutual exclusion.

    const shared = new SharedArrayBuffer(4);
    const view = new Int32Array(shared);
    worker.postMessage({ shared });
    Atomics.store(view, 0, 1);
    Atomics.notify(view, 0, 1);

    Worker types

    There are three kinds of Worker: Dedicated Workers (owned by one page, created with new Worker()), Shared Workers (shared across multiple pages from the same origin, accessible via new SharedWorker()), and Service Workers (covered in the next section). Dedicated Workers are by far the most common.

    Service Workers: the network proxy

    A Service Worker is a type of worker with a fundamentally different purpose. Where a Dedicated Worker offloads CPU computation, a Service Worker sits between your application and the network, intercepting and handling every fetch request the page makes. It is the foundation of Progressive Web Apps (PWAs) – offline support, background sync, push notifications, and fine-grained caching strategies all live here.

    The Service Worker lifecycle

    A Service Worker has a distinct lifecycle that separates it from ordinary workers:

    1. Registration: the page calls navigator.serviceWorker.register('/sw.js'). The browser downloads and parses the worker script.
    2. Install: the browser fires the install event. This is where you pre-cache static assets. You call event.waitUntil(promise) to tell the browser not to proceed until your caching is complete. If the promise rejects, the install fails and the worker is discarded.
    3. Activate: once installed, the worker waits to activate. A new worker only activates when no existing controlled pages are open (or when you call self.skipWaiting()). The activate event is where you clean up old caches from previous versions.
    4. Idle: the worker is now active and controlling pages, but the browser may terminate it at any time to save memory. It is relaunched on demand when a controlled page makes a fetch or a push notification arrives.
    const CACHE = 'v1';
    const STATIC = ['/index.html', '/app.js', '/style.css'];
    
    self.addEventListener('install', event => {
      event.waitUntil(
        caches.open(CACHE).then(cache => cache.addAll(STATIC))
      );
      self.skipWaiting();
    });
    
    self.addEventListener('activate', event => {
      event.waitUntil(
        caches.keys().then(keys =>
          Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k)))
        )
      );
      self.clients.claim();
    });

    Intercepting fetch requests

    The fetch event is the core of a Service Worker. Every network request made by a controlled page – including fetch(), XMLHttpRequest, CSS imports, image loads, script tags – passes through this handler. You use event.respondWith(promise) to provide the response, which can come from the cache, the network, or be constructed programmatically.

    self.addEventListener('fetch', event => {
      event.respondWith(
        caches.match(event.request).then(cached => {
          if (cached) return cached;
          return fetch(event.request).then(response => {
            const toCache = response.clone();
            caches.open(CACHE).then(cache => cache.put(event.request, toCache));
            return response;
          });
        })
      );
    });

    Caching strategies

    • Cache first: serve from cache if present, otherwise fetch from network and cache the result. Best for assets that change infrequently (fonts, versioned JS bundles).
    • Network first: try the network, fall back to cache if offline. Best for frequently updated content (API responses, news feeds).
    • Stale while revalidate: serve from cache immediately (fast), then fetch from network in the background to update the cache for the next request. Best for content where showing slightly stale data is acceptable.
    • Network only: never use the cache. For analytics, payment flows, anything where stale data is dangerous.
    • Cache only: never hit the network. For pre-cached static assets in a fully offline app.

    Background sync and push notifications

    Service Workers can be woken up by the browser even when no page is open. The sync event fires when connectivity is restored after a period offline – you can queue writes made while offline and flush them here. The push event fires when a push message arrives from your server, allowing you to display a notification even if the user doesn’t have your site open.

    self.addEventListener('sync', event => {
      if (event.tag === 'submit-form') {
        event.waitUntil(flushPendingSubmissions());
      }
    });
    
    self.addEventListener('push', event => {
      const data = event.data.json();
      event.waitUntil(
        self.registration.showNotification(data.title, {
          body: data.body,
          icon: '/icon.png',
        })
      );
    });

    Communicating with the page

    A Service Worker communicates with its controlled pages via postMessage, just like a Dedicated Worker. The worker can send messages to all controlled clients via self.clients.matchAll(), and individual pages can send messages to the worker via navigator.serviceWorker.controller.postMessage(). As with Dedicated Workers, these messages arrive as macrotasks on the receiving end.

    self.clients.matchAll().then(clients => {
      clients.forEach(client => client.postMessage({ type: 'CACHE_UPDATED' }));
    });
    
    navigator.serviceWorker.addEventListener('message', event => {
      if (event.data.type === 'CACHE_UPDATED') {
        showUpdateBanner();
      }
    });

    Web Worker vs Service Worker: when to use which

    The two are often confused because both are “workers that run off the main thread”. The distinction is purpose, not mechanism.

    Web Worker Service Worker
    Primary purpose CPU-intensive computation Network proxy and caching
    Lifetime As long as the page holds a reference Persists independently, browser controls termination
    Scope Single page All pages on the same origin
    fetch access Can make fetch calls Intercepts all fetch calls from the page
    DOM access No No
    Offline support No Yes, via Cache API
    Push notifications No Yes
    Background sync No Yes
    Typical use cases Image processing, data parsing, encryption, physics, ML inference Caching strategy, offline PWA, push, background sync

    Practical implications

    Long microtask chains can block rendering

    The browser cannot render a new frame until the microtask queue is empty. For large data processing, break work into chunks using setTimeout to yield to the rendering pipeline, or use a Web Worker to move the work off the main thread entirely.

    Service Workers require HTTPS

    Service Workers can intercept and modify any network request, which makes them powerful – and dangerous if compromised. Browsers only register Service Workers on HTTPS origins (and localhost for development). There are no exceptions.

    Service Workers are version-sensitive

    When you deploy a new Service Worker, users with the old version keep it until all their tabs are closed and reopened. skipWaiting() + clients.claim() force immediate takeover – useful during development but potentially disruptive if the new worker uses an incompatible cache structure. Design your activate handler to clean up old caches explicitly.

    Workers have their own event loops

    Both Web Workers and Service Workers have their own complete event loop – their own call stack, microtask queue, and macrotask queue. Promise, setTimeout, fetch, and async/await all work inside a worker the same way they work on the main thread. The only difference is the absence of the DOM and window APIs.

    Summary

    JavaScript’s concurrency model has three layers. The event loop handles async waiting on the main thread – sync code runs first, then all microtasks drain (Promises, await continuations), then one macrotask runs, then microtasks drain again. Web Workers handle CPU parallelism – genuine OS threads running their own event loops, communicating with the main thread via postMessage without blocking the UI. Service Workers handle network and persistence – a long-lived proxy thread that intercepts fetch requests, implements caching strategies, enables offline use, and receives push notifications and background sync events independently of any open page.

    Together they give JavaScript – a language with one main thread – a complete answer to async I/O, CPU parallelism, and network resilience.

  • ASP.Net and Ajax

    AJAX (Asynchronous JavaScript and XML) is arguably one of the most hyped technology acronyms around. The primary advantage of using AJAX is that page refreshes can be minimized, allowing users to get the information they need quickly and easily through a more rich and functional interface. Ajax accomplishes this by using JavaScript and an XmlHttp object to send data asynchronously from the browser to the Web server and back.

    ASP.Net Ajax Extensions

    Microsoft’s ASP.NET AJAX Extensions provide developers with a quick and simple way to add AJAX functionality into any ASP.NET Website, without requiring in-depth knowledge of JavaScript or other AJAX technologies.

    Visual Studio 2008 and above by default have AJAX extensions installed or you may download from microsoft site.

    Ajax Extension provides 3 controls named “Timer”, UpdatePanel and UpdateProgress you can implement partial page submit using updatepanels.

    ASP.Net Ajax Control Toolkit

    The ASP.NET AJAX Control Toolkit  is an open-source project built on top of the Microsoft ASP.NET AJAX framework. It is a joint effort between Microsoft and the ASP.NET AJAX community that provides a powerful infrastructure to write reusable, customizable and extensible ASP.NET AJAX extenders and controls, as well as a rich array of controls that can be used out of the box to create an interactive Web experience.

    The Ajax Control Toolkit contains more than 40 controls, including the AutoComplete, CollapsiblePanel, ColorPicker, MaskedEdit, Calendar, Accordion, and Watermark controls.

    Don’t miss to check all controls in online demo at ASP.Net control toolkit samples.

    Note : With ASP.Net ajax toolkit it is possible to have combobox having long text inside list items but fixed size combobox control. Check your self in above link.

    Microsoft has published huge learning contents including videos and articles , explore your self to learn Ajax toolkit here.

    Configure ASP.Net Ajax Toolkit :

    1. Download the latest release of the Ajax Control Toolkit from CodePlex.
    2. Unzip binary contents.
    3. Add tab to toolbox and choose item -> browse -> locate : AjaxControlToolkit.dll

    Note : You may encounter problems when trying to use the Ajax Control Toolkit for the first time.
    Asp.net Ajax Control Toolkit needs to be setup using its script manager and not the standard asp.net Script Manager.So one way to avoid the above issues is to drag the ToolkitScriptManager (found in the control toolkit) onto the Form tag and then use any other Ajax Controls.

  • HTA Widget – JCOMQuery

    JComQuery is javascript framework library that simplifies HTA on windos (HTML trusted applications) development on windows. HTA’s are HTML based desktop applications. With JComQuery you can develop application in JavaScript (IDE Used will be Vs2008 and above including Express editions) with features like Widgets, irregular shape HTML windows, Multithreaded javascript tasks,asyc. Or background long running tasks, Email, zip , unzip functionality (using PKZIP),FTP classes,application themes (mac : default as theme) etc. Alone with JavaScript libraries the framework includes COM components written in VB 6.0 and VC++.The source code for each component is included in the series of articles written on my blog.
    This post will include

    1.  JComCodeGenerator : Generating JavaScript wrapper code for COM interface that can be directly used to program with Intellisense features of VS2008 and above (including express editions) in javascript.The article also includes VB6 application which can read and COM interface and create Javascript library for HTA application.
    2. JComEventBridge : Library to route COM events to javascript library. The article includes VB 6 and VC++ libraries.The JComBridge acts as Bridge between HTML and COM libraries. By default there is no way to trap events generated by COM libraries (Except WScript engine).
    3. JComWindowsShaper : COM component written in VB6 which is used by JComQuery to create widgets and windows form effects like semitransparent, rounded rect windows etc.
    4. JComMultiThread : COM component to create long running background tasks.
    5. Base Java Script Libraries : JavaScrit base libraries to provide basic functionalities like email,threading etc.

    If you are familier to .net CLR you can easily develop rich HTA applications with minial javascript and html (css) code.

    All above mentioned components can be build into single ActiveX control and deployed as plugin to develop web based widgets application.The application will be limited on windows platform only.
    Please visit again to see updates on this post …

  • Javascript framework for HTA : Introduction

    Hope fully by next week i will publish Javascript framework for HTA (HTML trusted application) applications. The framework enables fast development in HTA based application with skin support. The default skin provided is for mac and can be customised easily just by changing skin css. Framework support email, FTP compression etc. from HTA application. The framework is capable to capture COM events in HTA application. Visual Studio 2008 express edition was used as code editor to enable intelligence in javascript and hence fast development. Some of the components used in framework are :

    COM Bridge : Tech- VC++, VB 6.0 – COM event sink to capture events from COM and pass them back to HTA or Javascript.

    JS COM Wrapper Code Generator: Tech – VB 6.0: Microsoft Visual Studio 2008 editor support Javascript intelligence with the help of commented xml tags. This tool generates wrapper javascript for any COM component which can be then used in your HTA page. The auto generated code emits xml tags alone with wrapper code. The wrapper code also takes care of event delegates etc.

    Will also post some experience on loading data from database to html tables asynchronously to meet high performance in HTA application.

    The framework was designed to develop small applications which will run with just HTML and javascript.