Are there any quicker methods to surrender to the Javascript event loop other than using setTimeout(0)?

Attempting to create a web worker that can pause during computation is proving to be challenging. As of now, the only method I am aware of (aside from using Worker.terminate()) involves periodically allowing the message loop to check for any incoming messages. For instance, the current web worker calculates the sum of integers from 0 to data, but if a new message is received while the calculation is ongoing, it will halt the current process and initiate a new one.

let currentTask = {
  cancelled: false,
}

onmessage = event => {
  // Cancel the current task if there is one.
  currentTask.cancelled = true;

  // Start a new task (leveraging JavaScript object reference).
  currentTask = {
    cancelled: false,
  };
  performComputation(currentTask, event.data);
}

// Wait for setTimeout(0) to finish in order to receive pending messages.
function yieldToMacrotasks() {
  return new Promise((resolve) => setTimeout(resolve));
}

async function performComputation(task, data) {
  let total = 0;

  while (data !== 0) {
    // Perform some calculations.
    total += data;
    --data;

    // Yield to the event loop.
    await yieldToMacrotasks();

    // Check if this task has been superseded by another one.
    if (task.cancelled) {
      return;
    }
  }

  // Return the result.
  postMessage(total);
}

While this approach works, it is incredibly slow. Each iteration of the while loop on average takes 4 ms on my machine! This overhead significantly impacts cancellation speed.

What is causing this slow performance? Are there quicker alternatives to achieve interruptible computations?

Answer №1

Absolutely, the message queue takes precedence over timeouts, resulting in a higher firing frequency.

To easily connect to that queue, you can utilize the MessageChannel API:

let i = 0;
let j = 0;
const channel = new MessageChannel();
channel.port1.onmessage = messageLoop;

function messageLoop() {
  i++;
  // loop
  channel.port2.postMessage("");
}
function timeoutLoop() {
  j++;
  setTimeout( timeoutLoop );
}

messageLoop();
timeoutLoop();

// for logging purposes
requestAnimationFrame( display );
function display() {
  log.textContent = "message: " + i + '\n' +
                    "timeout: " + j;
  requestAnimationFrame( display );
}
<pre id="log"></pre>

You might want to consider batching multiple rounds of the same operation per event loop as well.

These are some reasons why this approach is effective:

  • According to specifications, setTimeout will be limited to a minimum of 4ms after the fifth level of calls, which occurs after the fifth iteration of the loop.
    Message events do not have this limitation.

  • In certain cases, some browsers may assign lower priority to tasks initiated by setTimeout.
    For instance, Firefox does this during page loading to prevent blocking other events with setTimeout calls at that time. They even create a separate task queue for it.
    Although not officially specified, it seems that in Chrome, message events have a "user-visible" priority, potentially allowing UI events to take precedence. (This was tested using the upcoming scheduler.postTask() API in Chrome)

  • Modern browsers often throttle default timeouts when the page is inactive, and this behavior may apply to Workers as well.
    Message events are exempt from this restriction.

  • As OP discovered, Chrome enforces a minimum of 1ms even for the initial 5 calls.


However, it's important to note that these limitations on setTimeout exist because scheduling numerous tasks at such a rapid pace comes with a cost.

Only implement this method within a Worker thread!

Utilizing this in a Window context will slow down essential browser tasks like Network requests and Garbage Collection, deeming them less crucial.
Additionally, introducing a new task into the mix requires the event loop to operate at a high frequency without any downtime, resulting in increased energy consumption.

Answer №2

What's causing such slow performance?

In the Blink engine used by Chrome, there is a minimum timeout set to 4 ms:

// Chromium uses a minimum timer interval of 4ms. We'd like to go
// lower; however, there are poorly coded websites out there which do
// create CPU-spinning loops.  Using 4ms prevents the CPU from
// spinning too busily and provides a balance between CPU spinning and
// the smallest possible interval timer.
static constexpr base::TimeDelta kMinimumInterval =
    base::TimeDelta::FromMilliseconds(4);

It is noteworthy that this minimum of 4 ms is applied only when the nesting level exceeds 5. However, regardless of the nesting level, the minimum timeout is still set to 1 ms in all cases:

  base::TimeDelta interval_milliseconds =
      std::max(base::TimeDelta::FromMilliseconds(1), interval);
  if (interval_milliseconds < kMinimumInterval &&
      nesting_level_ >= kMaxTimerNestingLevel)
    interval_milliseconds = kMinimumInterval;

Conflicting opinions exist between WHATWG and W3C specifications regarding whether the minimum of 4 ms should always apply or only above a certain nesting level. Nevertheless, the implementation in the HTML specification takes precedence, and Chrome appears to adhere to the WHATWG spec.

Despite this information, it is puzzling why my tests indicate a delay of 4 ms.


