1

I'm looping through each value in an array and, for each value, I'm looping through a jQuery list of values to find an exact match. When a match is found I reveal its li element. Everything in the array will always be present in the list. This all works OK. However in real life there are 7,000+ items in both the array and the jQuery list and my approach already runs slow because of repeated looping. I've been trying to find a way to speed things up, perhaps with maps, but with my limited knowledge I'm not able to get my head around it. Can anyone help? https://jsfiddle.net/bt9xncea/

<ul>
  <li>Paris</li>
  <li>London</li>
  <li>Puntarenas</li>
  <li>Lisbon</li>
  <li>Balochistan</li>
  <li>Bonn</li>
  <li>Helsinki</li>
  <li>Moscow</li>
  <li>San Jose</li>
  <li>Paris</li>
  <li>Madrid</li>
  <li>Rome</li>
</ul>




var CityList = ["San Jose", "Alajuela", "Caldera", "Puntarenas", "Sisak-Moslavina", "Dalmatia", "", "Thatta", "Sindh", "Punjab", "Islamabad", "Balochistan", "Karachi"];

$("ul li").hide()

var elementText = "";
$.each(CityList, function(index, City) {

  $('ul li').each(function(dropindex) {
    elementText = $("ul li:eq(" + dropindex + ")").text()

    if (elementText === City) {
      $("ul li:eq(" + dropindex + ")").show();
      return false;
    }
  });

});
Silverburch
  • 457
  • 2
  • 12
  • 28

3 Answers3

1

You don't need 2 loops, you can achieve the same result by using a single loop and includes() like shown below:

var CityList = ["San Jose", "Alajuela", "Caldera", "Puntarenas", "Sisak-Moslavina", "Dalmatia", "", "Thatta", "Sindh", "Punjab", "Islamabad", "Balochistan", "Karachi"];

$("ul li").hide()

var elementText = "";

$('ul li').each(function(dropindex) {
    elementText = $("ul li:eq(" + dropindex + ")").text()

    if (CityList.includes(elementText)) {
      $("ul li:eq(" + dropindex + ")").show();
    }
  });
