3
\$\begingroup\$

I have a website which only needs to support IE11. It is a single page application, which has about 200 table rows and each table row has 5 child rows.

There is a pulsing function that updates the table as records come in. Table rows are skipped over if no update comes in.

However, when receiving large updates (which should only occasionally happening), the application will hang as it slowly processes the javascript. I've tried to limit the JavaScript as much as possible, but still have a long running function.

I am a backend developer by nature, and was wondering if anyone had any tips to help support large table Ajax updates for IE since IE so poorly handles JS.

function writeTableLines(tempRows){
    /* This Function takes care of updating the text and coloring of
    required dynamic fields. 
    All other values are not dynamically written.
    */
    for( i in tempRows){
   //i is the computer name
        tempValues = tempRows[i];
        // For Row
        selector = "[id='"+i+"']";

        // Network Name
        network_selector = "[id='"+i+"_network']";
        $(network_selector).text(tempValues['network']);

        if (tempValues['network_color']){
            $(network_selector).addClass(tempValues['network_color']);
            $(selector).find('.name').addClass(tempValues['network_color']);
        }else{
            $(network_selector).removeClass('warning');
             $(selector).find('.name').removeClass('warning');
        }

        // Boot Time
        boot_selector = "[id='"+i+"_boot']";
        $(boot_selector).text(tempValues['boot']);
        if (tempValues['boot_color']){
            $(boot_selector).addClass(tempValues['boot_color']);
            $(selector).find('.name').addClass(tempValues['boot_color'])
        }else{
            $(boot_selector).removeClass('issue');
            $(selector).find('.name').removeClass('issue');
        }

        // Last Checked In Timestamp
        check_in_selector = "[id='"+i+"_checked_in']";
        $(check_in_selector).text(tempValues['checked_in']);
        if (tempValues['service_unresponsive']){
            $(check_in_selector).addClass('redline');
            $(selector).find('.name').addClass('redline');
        }else{
            $(check_in_selector).removeClass('redline');
            $(selector).find('.name').removeClass('redline');
        }

        util_selector = $(selector).find('td.util').find('a');
        $(util_selector).text(tempValues['util'])
         if (tempValues['util_class']){
            $(util_selector).addClass(tempValues['util_class']);
        }else{
            $(util_selector).removeClass('redline warning');
        }

        workgroup_selector = $(selector).find('td.workgroup');

        if (($.trim(tempValues['workgroup'])) != $.trim($(workgroup_selector).text())){
            if ((tempValues['workgroup'] != selected) && (selected != 'All')){
                $(workgroup_selector).addClass('warning');
            }else{
                $(workgroup_selector).removeClass('warning');
            }
        }
        $(workgroup_selector).text(tempValues['workgroup'])
        toggle_links(i, tempRows[i]);
        $('#connectionGrid').trigger('updateAll', [false]);
    }
}

This function iterates over only received data. For each row item that was received, update the text of the cell, and add coloring as necessary.

I'm thinking I might just be screwed since its IE, but am open to all suggestions and ideas.

Image of the rows - child rows only available when expanded, but still need updates.

enter image description here

\$\endgroup\$
  • \$\begingroup\$ XSLT (declarative) may be a more elegant approach to this task than the spaghetti bowl of branch statements that you'd need to write in most imperative languages including JavaScript. CSS selectors are already a great help, but I think you can do better. \$\endgroup\$ – David Foerster Feb 2 '16 at 20:25
  • \$\begingroup\$ I am not familar with XSLT. Unfortunately :/ I will research it however at your advice \$\endgroup\$ – Busturdust Feb 2 '16 at 20:27
  • 1
    \$\begingroup\$ XSLT only works, if the responses to the asynchronous update requests contain XML (or (X)HTML) data, obviously. If you're sending JSON, YAML or CSV, my suggestion is moot. \$\endgroup\$ – David Foerster Feb 2 '16 at 20:43
  • 1
    \$\begingroup\$ Searching a large DOM for elements is a real performance killer. If the "network", "boot" and "check_in" elements could be found relative to the corresponding "selector" element, similar to the "util" and "workgroup" elements, that alone would be a significant improvement. \$\endgroup\$ – Roamer-1888 Feb 3 '16 at 2:40
  • \$\begingroup\$ You might consider wrapping each block (each row plus its subrows) in <tbody>...</tbody>, with considerable advantages to element selection and styling. Hard to give concrete advice without seeing the HTML. \$\endgroup\$ – Roamer-1888 Feb 3 '16 at 3:00
4
\$\begingroup\$