Is there a more efficient approach to address this issue?

Inspired by Kaiido's suggestion to utilize another message channel, you can try this method:


let currentTask = {
  cancelled: false,
}

onmessage = event => {
  currentTask.cancelled = true;
  currentTask = {
    cancelled: false,
  };
  performComputation(currentTask, event.data);
}

async function performComputation(task, data) {
  let total = 0;

  let promiseResolver;

  const channel = new MessageChannel();
  channel.port2.onmessage = event => {
    promiseResolver();
  };

  while (data !== 0) {
    // Do a little bit of computation.
    total += data;
    --data;

    // Yield to the event loop.
    const promise = new Promise(resolve => {
      promiseResolver = resolve;
    });
    channel.port1.postMessage(null);
    await promise;

    // Check if this task has been superceded by another one.
    if (task.cancelled) {
      return;
    }
  }

  // Return the result.
  postMessage(total);
}

Although not perfect, this code demonstrates improved efficiency with each loop taking around 0.04 ms on my machine.

Answer №3

After noticing a significant number of downvotes on my previous answer, I decided to test the code in this solution using my newfound knowledge regarding the enforced delay of approximately 4ms by setTimeout(..., 0) (specifically on Chromium). I introduced a workload of 100ms in each loop and strategically scheduled setTimeout() before the workload to account for the initial 4ms delay imposed by it. I also made similar adjustments with the postMessage() method for fairness and modified the logging approach.

The outcome was unexpected: initially, the message method outperformed the timeout method by 0-1 iterations while observing the counters; however, this trend remained consistent even up to 3000 iterations. This proves that a setTimeout() paired with a concurrent pathMessage() can maintain its efficiency share (in Chromium).

Interestingly, when the iframe was scrolled out of view, there was a notable shift in results - nearly 10 times more message-triggered workloads were processed compared to timeout-based ones. This behavior is likely tied to the browser's optimization strategy of allocating fewer resources to JavaScript operations outside the visible scope or in background tabs.

In Firefox, I observed a workload processing ratio of 7:1 in favor of messages over timeouts. Surprisingly, whether actively monitoring the process or leaving it running in another tab did not seem to influence the outcome.

Subsequently, I migrated the code (with slight modifications) to a Worker, leading to an intriguing revelation: the iterations processed through timeout scheduling mirrored exactly those based on message scheduling. This consistency was evident across both Firefox and Chromium environments.

let i = 0;
let j = 0;
const channel = new MessageChannel();
channel.port1.onmessage = messageLoop;

timer = performance.now.bind(performance);

function workload() {
  const start = timer();
  while (timer() - start < 100);
}

function messageLoop() {
  i++;
  channel.port2.postMessage("");
  workload();
}
function timeoutLoop() {
  j++;
  setTimeout( timeoutLoop );
  workload();
}

setInterval(() => log.textContent =
  `message: ${i}\ntimeout: ${j}`, 300);

timeoutLoop();
messageLoop();
<pre id="log"></pre>

Answer №4

After conducting multiple tests, I can confirm that the round trip time of setTimeout(..., 0) is approximately 4ms, although not consistently. To test this, I utilized a worker with the following setup (initialized with

let w = new Worker('url/to/this/code.js'
and terminated with w.terminate()).

During the initial rounds, the pause duration was less than 1ms. Subsequently, there was an outlier at around 8ms before stabilizing at around 4ms for each subsequent iteration.

To minimize the wait time, I positioned the yieldPromise executor before the workload execution. This adjustment allowed setTimeout() to maintain its minimum delay without unnecessarily extending the work loop pause. It seems that the workload must exceed 4ms to see any significant effect. Unless, of course, detecting the cancel message itself constitutes the workload... ;-)

Outcome: Only ~0.4ms delay observed. In other words, a reduction by a factor of at least 10.1

'use strict';
const timer = performance.now.bind(performance);

async function work() {
    while (true) {
        const yieldPromise = new Promise(resolve => setTimeout(resolve, 0));
        const start = timer();
        while (timer() - start < 500) {
            // perform work here
        }
        const end = timer();
        // const yieldPromise = new Promise(resolve => setTimeout(resolve, 0));
        await yieldPromise;
        console.log('Time taken to resume working:', timer() - end);
    }
}
work();


1 Could it be possible that the browser restricts timer resolution within that range? If so, further enhancements might go undetected...

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Headers permitted for CORS blocking

Greetings, I have recently developed an API and frontend using React, utilizing axios for making requests. I implemented an authorization header with axios and authenticated it on the PHP server-side. However, there seems to be an issue where the Authoriza ...

What could be causing the row not to delete from the table? Is there something crucial that is overlooked in the JavaScript, HTML, or PHP function?

In my admin interface, I have tables that display various data: questions, replies, and users. Each row in these tables includes a small cancel.png image, meant to serve as a delete button. However, clicking on these buttons does not trigger any action, ev ...

