Optimizing AJAX Requests with a Custom Wrapper in jQuery

Table of Contents

  1. Introduction
  2. Approach 1: Cancelling Only Duplicate Requests
  3. Approach 2: Cancelling All Active Requests
  4. Approach 3: Handling Only the Latest Response
  5. Comparison of Approaches
  6. Pros and Cons of Adding a Wrapper in jQuery AJAX
  7. Choosing the Right Approach
  8. Final Thoughts
  9. Conclusion

Introduction

In modern web applications, efficient network communication is crucial for performance and user experience. One common issue arises when multiple duplicate API requests are triggered, leading to unnecessary network traffic and potential data inconsistencies. To mitigate this, we can implement a custom AJAX wrapper in jQuery. This article explores three approaches:

  1. Cancelling Only Duplicate Requests: Tracks active requests and cancels duplicate API calls.
  2. Cancelling All Active Requests: Cancels all ongoing AJAX requests before initiating a new one.
  3. Handling Only the Latest Response: Processes only the latest response while ignoring older ones.

Avoid Race Conditions

One of the primary motivations for implementing a custom AJAX wrapper is to avoid race conditions in the code. Race conditions occur when multiple asynchronous requests are initiated simultaneously, and their responses arrive out of order, leading to inconsistent or incorrect data being displayed in the UI. For example, if a user rapidly clicks a button that triggers multiple API calls, the responses might not return in the same order they were sent, causing the UI to reflect outdated or incorrect information. By canceling duplicate requests, aborting all active requests, or processing only the latest response, we can ensure that the application handles data in a predictable and consistent manner, effectively mitigating race conditions.


Lazy vs Eager Approach

When optimizing AJAX requests, you can adopt either a lazy or eager approach. A lazy approach focuses on minimizing redundant requests by canceling duplicates or ignoring outdated responses. On the other hand, an eager approach aggressively cancels all active requests to ensure only the latest request is processed. The choice depends on your application's requirements and the nature of the API calls.


Approach 1: Cancelling Only Duplicate Requests

1 Core Concept

  • Each API request is assigned a unique ID based on the URL and method.
  • Active requests are stored in a Map.
  • When a duplicate request is detected, the previous one is aborted.
  • Requests are removed from tracking once completed.

1 Code Implementation

// Map to store active XHR requests
var x_activeXHRsMap = new Map();

// Function to override jQuery's AJAX method
function overrideAjaxCalls($, utils) {
  var originalAjax = $.ajax;

  $.ajax = function (options) {
    // Skip non-relevant API calls
    if (!utils.isIrServicePath(options.url)) {
      return originalAjax.call($, options);
    }

    // Generate a unique request ID based on URL and method
    const requestId = utils.generateRequestId(options.url, options.method || "GET");

    // Check if a duplicate request is already in progress
    if (x_activeXHRsMap.has(requestId)) {
      const existingXHR = x_activeXHRsMap.get(requestId);
      if (existingXHR && existingXHR.readyState !== 4) {
        existingXHR.abort(); // Abort the duplicate request
        console.log(`Aborted duplicate API call: ${options.url}`);
      }
    }

    // Initiate the new request
    let xhr = originalAjax.call($, options);
    x_activeXHRsMap.set(requestId, xhr); // Track the new request

    // Clean up the request from the map once it completes
    xhr.always(() => {
      x_activeXHRsMap.delete(requestId);
    });

    return xhr;
  };
}

Approach 2: Cancelling All Active Requests

2 Core Concept

  • Cancels all ongoing AJAX requests before initiating a new one.
  • Useful when clicking a button triggers multiple requests in quick succession.

2 Code Implementation

// Array to store active XHR requests
var activeXHRs = [];

// Function to override jQuery's AJAX method
function overrideAjaxCalls($) {
  var originalAjax = $.ajax;

  $.ajax = function (options) {
    // Abort all active XHR requests before starting a new one
    activeXHRs.forEach(xhr => xhr.abort());
    activeXHRs = []; // Clear the array

    // Initiate the new request
    let xhr = originalAjax.call($, options);
    let originalFailHandlers = [];

    // Handle failed requests
    xhr.fail((xhrObj, status, error) => {
      if (status === "abort") {
        console.warn(`Request aborted: ${options.url}`);
      } else {
        originalFailHandlers.forEach((fn) => fn.apply(this, [xhrObj, status, error]));
      }
    });

    // Override the fail method to capture custom handlers
    xhr.fail = function (callback) {
      if (typeof callback === "function") {
        originalFailHandlers.push(callback);
      }
      return this;
    };

    // Track the new request
    activeXHRs.push(xhr);

    // Clean up the request from the array once it completes
    xhr.always(() => {
      activeXHRs = activeXHRs.filter(activeXhr => activeXhr !== xhr);
    });

    return xhr;
  };
}

Approach 3: Handling Only the Latest Response

3 Core Concept

  • Does not abort previous requests but only processes the latest response.
  • Uses a versioning mechanism to ignore outdated responses.

3 Code Implementation

