0

I have a very large and complicated jQuery plugin which I will avoid posting here for simplicity's sake. My problem is very simple, and I will reduce it only to the relevant code:

I have a click event attached to a set of buttons:

$("ul.tick-boxes button").click(function(event) {
    event.preventDefault();
    $("ul.product-grid").addClass("loading");
    $(this).toggleClass("active");
    $theElement.trigger("filterOptionsChanged");
});

If you go to this link, you can see these in action in the left sidebar:

http://mazer.com/collections/refrigerator?preview_theme_id=22019779

Here is the css that produces a check-mark when you click the buttons:

ul.tick-boxes button.active .tick-box::after {
    content: "\e603";
    font-family: "custom-icons";
    color: #51425d;
    top: 2px;
    left: 2px;
    font-size: 0.75rem;
    position: absolute;
}

If your computer is as slow as mine, then when you click these filter options, it takes a second or so to "tick" the "tick-box". If you can't see it, try unticking it, which for me takes noticeably longer. The time-point where the "tick" visibly renders is always simultaneous with the product-grid rebuilding and rendering. I haven't posted the code for manipulating the product-grid, but you can know that the line $theElement.trigger("filterOptionsChanged") triggers a lot of array and object processing to build a document fragment of the new product list, and updates the DOM at the end. I understand this can take a second, that is not my problem. But what I don't understand is why my "tick-boxes" are waiting until after the code of that event is finished to render. According to my css, all I need is a class active on the button, and that code is fired one line above triggering the "filterOptionsChanged" event, so it should fire before any product grid changes happen.

Now. If I open up my inspector in chrome, I can actually see the active classes toggling instantaneously on click, before the product grid updates. However, the css which adds the tick doesn't catch the active class on the element until after my "filterOptionsChanged" code completes.

My first attempt to solve the problem will be posted below. I read a good bit about the "expensiveness" of css pseudo-selectors. That essentially to a browser, it is like a dom manipulation every time an ::after element is created. So I then write this css:

ul.tick-boxes button .tick-box::after {
    content: "\e603";
    font-family: "custom-icons";
    color: #51425d;
    top: 2px;
    left: 2px;
    font-size: 0.75rem;
    position: absolute;
    opacity: 0;
}

ul.tick-boxes button.active .tick-box::after {
    opacity: 1;
}

So now an ::after element always exists from the beginning, any rendering costs are paid for at the outset, so my thought was, now when I click this button, the tick is already there, we are just giving it an opacity of 1. Didn't fix the delay.

Now, I try removing the "filterOptionsChanged" event trigger entirely. This makes my whole sorting plugin stop working, but I don't care at this point, because I want to understand what is causing the problem. When I do remove that event trigger, the buttons and css render snappy. No more problems.

I have a vague thought that, ok, if a click event can be snappy without that event trigger, I need a way of seperating the two. First add the active class, then trigger "filterOptionsChanged". I think, ok, jQuery Deferreds. Here is that code:

$("ul.tick-boxes button").click(function(event) {
    event.preventDefault();
    var showLoading = jQuery.Deferred();
    $("ul.product-grid").addClass("loading");
    $(this).toggleClass("active");
    showLoading.resolve();
    $.when(showLoading).done(function() {
        $theElement.trigger("filterOptionsChanged");
    });
});

So showLoading is a blank Deferred, I then add my classes for the tick boxes to show, then I resolve the deferred. Now I say, when showLoading is done, then do the whole product-grid manipulation. Don't do these at the same time, javascript, wait for one to finish, then do the other. Still no avail. Any ideas?

Colin Brogan
  • 728
  • 10
  • 26
  • [`window.requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame) may be helpful – Paul S. Jul 30 '15 at 21:53
  • I can't tell what your code is doing, but I don't see any HTTP requests going in. There's no reason for it to be that slow, but without seeing it I can't offer any suggestions. The work to build up even a large fragment of a page should take far less time (if done properly) on a modern computer. There are lots of expensive things that should be avoided, however. – Pointy Jul 31 '15 at 12:14
  • It's hard to tell because it's minified, but I bet the real problems like in that "big fast shopify filter". Seems not to be so fast. – Pointy Jul 31 '15 at 12:30
  • Pointy, the following is, basically, what bigFastShopifyFilter is doing: At the time of page load, the plugin makes 7 ajax requests at a time to a json endpoint on the shopify server to retrieve all the products. If any of those pages are not empty, it makes another 7 calls, and repeats until an empty page is found. However, all those calls happen at the beginning of the plugin initiation, and by the time you are clicking each filter option, which my post is about, there are no ajax calls, only document fragment builds from a javascript object. – Colin Brogan Jul 31 '15 at 17:35

2 Answers2

1

According to this, all function calls in JavaScript block the UI until they complete; I'd wager that this includes the original function call to start the click event. A cheap solution might be to replace the trigger with something like:

setTimeout(function() { $theElement.trigger("filterOptionsChanged"); }, 200);

Which will hopefully delay the trigger long enough for the browser to repaint the UI (you could/should add a little loading icon in the original function, then remove it in the timeout). You could also take a look at web workers, which look like they're pretty much threads.

Community
  • 1
  • 1
Alex Van Liew
  • 1,339
  • 9
  • 15
  • Alex, spot on. This is revolutionary for me, I didn't know js freezes the UI while it does intensive work. I see now Web Workers are the way to go, but they aren't supported below ie10, and I am working on a project that has many users on old computers. Nonetheless your quick fix makes the ticking button snappy and initiates the load message I want while the more intensive stuff does it's work. Works great. Maybe I can come up with a browser check and route the code to a Web Worker for a later, better solution. – Colin Brogan Jul 31 '15 at 20:55
  • If you do a browser check, make sure to check *capabilities*, not *versions*. `typeof(Worker) === "undefined"` and whatnot. Cheers! – Alex Van Liew Jul 31 '15 at 21:04
0

Try this,

CSS:

ul.tick-boxes button.active .tick-box::after {
    content: "\e603";
    font-family: "custom-icons";
    color: #51425d;
    top: 2px;
    left: 2px;
    font-size: 0.75rem;
    position: absolute;
}

JS:

    $("ul.tick-boxes button").click(function(event) {
        event.preventDefault();
        $("ul.product-grid").addClass("loading");
        $(this).toggleClass("active").fadeIn(10, function () {
            $theElement.trigger("filterOptionsChanged");
        });
    });

Using callback to delay the triggering. See if this work for you.