Adding conditional href based on a specific criteria to an <a> tag in AngularJs

I have been working on a menu list where some menus contain URLs and others do not. When a menu item includes a URL, the href attribute is displayed, otherwise just the span tag is shown. I tried checking it like this but ended up with href="#" when the me ...

JavaScript is failing to set the value of the hidden field

Struggling to transfer the clicked star's value to a hidden field within an existing JavaScript popup for a rating system. The current code is: echo 'Please rate the quality of this transcript:<br/>'; echo '<div id="stars"> ...

How can I detect the @mouseup event in a Vue Bootstrap slider?

I have been using a vue-bootstrap slider that comes with two actions, @change and @update. My goal is to capture the final position of the slider once the movement ends. To achieve this, I decided to test both actions. In my scenario, @change triggers ev ...

What is the best way to rearrange DOM elements using the output of a shuffle function?

Looking for a solution to shuffle and move around cards in an HTML memory game? Let's analyze the current setup: <ul class="deck"> <li class="card"> <i class="fa fa-diamond"></i> </li> ...

Even though my form allows submission of invalid data, my validation process remains effective and accurate

Here is the code I have written: <!doctype html> <html lang="en"> <head> <title>Testing form input</title> <style type="text/css></style> <script type="text/javascript" src="validation.js"></script> &l ...

Setting Start and End Dates in Bootstrap Vue Datepicker to Ensure End Date is After Start Date

My Vue.js form includes two BootstrapVue datepickers for holiday management. Users can define the start date and end date of their holiday, with the condition that the end date must be equal to or greater than the start date. Here is my current implementat ...

What is the correct way to activate buttons within a v-for loop in Vue.js?

My current situation is as follows: https://plnkr.co/edit/LdbVJCuy3oojfyOa2MS7 https://i.sstatic.net/ivWnE.png I am looking to activate the "Press me" buttons when the input changes. At this point, I have a code snippet that can detect when the input h ...

Passing a particular object from an array of objects as props in React Native

Suppose you have a static array consisting of 4 objects and the goal is to pass specific data items from the third object in this array. How can this task be accomplished? Let's consider an example: const ENTRIES = [ { name: "John" color: "#fffff" f ...

Checking for the existence of a Vue.js component

In the ready: method of the root instance, I have certain tasks to perform but only if some components are not present (not declared in the HTML). Is there a way to verify the existence of a component? ...

Oops! You're trying to render a Next.js page using a layout that has already been declared

I have created two different layouts in Next.js - MainLayout and DashboardLayout. import Navigation from '../Navigation'; export default function MainLayout({ children }) { return ( <> <Navigation links={[ ...

Leveraging ForEach to merge two arrays and generate a fresh entity

I am in search of my final result which should be as follows: result = [{x: '12/12', y: 90 }, {x: '12/11', y: 0}, {x: '12/10', y: 92}, {x: '12/9', y: 0}, ... ] Currently, I have two arrays to work with. The first a ...

The debate between client-side and server-side video encoding

My knowledge on this topic is quite limited and my Google search didn't provide any clear answers. While reading through this article, the author mentions: In most video workflows, there is usually a transcoding server or serverless cloud function ...

Generate a new passport session for a user who is currently logged in

I am currently utilizing passport js for handling the login registration process. After a new user logs in, a custom cookie is generated on the browser containing a unique key from the database. Here's how the program operates: When a new user logs ...

Gather image origins from a website, even if img tags are inserted through javascript or other methods during the page's rendering

I am looking to extract the URLs of all the images from a web page using C# and asp.net. Currently, I am utilizing: WebClient client = new WebClient(); string mainSource = client.DownloadString(URL); Afterwards, I am scanning the mainSource string for i ...

How to resolve: Preventing Ajax - Post request from executing repetitively

I am facing an issue with a table that is formatted using the Datatables script. There is a column in the table which contains icons for actions. When a user clicks on an icon, a modal is loaded and its content is fetched using the POST method. The modal ...

Conceal the loading image once the AJAX function has been successfully

I've been trying to figure out a way to load heavy images after an ajax call using animated GIF as a pre-loader, but haven't found a satisfactory solution yet. Here's the code snippet I'm currently using: function loadProducts(url) { ...

What could be causing certain emails to disappear when trying to retrieve them through IMAP?

import Imap from 'node-imap' import { simpleParser } from 'mailparser' import { inspect } from 'util' export default async function emailHandler(req, res) { try { const credentials = req.body const imapConnection = ...

Ways to reduce lag while executing a for loop

How can I optimize my prime number generation process to avoid lagging? For example, is there a way to instantly display the results when generating primes up to 1000? HTML: <!DOCTYPE html> <html> <meta name="viewport" content="width=dev ...