Searching a whole, large DOM for elements is a real performance killer. When possible, always try to search a fragment, or traverse the DOM relative to a known element.

With a little rearrangement of the HTML, "network", "boot" and "check_in" elements can be found within the corresponding "selector" element, similar to the way "util" and "workgroup" elements are currently found. This alone should give a significant performance boost.

HTML

  • There's a missing </tr> somewhere.
  • Move <tbody> and </tbody> inside the loop/if lines to give one tbody per computer block. (Hopefully tbodys will not mess up tablesorter).
  • Move id="{{computer.name}}" into the <tbody> tag.
  • Give a class name to elements that need to be addressed :

For example, change :

<td class="info" colspan="1" id="{{computer.name}}_network">{{ computer.active_drive.name }}</td>

to:

<td class="info network" colspan="1" id="{{computer.name}}_network">{{ computer.active_drive.name }}</td>
  • Then, if they are not required elsewhere, purge all IDs in the repeated block.

Javascript

The javascript can now be written to exploit the tbody wrappers.

function writeTableLines(tempRows) {
    /* 
     This Function takes care of updating the text and coloring of required dynamic fields. 
     All other values are not dynamically written.
    */
    var tempValues, $tbody, $name, $network, $boot, $check_in, $util, $workgroup,
        $connectionGrid = $('#connectionGrid');

    // Avoid creating so many strings in the loop by defining class names and selectors here.
    // This is more a memory consideration than speed.
    var clss = {
        'warning': 'warning',
        'issue': 'issue',
        'redline': 'redline'
        'redlineWarning': 'redline warning',
    };
    var selectors = {
        'network': '.network',
        'boot': '.boot',
        'check_in': '.checked_in',
        'name': '.name',
        'util': 'td.util a',
        'workgroup': 'td.workgroup'
    };

    for(i in tempRows) {
        tempValues = tempRows[i];

        // Find the container
        $tbody = $('#' + i); // This is the only element in each block that needs an ID.

        if($tbody.length == 0) return; // avoid unnecessary work if element is not found

        // Now find elements by class, within the container
        $network = $tbody.find(selectors.network);
        $boot = $tbody.find(selectors.boot);
        $check_in = $tbody.find(selectors.checked_in);
        $name = $tbody.find(selectors.name);
        $util = $tbody.find(selectors.util);
        $workgroup = $tbody.find(selectors.workgroup);

        // In all the code below, address tempValues properties with dot.notation, not associative['notation']

        $network.text(tempValues.network);
        if (tempValues.network_color) {
            $network.addClass(tempValues.network_color);
            $name.addClass(tempValues.network_color);
        } else {
            $network.removeClass(clss.warning);
            $name.removeClass(clss.warning);
        }

        $boot.text(tempValues.boot);
        if (tempValues.boot_color) {
            $boot.addClass(tempValues.boot_color);
            $name.addClass(tempValues.boot_color);
        } else {
            $boot.removeClass(clss.issue);
            $name.removeClass(clss.issue);
        }

        $check_in.text(tempValues.checked_in);
        if (tempValues.service_unresponsive) {
            $check_in.addClass(clss.redline);
            $name.addClass(clss.redline);
        } else {
            $check_in.removeClass(clss.redline);
            $name.removeClass(clss.redline);
        }

        $util.text(tempValues.util);
        if (tempValues.util_class) {
            $util.addClass(tempValues.util_class);
        } else {
            $util.removeClass(clss.redlineWarning);
        }

        if (($.trim(tempValues.workgroup)) != $.trim($workgroup.text())) {
            if (tempValues.workgroup != selected && selected != 'All') {
                $workgroup.addClass(clss.warning);
            } else {
                $workgroup.removeClass(clss.warning);
            }
        }
        $workgroup.text(tempValues.workgroup);

        toggle_links(i, tempValues);
        $connectionGrid.trigger('updateAll', [false]);
    }
}

Some of the code looks to be as little dodgy. For example, .addClass(tempValues.network_color) ... .removeClass('warning') means that any added class that is not warning will never be removed (unless by some other code). Contrast with .addClass('redline') ... .removeClass('redline'), which is guaranteed to add/remove the same class.

Aside: With the tbodys in place, you could consider styling them with eg a border that will expand/contract as the details are shown/hidden.

If performance is still poor, you'll need to investigate deeper to discover what's taking time. Though I'm not an expert driver, Chrome debug tools are very good for diagnosis.

Edit

Back to a single <tbody> but with class="info network", class="info boot", class="info check_in", in place, try selecting as follows :

// Find the parent row
var $tr = $('#' + i); // A parent row

if($tr.length == 0) return; // avoid unnecessary work if element is not found

