We’d love to hear from you to learn how we can make Outside better. Tell us what you think.

Technology

We Removed jQuery

A quick overview of Outside's journey to rid its Drupal 7 site of jQuery and use emerging standards

A quick overview of Outside's journey to rid its Drupal 7 site of jQuery and use emerging standards

Disclaimer 1: We actually didn't totally remove jQuery as we have a few custom features and pages that use it, but for ~99.7% of our pages, it's gone!

Disclaimer 2: The following method isn't something we recommend if you rely heavily on front-end contrib modules, authenticated traffic, or parts of Drupal 7 core that depend on jQuery.

What The Heck Is jQuery?

jQuery makes writing JavaScript code easier and quicker by abstracting overly complicated JavaScript tasks (HTML document traversal and manipulation, event handling, animation, and Ajax) with an easy-to-use API that works across a multitude of browsers. Originally created by John Resig, jQuery is ubiquitous in front-end development and is used by millions of developers on a daily basis. Some reports even indicate that 97% of the web uses jQuery.

Why Remove jQuery?

I love jQuery. It's what has kept me sane by helping me achieve many cross-browser effects throughout the years. I'm forever grateful for all the time I've saved and all of the great developers who brought this open-source goodness to the masses. But I started to realize that I (we) often use it as a crutch instead of really understanding what is happening with JavaScript code. So when the following four conditions were met, we decided it was time to remove it.

  1. We adopted a modern modular build system for our custom JavaScript.

  2. We fell below 2% of users using any version of Internet Explorer to view our site.

  3. Our quest for greater page-speed optimizations reached a point were we considered jQuery as unnecessary overhead.

  4. We wanted to be one of the first major consumer-publishing websites to be jQuery-free!

The above four conditions were paramount before we even thought about looking into removing jQuery. If, for instance, your site has bigger page-speed issues than a library that adds an additional 87kb (~30kb gzipped) to your page load, you should probably stop reading this and work out those issues, as you'll get greater bang for your buck.

Our Modern Build System

We started using a modern build system for our custom JavaScript in 2017. Prior to that, we were already using Gulp to minify our CSS and JavaScript before we drupal_add_js() it on our site, but we found that we were using a lot of repetitive code in our JavaScript files for each of our custom templates. Then we discovered Rollup.js, which allowed us to use modern JavaScript and ES6 style modules in order to modularize our frontend code. Each of our custom templates (content types) now had a single custom JavaScript file that we compiled. This greatly reduced the amount of overlapping code and taught us to think from a functional programming perspective rather than spaghetti coding the JavaScript for each of our templates. Our current setup looks something like this. 

  1. Gulp uses each JavaScript file found in specific ../modules/custom/**/js/src/ directories as an input file.
  2. Rollup then compiles each input file using Babel and adds in necessary pollyfills for ES6 code.
  3. Rollup then minifies and saves each production-ready JavaScript file in a ../modules/custom/**/js/dist/ directory. 
  4. Gulp watches our JavaScript files and dependencies.

There are many different build tools that allow you to do the same kinds of things, but we found that Gulp + Rollup was faster and more functional than the alternatives. As we started writing more performant code, we considered removing jQuery as a next step. We also wanted to trade in our jQuery plugins for more lightweight options. This forced us to really consider all our JavaScript necessary to run our user experience. View our full gulpfile.js at this gist.

Drupal 7 Dependency Hurdle

As you may know, Drupal 7 shipped with jQuery as a dependency, and the more I searched how to remove jQuery from Drupal 7, the more I lost hope. Then I remembered that we had done something similar with one of our progressive web app features, 100 Days of Winter, which was built with vue.js and used a hook_js_alter() to remove unnecessary scripts such as misc/drupal.js from that template.

We ended up with the following hook used in our template.php file.


/**
 * Implements hook_js_alter().
 */
function outside_js_alter(&$javascript) {
  // Remove jQuery dependent scripts and recreate settings.
  if (!path_is_admin(current_path()) && !user_is_logged_in() && empty($javascript['jquery'])) {
    $javascript['custom_settings'] = array(
      'type' => 'inline',
      'scope' => 'head_scripts',
      'weight' => -99.999,
      'group' => 0,
      'every_page' => TRUE,
      'requires_jquery' => FALSE,
      'cache' => TRUE,
      'defer' => FALSE,
      'preprocess' => TRUE,
      'version' => NULL,
      'data' => 'document.querySelector("html").classList.add("js"); var Drupal = {}; Drupal.settings = ' . json_encode(array_merge_recursive(...$javascript['settings']['data'])),
    );
    $remove_scripts = array(
      // Jquery dependent JS to remove.
      'misc/drupal.js',
      'settings',
      'misc/ajax.js',
      'misc/progress.js',
    );
    // Remove it.
    foreach (array_keys($javascript) as $value) {
      if (in_array($value, $remove_scripts) || strpos($value, 'jquery')) {
        unset($javascript[$value]);
      }
    }
  }
  elseif (!empty($javascript['jquery'])) {
    // Remove placeholder.
    unset($javascript['jquery']);
  }
}

