Removing jQuery from an existing codebase

The past couple of weeks I’ve been looking into removing jQuery from SCEditor my JS WYSIWYG editor.

SCEditor was created in 2011 when support for IE < 9 was required and with Query being so popular at the time it made sense to use it.

Now in 2017, IE < 9 only accounts for a small fraction of web users and is no longer supported by Microsoft so I think it now makes sense to drop support for it in SCEditor too.

That now means SCEditor can use all the new browser API’s that IE 8 and below lack making jQuery

So I created a test branch to try it out and see if it would be worth doing.

Removing jQuery from SCEditor

There were a few bits of jQuery that needed to be re-implemented and a few bits where a nicer wrapper around the native API made sense.

For the on() and off() event handlers I created a small wrapper to allow binding multiple events at once and also having a selector to match the jQuery API:

/**
 * For on() and off() if to add/remove the event
 * to the capture phase
 *
 * @type {boolean}
 */
export var EVENT_CAPTURE = true;

/**
 * For on() and off() if to add/remove the event
 * to the buibble phase
 *
 * @type {boolean}
 */
export var EVENT_BUBBLE = false;

/**
 * Adds an event listener for the specified events.
 *
 * Events should be a space seperated list of events.
 *
 * If selector is specified the handler will only be
 * called when the event target matches the seector.
 *
 * @param {!Node} node
 * @param {string} events
 * @param {string} [selector]
 * @param {function(Object)} fn
 * @param {boolean} [capture=false]
 * @see off()
 */
export function on(node, events, selector, fn, capture) {
  events.split(' ').forEach(function (event) {
    var handler;

    if (utils.isString(selector)) {
      handler = fn['_sce-event-' + event + selector] || function (e) {
        var target = e.target;
        while (target && target !== node) {
          if (is(target, selector)) {
            fn.call(target, e);
            return;
          }

          target = target.parentNode;
        }
      };

      fn['_sce-event-' + event + selector] = handler;
    } else {
      handler = selector;
      capture = fn;
    }

    node.addEventListener(event, handler, capture || false);
  });
}

/**
 * Removes an event listener for the specified events.
 *
 * @param {!Node} node
 * @param {string} events
 * @param {string} [selector]
 * @param {function(Object)} fn
 * @param {boolean} [capture=false]
 * @see on()
 */
export function off(node, events, selector, fn, capture) {
  events.split(' ').forEach(function (event) {
    var handler;

    if (utils.isString(selector)) {
      handler = fn['_sce-event-' + event + selector];
    } else {
      handler = selector;
      capture = fn;
    }

    node.removeEventListener(event, handler, capture || false);
  });
}

For the selection API’s, luckily SCEditor doesn’t use any that require relative selectors which made replacing them fairly simple:

/**
 * Finds any child nodes that match the selector
 *
 * @param {!HTMLElement} node
 * @param {!string} selector
 * @returns {NodeList}
 */
export function find(node, selector) {
  return node.querySelectorAll(selector);
}

/**
 * Checks if node matches the given selector.
 *
 * @param {?HTMLElement} node
 * @param {string} selector
 * @returns {boolean}
 */
export function is(node, selector) {
  var result = false;

  if (node && node.nodeType === ELEMENT_NODE) {
    var doc = node.ownerDocument;
    var parent, nextSibling, isAppended;

    // IE 9 fails on disconnected nodes so must
    // add them to the body to test them
    if (IE_VER < 10 && !node.document) {
      isAppended = true;
      parent = node.parentNode;
      nextSibling = node.nextSibling;

      appendChild(doc.body, node);
    }

    result = (node.matches || node.msMatchesSelector ||
      node.webkitMatchesSelector).call(node, selector);

    // Put node back where it came from after IE 9 fix
    if (isAppended) {
      remove(node);

      if (parent) {
        parent.insertBefore(node, nextSibling);
      }
    }
  }

  return result;
}

There’s a small issue with IE 9 not matching detached nodes but you can work around it by tempoarily adding it to the body element and removing it again.

The only method that had to be re-implemented entirely (no similair built in APIs) is extend(). There is Object.assign() for shallow copying but lack of IE support makes it no use for SCE.

In the end I came up with:

/**
 * Extends the first object with any extra objects passed
 *
 * If the first argument is boolean and set to true
 * it will extend child arrays and objects recursively.
 *
 * @param {boolean} [isDeep]
 * @param {!Object} targetArg
 * @param {...Object} sourceArg
 * @return {Object}
 */
export function extend(targetArg, sourceArg) {
  var isTargetBoolean = targetArg === !!targetArg;
  var i      = isTargetBoolean ? 2 : 1;
  var target = isTargetBoolean ? sourceArg : targetArg;
  var isDeep = isTargetBoolean ? targetArg : false;

  for (; i < arguments.length; i++) {
    var source = arguments[i];

    // Copy all properties for jQuery compatibility
    /* eslint guard-for-in: off */
    for (var key in source) {
      var value = source[key];

      // Skip undefined values to match jQuery
      if (!isUndefined(value)) {
        var isObject = value !== null && typeof value === 'object';
        var isArray = Array.isArray(value);

        if (isDeep && (isObject || isArray)) {
          target[key] = extend(
            true,
            target[key] || (isArray ? [] : {}),
            value
          );
        } else {
          target[key] = value;
        }
      }
    }
  }

  return target;
}

And that’s about it. The above accounts for the majority of SCEditors jQuery usage with the remainder being quite easy to replace.

The final commit: e7b230c.

It took a while and a lot of changes but it went fairly smoothly. The unit tests were very helpful catching a few edge cases that I missed.

I’m sure there will be more I’ve missed given how big a change it is but so far it’s looking good from all my manual testing.

Conculustion

Overall there removing jQuery didn’t increase the output size much at all The final output sizes were:

Part of taht is from finding code that could be removed and some optimisation during the conversion so it’s not a perfect comaprrison.

Personally, if you’re already using jQuery then removing it probably isn’t worth it unless you’re writing a library. jQuery has a nice API, it fixes some browser inconsistancies and is well tested.

If you’re starting from scratch, only need to support IE9+ and only using jQuery for basic DOM manipulation and even handling, then it’s probably worth using the native browser API’s instead of jQuery.

However if you need to support IE < 9 then use jQuery! IE < 9 isn’t worth not using jQuery.

You need JavaScript enabled to see comments.