Ajay Poshak

August 24, 2019

Lazy Evaluation of low priority tasks

Browsers run on single thread which means that they can do single thing at a time. You may need to prioritize among different tasks to run more important tasks first and defer the execution of other non-important tasks.

For example, while scrolling a listing page, scroll event fires. This event adds some DOM elements to the list to achieve the infinite scroll effect. But at the same time, an analytics event is also fired, thus eating up the precious time in the main thread, thus delaying the code to append DOM elements in the list.

This can be achieved via setTimeout. Using setTimeout, non-important tasks can be pushed into callback queue. In callback queue, after it completes the delay time, the Event Loop pushes it into call stack whenever it finds call stack empty. Problem with this approach is that we can't have the fine grained control over when the task would be executed.

Recently an API called requestIdleCallback has been added that tells when the main thread would be idle. So instead of relying on the interplay of callback queue and callstack, we can reliabily defer the execution of non-essential tasks.

One of such use cases are analytics. Analytics while being extremely useful to provide valuable insights into user behaviour, at the same time does not contribute towards user experience. So it makes a good case of lazy execution of code.

We should push execution of analytics calls to requestIdleCallback so it won't block the execution of essential tasks.

But there might be some edge cases around this approach. One, what if user closes the browser tab or the browser itself before the main thread becomes idle. Another, what if main thread never becomes idle. So we need some sort of guarantee that we won't miss the analytics events in case of any of these edge cases occur.

Both of the above cases can be handled by building a wrapper around requestIdleCallback that guarantees that these events would be executed. That wrapper could use the beforeunload event that fires before a page/tab is closed.

So our solution, essentially would be a queue of events. Whenever an analytics event occurs, it'll be pushed to the queue. On every callback of requestIdleCallback an event would be removed from the queue and passed to the requestIdleCallback.

This wrapper would also listen to the beforeunload event, and whenever this event called, wrapper would execute all events in the queue synchronously. Thus, providing guarantee of execution also.

1class EventsQueue {
2  static instance;
3  constructor() {
4    if (typeof instance !== "undefined") {
5      return instance;
6    }
7    this.taskQueue = [];
8    window.addEventListener("beforeunload", this.runImmediately, true);
9    window.addEventListener(
10      "onVisibilityChange",
11      this.onVisibilityChange,
12      true,
13    );
14  }
15
16  push = (task) => {
17    this.taskQueue.push(task);
18    // Schedule Tasks to run as soon as they're added in queue
19    this.scheduleTaskToRun();
20  };
21
22  isEmpty = () => {
23    return this.taskQueue.length === 0;
24  };
25
26  runImmediately = () => {
27    while (!this.isEmpty()) {
28      const task = this.taskQueue.shift();
29      task();
30    }
31  };
32
33  /**
34   * Schedules tasks to run in rIC
35   * @memberof EventsQueue
36   */
37  scheduleTaskToRun = () => {
38    if (!this.isEmpty()) {
39      const task = this.taskQueue.shift();
40      this.runTask(task);
41    }
42  };
43
44  /**
45   * Gives the task to rIC to execute.  And also
46   * schedules another one for next rIC, if queue
47   * is not empty yet.
48   * @memberof EventsQueue
49   */
50  runTask = (task) => {
51    requestIdleCallback(task);
52    if (!this.isEmpty()) {
53      requestIdleCallback(this.execute());
54    }
55  };
56
57  onVisibilityChange = () => {
58    if (document.visibilityState === "hidden") {
59      this.runImmediately();
60    }
61  };
62}
63
64export default new EventsQueue();

Implementation <code class="code">example.js</code>

1import eventsQueue from "./EventsQueue";
2eventsQueue.push(pageViewEvent);

This is one the examples of the wonders requestIdleCallback can do. Deferring the execution of analytics code is one of the use cases of rIC. Another cases might be prefetching the API data for the next page, or downloading the dynamically loaded assets before user interacts with them.

References:

Cooperative Scheduling of Background Tasks

Using requestIdleCallback

Idlize: rIC made easy