// updating original XHR method & using return
function overrideAjaxCalls($, utils) {
  var originalAjax = $.ajax;

  $.ajax = function (options) {
    if (!utils.isIrServicePath(options.url)) {
      return originalAjax.call($, options);
    }

    let X__currentXHRVersion = X__XHRVersion;

    // Wrap success callback
    if (options.success) {
      let originalSuccess = options.success;
      options.success = function () {
        if (X__currentXHRVersion === X__XHRVersion) {
          console.log("from success");
          originalSuccess.apply(this, arguments);
        }
      };
    }

    // Wrap error callback
    if (options.error) {
      let originalError = options.error;
      options.error = function () {
        if (X__currentXHRVersion === X__XHRVersion) {
          console.log("from error");
          originalError.apply(this, arguments);
        }
      };
    }

    // Wrap complete callback
    if (options.complete) {
      let originalComplete = options.complete;
      options.complete = function () {
        if (X__currentXHRVersion === X__XHRVersion) {
          console.log("from complete");
          originalComplete.apply(this, arguments);
        }
      };
    }

    let xhr = originalAjax.call($, options);

    // Store original functions
    let originalDone = xhr.done;
    let originalFail = xhr.fail;
    let originalThen = xhr.then;
    let originalAlways = xhr.always;

    // Override .done()
    xhr.done = function (callback) {
      return originalDone.call(this, function () {
        console.log("done - X__currentXHRVersion, X__XHRVersion", X__currentXHRVersion, X__XHRVersion);
        if (
          X__currentXHRVersion === X__XHRVersion &&
          typeof callback === "function"
        ) {
          console.log("from done");
          return callback.apply(this, arguments);
        }
      });
    };


    // Override .fail()
    xhr.fail = function (callback) {
      return originalFail.call(this, function () {
        console.log("fail - X__currentXHRVersion, X__XHRVersion", X__currentXHRVersion, X__XHRVersion);
        if (
          X__currentXHRVersion === X__XHRVersion &&
          typeof callback === "function"
        ) {
          console.log("from fail");
          return callback.apply(this, arguments);
        }
      });
    };

    // Override .then()
    xhr.then = function (successCallback, failureCallback) {
      return originalThen.call(
        this,
        successCallback
          ? function () {
            console.log("then success - X__currentXHRVersion, X__XHRVersion", X__currentXHRVersion, X__XHRVersion);
              if (X__currentXHRVersion === X__XHRVersion) {
                console.log("from then success");
                return successCallback.apply(this, arguments);
              }
            }
          : undefined,
        failureCallback
          ? function () {
            console.log("then fail - X__currentXHRVersion, X__XHRVersion", X__currentXHRVersion, X__XHRVersion);
              if (X__currentXHRVersion === X__XHRVersion) {
                console.log("from then failure");
                return failureCallback.apply(this, arguments);
              }
            }
          : undefined
      );
    };

    // Override.always();
    xhr.always = function (callback) {
      return originalAlways.call(this, function () {
        console.log("always - X__currentXHRVersion, X__XHRVersion", X__currentXHRVersion, X__XHRVersion);
        if (
          X__currentXHRVersion === X__XHRVersion &&
          typeof callback === "function"
        ) {
          console.log("from always");
          return callback.apply(this, arguments);
        }
      });
    };

    return xhr;
  };
}

Comparison of Approaches

Feature Cancelling Duplicate Requests Cancelling All Requests Handling Only Latest Response
Use Case Avoid duplicate API calls Cancel all requests before new one Ensure only the latest response is processed
Request Tracking Tracks using unique request ID No tracking, just abort all Uses versioning for response management
Best For APIs with heavy traffic UI events triggering rapid API calls Ensuring UI reflects only the most recent data

Pros and Cons of Adding a Wrapper in jQuery AJAX

Pros

  • Improves performance by reducing redundant API calls.
  • Enhances user experience by avoiding unnecessary delays.
  • Provides better resource management and automatic cleanup.
  • Offers more control over API calls.
  • Prevents UI flickering and inconsistent states.

Cons

  • Approach 2 may cancel important requests.
  • Requires additional tracking logic, adding some complexity.
  • Debugging aborted requests can be more difficult.
  • Might interfere with WebSocket or long-polling requests.

Choosing the Right Approach

Feature Approach 1 Approach 2 Approach 3
Efficiency High Medium High
Control Fine-grained Aggressive Moderate
Best For Avoiding duplicates UI-triggered frequent API calls Processing only the latest response

Final Thoughts

  • Approach 1 (Cancelling Only Duplicate Requests) is best for APIs that experience duplicate calls.
  • Approach 2 (Cancelling All Requests) is useful when UI actions trigger multiple requests rapidly.
  • Approach 3 (Handling Only Latest Response) is ideal when only the most recent response should be used.

Conclusion

By implementing these strategies, developers can optimize network usage, improve performance, and enhance user experience in web applications. Each approach has its strengths and trade-offs, so choose the one that aligns best with your application's requirements. Whether you need to cancel duplicates, abort all requests, or process only the latest response, a custom AJAX wrapper in jQuery can help you achieve your goals efficiently.