Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is jQuery's .each drastically slower in Safari than Firefox/Chrome?

This question is not looking for a solution to a specific problem but trying to understand why Safari is inefficient in this instance. When I talk about drastically slower, the code runs in Firefox and Chrome in under 1 second while Safari takes in the range of 30-90 seconds. It's likely already a documented issue but I don't know why.


The situation is that I have an HTML table that is fairly large. It's 1,000-1,500 rows by 40 columns wide. The structure looks something like:

<table id="myTablePlayers" class="tablesorter table table-striped table-bordered table-hover" style="overflow: visible">
    <thead>
        <tr>
          <th>...</th>
          <th>...</th>
          <th>...</th>
          <th>...</th>
          ...
          <th>...</th>
        </tr>
    </thead>
    <tbody>
        <tr class="playerData">
            <td>...</td>
            <td>...</td>
            <td>...</td>
            <td>...</td>
            ...
            <td>...</td>
        </tr>
        ...
    </tbody>
</table>

A number of form fields allow users to select and enter information that help filter out rows. The jQuery looks like:

function autoRank() {
    // auto number
    rank = 0;
    $("#myTablePlayers .playerData").each(function() {
        if ($(this).css("display") != "none") {
            rank++;
            $(this).find('td').eq(colRank).text(rank);
        }
    });
}

function filterTable() {
    // Need some error checking on input not number
    minGP = $("#mingp").val()
    teams = $("#teamFilter").val()
    position = $("#position").val()
    age = $("#age").val()

    $("#myTablePlayers .playerData").show();

    $("#myTablePlayers .playerData").each(function() {
        toHide = false;

        if (teams != "") {
            if ( !$(this).find('td').eq(colTeam).text().toUpperCase().includes(teams.toUpperCase())) {
                toHide = true;
            }
        }

        if ( Number($(this).find('td').eq(colGP).text()) < minGP ) {
            toHide = true;
        }

        if (position != "") {
            if (position == "D") {
                if ($(this).find('td').eq(colPos).text().indexOf("D") == -1) {
                    toHide = true;
                }
            } else if (position == "F") {
                if ($(this).find('td').eq(colPos).text().indexOf("D") != -1) {
                    toHide = true;
                }
            } else if ( $(this).find('td').eq(colPos).text() != position) {
                toHide = true;
            }
        }

        if (age != "") {
            column = Number($(this).find('td').eq(colAge).text())
            age = Number(age)
            if (  column < age || column >= age+1  ) {
                toHide = true;
            }
        }

        if (toHide == true) {
            $(this).hide();
        }

    });

    autoRank();
}

$("#teamFilter").on('change', filterTable);

$("#mingp").on('change', filterTable);

$("#position").on('change', filterTable);

$("#age").on('change', filterTable);

When I start pruning down the code, the offending code that takes a long time to run, regardless of what's inside the loop, seems to be $("#myTablePlayers .playerData").each(function() {...

I solved the issue by re-writing the code in vanilla JS but that doesn't answer why this code is so inefficient in just one browser.

like image 474
Reily Bourne Avatar asked Dec 01 '16 16:12

Reily Bourne


1 Answers

Checking the DOM status by making inquiries via .css() can be extremely expensive. Instead of hiding/revealing elements with .hide() and .show(), add/remove a class. In your CSS:

.hidden { display: none; }

Then your .each() loop can just check for that class:

$("#myTablePlayers .playerData").each(function() {
    if (!$(this).hasClass("hidden")) {
        rank++;
        $(this).find('td').eq(colRank).text(rank);
    }
});

To hide something, you'd just add that class, and to show it you'd remove it:

    if (toHide) {
        $(this).addClass("hidden");
    }

And to show:

$("#myTablePlayers .playerData").removeClass("hidden");

Now, all those .find() and .text() calls are going to be expensive too. It would probably be worthwhile to initialize the table by going over it once and creating data properties on each <tr> to effectively cache the interesting values from each row. Lookups via jQuery's .data() will be significantly cheaper than looking by selector in the DOM (though modern DOM implementations are pretty fast).

like image 157
Pointy Avatar answered Nov 06 '22 23:11

Pointy