var $childRows = $tr.nextUntil(".parent"); // the parent's child rows

$network = $childRows.find(selectors.network);
$boot = $childRows.find(selectors.boot);
$check_in = $childRows.find(selectors.checked_in);

$name = $tr.find(selectors.name);
$util = $tr.find(selectors.util);
$workgroup = $tr.find(selectors.workgroup);

This will be slightly less efficient than finding elements within tbody containers but still better than finding by ID - and Tablesorter will still work.

\$\endgroup\$
  • \$\begingroup\$ Thanks for the detailed answer Roamer, I will digest and dig in and hopefully see some improvement. Off the initial look it appears to break tablesorter zebra widget as well as tablesorter sorting, but tablesorter filtering is working. I will provide a more detailed update after digesting \$\endgroup\$ – Busturdust Feb 3 '16 at 14:28
  • \$\begingroup\$ $connectionGrid.trigger('updateAll', [false]); appears to be the bottle neck. This is a tablesorter update call. But I think I need to put the tbody back outside the loop so that I can maintain sorting on the whole table \$\endgroup\$ – Busturdust Feb 3 '16 at 14:56
  • \$\begingroup\$ Understand - I contributed to Tablesorter a few years ago and had a feeling it was limited to single tbody (same as DataTables I think). But all is not lost, the objective of multiple tbodies was to make for efficient selections, which is still possible by traversing the DOM slightly differently. \$\endgroup\$ – Roamer-1888 Feb 3 '16 at 15:46
  • \$\begingroup\$ According to the Tablesorter docs, updateAll "allows you to update the cache with data from both the thead and tbody of the table", while update "only updates the cache from the tbody". As your writeTableLines() only addresses the tbody, try update and see if it performs better. \$\endgroup\$ – Roamer-1888 Feb 3 '16 at 15:46
  • \$\begingroup\$ UGH! I had the updateAll inside the loop. Such a fool! It now responds to an initial overhead of half a second, + ~.2 seconds per computer... I would still like to learn more about proper DOM navigation using closest or find with the supplied html for edification and best practice \$\endgroup\$ – Busturdust Feb 3 '16 at 15:54
2
\$\begingroup\$
  1. Declare new local variables in local scope instead of global scope with var or let:

    var selector = ...
    let selector = ...
    for (var i in tempRows) { ... }
    

    Otherwise they'll become member variables of the global scope which is typically the window object.

  2. There's a CSS shorthand to select elements by attribute id: prefix the attribute value with a # sign:

    selector = "#" + i;
    

    instead of

    selector = "[id='"+i+"']";
    

    If you want to avoid potential escaping issues, you can also use:

    selector = document.getElementById(i.toString())
    
  3. Reuse jQuery objects that refer to the same (set of) element(s):

    selector = $("#"+i);
    [...]
    selector.find(".name").addClass();
    [...]
    selector.find(".name").removeClass();
    

    or better yet:

    [...]
    selector_name = selector.find(".name");
    [...]
    selector_name.addClass();
    [...]
    selector_name.removeClass();
    

    The same goes for the variables network_selector, boot_selector, etc.

\$\endgroup\$
  • \$\begingroup\$ Thanks David, these are some useful tips and I believe they will help streamline the function. Allow me some time to restructure and I will accept. Thanks for the help \$\endgroup\$ – Busturdust Feb 2 '16 at 20:21
  • \$\begingroup\$ can you please elaborate why selector = document.getElementById(i.toString()) is better than selector = "[id='"+i+"']"; I would think this adds an extra DOM access? \$\endgroup\$ – Busturdust Feb 2 '16 at 20:28
  • \$\begingroup\$ Look at what the code is doing later with the value of selector: $("[id='foobar']"), $("#foobar") and require exactly one DOM look-up just like $(document.getElementById("foobar")), but the latter is more robust in the defense against programming mistakes or attacks, if the ID string is not a literal or even comes from user input. \$\endgroup\$ – David Foerster Feb 2 '16 at 20:38
  • 1
    \$\begingroup\$ 2 seconds per item is way too long. I would expect to be seeing a few tens of milliseconds per item. \$\endgroup\$ – Roamer-1888 Feb 3 '16 at 3:06
  • 1
    \$\begingroup\$ Are you sure, you're only measuring the DOM manipulation time for a single item? Or do you maybe also measure the time to fetch it over the network and/or rendering the new DOM? Please provide your updated code in a follow-up question referring to this question and stating what you have tried already and what and potentially how you're measuring exactly. \$\endgroup\$ – David Foerster Feb 3 '16 at 6:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.