This hook allows us to remove jQuery-dependent JavaScript, which Drupal adds, and still allows us to grab important Drupal.settings data in JavaScript. We also built in an override that allows jQuery to be added to the page should we need it using a simple drupal_add_js('jquery'); function. We definitely had to rework any Drupal.attachBehaviors() calls, but we never liked that method in the first place, so there weren't many to begin with. (Your mileage may vary depending on which scripts your site requires.)

The next issue for us was replicating the functionality of Drupal AJAX forms found in misc/ajax.js, which we accomplished with a custom JavaScript module. 


/*
 * Drupal ajax.js replacement
 *
 * @description
 *  JS for replicating default drupal ajax form handling.
 *
 */

import extend from './extend.js';
import propegatedEvent from './propegated-event.js';
import stringProcessScript from './string-process-script';

const ajaxCommands = {
  insert(formEl) {
    let targetEl = document.querySelector(this.selector) || formEl;
    switch (this.method) {
      case 'html':
        targetEl.innerHTML = stringProcessScript(this.data, targetEl);
        break;
      case 'append':
        targetEl.insertAdjacentHTML('beforeend', stringProcessScript(this.data, targetEl))
        break;
      case 'prepend':
        targetEl.insertAdjacentHTML('beforebegin', stringProcessScript(this.data, targetEl));
        break;
      case null:
        targetEl.insertAdjacentHTML('afterend', stringProcessScript(this.data, targetEl));
        targetEl.remove();
        break;
      default:
        alertNonExisting(this);
        break;
    }
  },
  settings() {
    if (this.merge) {
      Drupal.settings = extend(Drupal.settings, this.settings);
    }
    else {
      // Don't have a need for this yet (ajax settings)?
      // ajax.settings = response.settings;
    }
  },
  updateBuildId() {
    document.querySelector('input[name="form_build_id"][value="' + this.old + '"]').value = this.new;
  }
}

function alertNonExisting(obj) {
  /*eslint-disable */
  console.log("%cThis ajax command isn't setup yet: " + obj.command + (obj.method ? "-" + obj.method : ""), "color: red");
  console.log(obj);
  /*eslint-enable */
}

export default function(formSelector) {
  formSelector = formSelector || '[data-js-ajax-form]';
  propegatedEvent(document.body, ['submit'], formSelector, function(e){
    e.preventDefault();
    let postData = new URLSearchParams(new FormData(document.querySelector(formSelector))).toString(),
        xhttp = new XMLHttpRequest(),
        msg = document.querySelector(formSelector).parentElement.querySelector('.messages');
    // Remove old messages.
    if (msg) {
      msg.remove();
    }
    // Add callback.
    xhttp.onreadystatechange = function() {
      if (this.readyState == 4 && this.status == 200) {
        let formData = JSON.parse(this.responseText);
        // Loop through drupal response commands.
        formData.forEach((cmd) => {
          if (ajaxCommands[cmd.command]) {
            ajaxCommands[cmd.command].call(cmd, document.querySelector(formSelector));
          }
          else {
            alertNonExisting(cmd);
          }
        });
      }
    };
    xhttp.open("POST", "/system/ajax", true);
    xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8");
    xhttp.send(postData + '&ajax_page_state%5Btheme_token%5D=' + Drupal.settings.ajaxPageState.theme_token + '&ajax_page_state%5Btheme%5D=' + Drupal.settings.ajaxPageState.theme);
  })
}

So we didn't replicate all the functionality, but just enough to cover our own usage while providing a console.log message, should we need help with additional methods in the future.

Real-World File-Size Savings

Before our jQuery removal refactoring, the bundle file on our most popular content type, article.min.js, was 139kb minified (gzipped 47kb). After our refactor, our jQuery-free code comes in at 50kb (gzipped 15kb).

This might seem trivial, but when you consider that we have over 5 million pageviews to the article content type in any given 30 days, it translates to roughly 150GB of bandwidth saved. Also we've tested the JavaScript computation time on mobile devices, which accounts for 70% of site traffic, and we found that on average we are saving roughly 177 ms of processing time for our article.min.js file compared with the jQuery version. That time savings is even larger on older devices and on antiquated jQuery plugins, which don't utilize newer browser APIs like MutationObserver and Intersection Observer.

Recap And Final Thoughts

Keep in mind this was not just a simple exercise in removing jQuery from our bundled code, but also many jQuery plugins, which forced us to rewrite the majority of our JavaScript code from the ground up. The entire process has been kind of exhausting, as we had to test nearly every template type against all our browser and device requirements. But we've streamlined our code and reduced our JavaScript footprint by almost 65% on the majority of our bundles, and we've learned valuable insights along the way—e.g., browsers aren't that dissimilar when it comes to JavaScript rendering. We also found that the new methods are also the best methods, and that modern browsers are often optimized for such.

Filed To: Technology
Pinterest Icon