Thanthu
  • 4,399
  • 34
  • 43
  • Link to the fiddle for the same [https://jsfiddle.net/thanthu/sjqt3mcw/4/](https://jsfiddle.net/thanthu/sjqt3mcw/4/) – Thanthu Mar 16 '19 at 17:31
1

i see a few things that could be simplified. while you're looping the li objects, you can just use $(this) to reach to the current li element. no need to use other selectors.

this would, i think, perform better, because you're using much less selectors and function calls.

also, i would put $('ul li').each loop outside of $.each(CityList loop, because cityList loop is cheaper.

$("ul li").hide().each(function(dropindex) {
    var $cur = $(this);
    var elementText = $(this).text();

    $.each(CityList, function(index, City) {
      if (elementText === City) {
        $cur.show();
      }
    });
});

https://jsfiddle.net/a5fm4tcx/

however, just hide()ing 7000 elements would still be a bit slow.

keune
  • 5,779
  • 4
  • 34
  • 50
  • This is really interesting. Both codes have made a huge difference in speed. I'd have bet on the code with one loop less being fastest (really clever). However, during testing @keune code was a bit quicker! I'd love to understand which particular code change made the most difference - perhaps using fewer selectors? Fastest code gets the award, but many thanks to you both – Silverburch Mar 16 '19 at 18:24
1

Performance

Plain JavaScript and Minimal DOM Access

If performance is really an issue, you should know this:

  1. jQuery is slow compared to plain JavaScript.

  2. Any procedure that accesses the DOM is the slowest of all other aspects involving a webpage.

Having said that, we can come to certain conclusions:

  1. Use plain JavaScript don't even bother to load jQuery if you don't need it. Any procedures that hide or show nodes (elements, text...) should be done with CSS.

  2. Avoid accessing the DOM as much as possible.

#1 needs no explanation. However #2 is an odd requirement considering that the DOM is the main component (<ul> and a bazillion <li>). Going forward we will assume:

  1. The <ul> and the <li> are already on the page (hard coded HTML).

  2. The <ul> must be present after a search (meaning the final result must have the <ul> and <li> hidden or not).

If any of the above assumptions are wrong or not a requirement, then the procedure can be even faster.


Synopsis

In regards to item:

2. Avoid accessing the DOM as much as possible.

The following demo will:

  • clone the <ul> with .cloneNode()        [DOM operation #1]

  • append clone to a documentFragment [DOM operation #2]

    • A documentFragment is like a Document Object that's not part of the DOM. Whatever is done on the documentFragment never touches the DOM with the exception of removing or adding to or from the fragment to or from the DOM. Suffice it to say, using a documentFragment is a big boost in performance (it's what documentFragment was made for).
  • convert the <li> of the clone into an Array of htmlStrings.

  • convert the Array of htmlString into a Set.

  • iterate through the search list (ex. query Array [..."Karachi"]) and compare each city to the contents of the cities Set. For every match, assign the class .match.

  • convert cities Set into an Array then a String which is used as the htmlString to be parsed into the clone. The <ul> on the DOM is then .replaceWith() the clone. [DOM operation #3]

So all together we have only 3 DOM operations as opposed to an average procedure in jQuery which is more than what is seen on the surface.


Demo

Details commented in demo

// Query Array
const query = ["San Jose", "Alajuela", "Caldera", "Puntarenas", "Sisak-Moslavina", "Dalmatia", "", "Thatta", "Sindh", "Punjab", "Islamabad", "Balochistan", "Karachi"];
// Create a documentFragment 
const frag = document.createDocumentFragment();
// Reference list on DOM
const list = document.getElementById('cities');
// Clone the list
const dupe = list.cloneNode(true);
// Append clone to documentFragment
frag.appendChild(dupe);
/* 
Note: the objective of this process is to basically have
      an Array of Strings -- pattern: "<li>item</li>"
      There's probably a better way...
- Get the HTML of <li> within clone
- Convert HTML into a String
- .split() the String into an Array
- .join() the Array back into a String
- .split() the String back into an Array
*/
let text = (dupe.innerHTML).toString().split(/\n/).join('').split('  ');
// Convert the text Array into a Set
let cities = new Set(text);
/*
- .forEach() iteration of the query Array...
- if the cities Set .has() a city that query Array has...
- .delete() the city from cities Set...
- .add() the query Array item to cities Set
Note: the markup has changed -- .match class is assigned
*/
query.forEach(city => {
  if (cities.has(`<li>${city}</li>`)) {
    cities.delete(`<li>${city}</li>`);
    cities.add(`<li class='match'>${city}</li>`);
  }
});
/*
- Convert cities Set into an Array then into a String
- Change the clone's contents into the new String
*/
dupe.innerHTML = Array.from(cities).join('');
// Replace the list in DOM with the clone
list.replaceWith(frag);
/*
The list is initially hidden and only the matxhes are 
actually revealed. This is far better than `.hide()`
*/

#cities {
  visibility: hidden;
}

li {
  line-height: 0;
  height: 0;
  font-size: 0
}

#cities li.match {
  visibility: visible;
  line-height: 20px;
  height: 20px;
  font-size: 16px
}
<ul id='cities'>
  <li>Paris</li>
  <li>London</li>
  <li>Puntarenas</li>
  <li>Lisbon</li>
  <li>Balochistan</li>
  <li>Bonn</li>
  <li>Helsinki</li>
  <li>Moscow</li>
  <li>San Jose</li>
  <li>Paris</li>
  <li>Madrid</li>
  <li>Rome</li>
</ul>
Community
  • 1
  • 1
zer00ne
  • 41,936
  • 6
  • 41
  • 68
  • Excellent explanation here. I do see the logic. – Silverburch Mar 17 '19 at 08:31
  • @Silverburch I'm sure it's much faster than any jQuery solution. If there wasn't a `
      ` with `
    • ` and just an array of 7000 items, then just the matches were just generated it probably wouldn't even slowdown.
    – zer00ne Mar 17 '19 at 08:42