My $.each loop is quite slow. Any methods of making it faster? - javascript

I have a chrome extension that needs to look through every <p></p> on a webpage. My chrome extension looks at the p text and checks if the text is located in an array. The problem is, the array has over 3,000 elements and I'd quite like to get that up to 12,000 or more if possible.
At the current rate, that just isn't feasible because it takes the webpage about 4 extra seconds to load the page. My chrome extension runs at the end of the document so the user can technically browse the site, it just takes 4 seconds for everything to load.
Here is my code:
$.each(arrayObj, function(key, value) {
$("p").highlight(key, {caseSensitive: true, className: 'highlight-882312', wordsOnly:true });
});
$('.highlight-882312').each(function() {
var currentKey = $(this).text();
console.log(currentKey);
//do something else here that doesn't really
//apply to the question as it only runs when the user hovers over the class.
});
and then the array looks pretty simple like this:
var arrayObj = {
"keyword1": 'keyword_1_site',
"keyword2": 'keyword_2_site',
... etc
... 3,000+ more lines ...
...
}
I'm assuming $.each isn't the most efficient, and as I said, 4 seconds to load is quite a bit. Is there anything I could do to make this more efficient? Will I ever be able to hit 12,000 lines in the array?
Thanks :)

You are running global selectors for each element in array. That's quite a lot.
So at least I would suggest to replace this:
$.each(arrayObj, function(key, value) {
$("p").highlight(key, ...);
});
by this:
var $all_p = $("p");
$.each(arrayObj, function(key, value) {
$all_p.highlight(key, ...);
});

Here's a couple of ideas :
$("p") and the .highlight() options are static, so define once, use many.
apply .highlight() only to those words that actually exist on the page.
var options = {
caseSensitive: true,
className: 'highlight-882312',
wordsOnly:true
},
arrayObj_ = {},
$p = $("p");
//Populate arrayObj_ with true for every word that exists on the page
$p.each(function() {
$(this).text().split(' ').forEach(function(word) {
arrayObj_[word] = true;
});
});
//Now loop through arrayObj_ ,
// which is hopefully much smaller than arrayObj,
// and highlight those words that are represted as keys in arrayObj.
$.each(arrayObj_, function(key, value) {
if(arrayObj[key]) {
$p.highlight(key, options);
}
});
No idea whether this will be faster or slower. You will need to run tests.
Edit ...
Even better, discover words and highlight in a single pass.
var options = {
caseSensitive: true,
className: 'highlight-882312',
wordsOnly:true
},
wordsFound = {},
$p = $("p");
$p.text().split(/\b/).forEach(function(word) {
if(!wordsFound[word]) {
wordsFound[word] = true;
if(arrayObj[word]) {
$p.highlight(word, options);
}
}
});

Try this:
var p = $("p");
$.each(arrayObj, function(key, value) {
p.highlight(key, {caseSensitive: true, className: 'highlight-882312', wordsOnly:true });
});
$('.highlight-882312').each(function(idx, element) {
var currentKey = element.text();
console.log(currentKey);
// other stuff
});
Generally speaking, you want to avoid $() selectors inside loops as they're time consuming. Notice that each()'s callback receives the element that's being iterated and you don't need to select it again.
This solution might not completely solve your problem but at least is the most efficient way to do it that I can think of without knowing more details.

Refactor them to javascript for loop instead of jQuery's each loops. https://stackoverflow.com/a/14808512/1547497
i.e.
var elems = $('.highlight-882312');
for (var i = 0; i < elems.length; ++i){
}

Related

jQuery - Selectively storing table data to 2d array

I came up with the problem of storing html table data to a 2D array. Instead of using nested loops, I was looking for jquery functions that would allow me to do the same. I found this method which seems to do the job.
var tRows = $('tr').map(function() {
return [$(this).find('td').map(function() {
return $(this).text()
}).get()]
}).get()
console.log(tRows)
I'd like to know, how I can use a condition, in the given code, to check if the first td of a tris empty or not (or some other condition in general) and thus not add that row to the final array.
EDIT:
Reading the comments and building upon this fiddle, I now have
var tRows = $('tr').map(function() {
var arr = $(this).find('td').map(function() {
return $(this).text()
}).get();
if (arr[0].length) return[arr];
}).get()
This works somewhat, but I was hoping to check the condition in the inner map and return a NULL instead of the entire array. From the discussion here, it seems that it is not possible to abort the inner map call midway, and it will return the entire array. Seems like loops might be better in the end for this job, or did I miss something?
Assuming the code you posted is working for you...
If you want the .text of the first td not empty of each row, the use of a combination of CSS selectors may be the solution.
:first-child
:not
:empty
EDIT
Okay... I improved it a little.
Seem like there can ba some spaces in an empty td...
So I used a regex to test it.
Here are the new things I used:
:first
.test() JavaScript method
.trim() JavaScript method
And the CodePen where I tryed it.
$(document).ready(function(){
console.clear();
console.log("loading...");
var tRows = $('tr').map(function() {
return [$(this).find('td:first').map(function() {
var pattern = /^\s+$/;
if($(this).text()!="" && !pattern.test($(this).text())){
return $(this).text().trim();
}
}).get()]
}).get()
console.log(tRows)
});
This return: [["blah1],[],[blah7]], in the CodePen...
If you do not want the empty one in the middle:
Second CodePen
$(document).ready(function(){
console.clear();
console.log("loading...");
var tRows = $('tr').map(function() {
var arr = [$(this).find('td:first').map(function() {
var pattern = /^\s+$/;
if($(this).text()!="" && !pattern.test($(this).text())){
return $(this).text().trim();
}
}).get()];
if(arr[0]!=""){
return arr;
}
}).get()
console.log(tRows)
});
This return: [["blah1],[blah7]]
2nd EDIT
This can also be achieved this way.
(it implies only one loop and the code is more simple to read).
See 3rd CodePen
$(document).ready(function(){
console.clear();
console.log("loading...");
var arr=[];
var counter=0;
$('tr').each(function() {
var thisTDtext = [$(this).find("td").first().text().trim()];
if(thisTDtext!=""){
arr[counter] = thisTDtext;
counter++;
}
});
console.log(JSON.stringify(arr));
});
This also return: [["blah1],[blah7]]

Looping through jQuery objects in a collection without initializing a new jQuery object for each iteration

I find myself doing this all the time:
$myElements.each( function(index, currentHtmltmlElement) {
var $currentJqueryElement = $(currentHtmltmlElement);
// Working with $currentJqueryElement
});
Initializing a new jQuery object in each iteration is a huge performance penalty.
So i thought of doing this instead (credit also goes to decx#freenode):
for (var index = 0; index < $myElements.length; index++) {
var $currentJqueryElement = $myElements.eq(i);
// Working with $currentJqueryElement
}
But i fixed up a JSPerf test and this snippet's performance turned out to be identical to that of the first snippet! :(
Both are freaking slow! For really large collections, you could even notice the page freeze.
So i wonder what is the fast way of iterating through jQuery objects in a collection.
The way should also be as convenient to use as possible. This:
$items.each$item( function( $item, index ) { /* Working with $item directly. */ });
would be way better than the ancient for (var i=0... ineptitude.
UPD 2014-05-27
Here's an example. Imagine that you have a number of jQuery UI Sliders:
<div class="my-slider"></div>
<div class="my-slider"></div>
<div class="my-slider"></div>
$sliders = $('.my-slider').slider();
Now you would like to log to console the value of each slider. You have to do:
$sliders.each( function(index, htmlSlider) {
$current_slider = $(htmlSlider); // This step is a huge performance penalty
console.log(
$current_slider.slider('value')
);
});
So the task is to get rid of the performance penalty.
You can't simply do $sliders.slider('value') because this will output the value only of the first slider in the collection.
You can't revert to vanilla JS inside the loop because you can't access a jQuery widget without a jQuery object.
So far, all these approaches...
$sliders.each( function(index, htmlSlider) { htmlSlider });
$sliders.each( function() { this });
for (var i = 0; i < $sliders.length; i++) { $sliders.eq(i); }
$.each, $.makeArray, $.map, etc
...work with $sliders's underlying array of HTML elements and require a time-costy $( ) initialization to access jQuery features.
Try
$.each($p, function(i, v) {
test = $(v)
})
for + eq() : 702
each() : 722
this : 718
$.each() : 757 ("fastest")
http://jsperf.com/jquery-for-eq-vs-each/5
Edit
You can't split a jQuery collection into parts directly, you can only create new ones out of separate HTML elements. -lolmaus - Andrey
Mikhaylov
Try (at console, this page)
$("div"); // jQuery `collection` ? approx. `222` members, changes
// `split` 1st `101` members of jquery `obj`, or `collection`
var split_jq_obj_1 = $("div").slice(0, 111);
// `split` 2nd `101` members of jquery `obj`, or `collection`
var split_jq_obj_2 = $("div").slice(-111);
// Note `prevObject` and `selector` properties of `jQuery` `object`
// `split_jq_obj_1` and `split_jq_obj_2`
// `prevObject: e.fn.e.init[222]` , `selector: "div.slice(0,111)"`,
// `prevObject: e.fn.e.init[222]`, `selector: "div.slice(-111)"`)
// check results
console.log($("div"), split_jq_obj_1, split_jq_obj_2);
console.log($("div").length, split_jq_obj_1.length, split_jq_obj_2.length);
other possible approaches
// `jquery` `object`, or `objects`
var arr = $.makeArray($.map($("div"), function(value, index) {
return ( index < 111 ? [$(value)] : null )
}), $.map($("div"), function(value, index) {
return ( index >= 111 ? [$(value)] : null )
}));
console.log(arr); // `collection` of `jQuery` `objects`
// iterate `jquery` object`, or `objects`,
// find `length` of `class` `item-content`
$.each($(arr), function(index, value) {
console.log($(value).find(".item-content").length) // `29` (see `iteration` at `console`)
});
// check results
$(".item-content").length; // `29`
Edit 2014-05-28
Try this (pattern)
$(function () {
var sliders = $('.sliiider').slider({
min: 0,
max: 100
});
$('html').click(function () {
$.when(sliders)
.then(function (data) {
for (var i = 0; i < data.length; i++) {
// `$()` jQuery `wrapper` _not_ utilized,
// not certain about quantifying `cost`, if any,
// of utilizing jquery's `deferred` `object`s
// test by sliding several `sliders`,
// then `click` `document`, or `html`
console.log(data.eq(i).slider("value"))
};
})
});
});
jsfiddle http://jsfiddle.net/guest271314/8gVee/
fwiw, this pattern also appears to work, again without utilizing jquery's init wrapper $()
$(function () {
var sliders = $('.sliiider').slider({
min: 0,
max: 100
});
$('html').click(function () {
$.when(sliders)
.then(function (data) {
// Note, 2nd parameter to `.each()` not utilized, here
data.each(function (i) {
// `$()` jQuery `init` `wrapper` _not_ utilized,
// within `.each()` `loop`,
// same result as utilizing `for` loop,
// "performance", or "fastest" not tested as yet
console.log(data.eq(i).slider("value"));
})
});
});
})
jsfiddle http://jsfiddle.net/guest271314/G944X/
Just use this keyword? Seems to be fastest, not to mention (objectively) easiest to understand.
$myElements.each( function() {
var $currentJqueryElement = $(this);
});
http://jsperf.com/jquery-for-eq-vs-each/3
If performance is a problem, why not skip out on jQuery and use pure JS?
Answer to update
If you want to get the slider values (which seem to be just the amount of css style "left" , you could do something such as:
var elems = document.getElementsByClassName('my-slider');
for (var i = 0; i < elems.length; i++){
console.log(elems[i].style.left); // logs : xx.xx%
}
Would it be faster? Maybe. Will the approach fit in all use cases? Most likely not, but for that specific question it should work.
I'll be happy if you prove me wrong, but i came to a conclusion that a jQuery collection is a uniform object that cannot be iterated through and only the HTML elements it represents are stored as separate entities that can be enumerated. You can't split a jQuery collection into parts directly, you can only create new ones out of separate HTML elements.
Thus, revert to native JS features inside the loop whenever possible to improve performance.

jQuery/Javascript: More complex Searching and Replacing in an HTML document

I have done some stuff in jQuery and Javascript before, but unfortunately I am no expert. I couldn't find any hints on how to accomplish my task with using as few resources as possible. You guys can probably help me out:
Here is what I want to do:
I want to find (using regex) all BB-code-like elements on a page that look something like this:
[ndex here=parameter randomdata]
I want to then replace each of them with the contents I receive from an ajax-call that looks like this:
call.php?ndex=here=parameter randomdata
or whatever parameters I pick up from within the according [ndex]-tag.
Here is my solution/thought-process so far:
$(document).ready(function() {
var pattern = /\[ndex\s+(.*?)\]/mg;
var documentText = $(document.body).text();
var matches = documentText.match(pattern);
$('*').each(function () {
var searchText = this;
if ($(searchText).children().length == 0) {
$.each(matches, function() {
//here is where I would need to check for a match and make a call
}
});
}
});
});
I don't really know how to work from here. My sketch seems really clunky and complicated. There must be a more elegant and straight-forward solution.
Thank you guys so much for your help. :)
i would do something like this :
function ndex_treat(n) {
// If element is ELEMENT_NODE
if(n.nodeType==1)
{
// If element node has child, we pass them to function ndex_treat
if(n.hasChildNodes())
for(var i= 0; i<n.childNodes.length; i++)
ndex_treat(n.childNodes[i]);
}
// If element is TEXT_NODE we replace [ndex ...]
else if(n.nodeType==3)
{
var matches, elemNdex, elemText;
// While there is one
while(/\[ndex\s+(.*?)\]/m.test(n.nodeValue))
{
// Taking what's before (matches[1]), the "attribute" (matches[2]) and what's after (matches[3])
matches= n.nodeValue.match(/^([\s\S]*?)\[ndex\s+(.*?)\]([\s\S]*)$/m)
// Creating a node <span class="ndex-to-replace" title="..."></span> and inserting it before current text node element
elemNdex= document.createElement("span");
elemNdex.className= 'ndex-to-replace';
elemNdex.title= matches[2];
n.parentNode.insertBefore(elemNdex, n);
// If there was text before [ndex ...] we add it as a node before
if(matches[1]!=="")
{
elemText = document.createTextNode(matches[1]);
elemNdex.parentNode.insertBefore(elemText, elemNdex);
}
// We replace content of current node with what was after [ndex ...]
n.nodeValue=matches[3];
}
}
}
$(function(){
// Get the elements we want to scan ( being sharper would be better )
$('body').each(function(){
// Passing them to function ndex_treat
ndex_treat(this);
});
// Make the ajax calls
$('.ndex-to-replace').each(function(){
// Don't know if necessary
var current= this;
$.get('call.php?ndex='+encodeURIComponent(this.title),function(data){
$(current).replaceWith(data);
});
});
});
i replaced by node rather than by jquery because i find it rather bad to work on textNode with jquery. if you don't care and would rather do the barbarian way, you could replace all the first part with simply :
$(function(){
// Get the elements we want to scan ( being sharper would be better )
$('body').each(function(){
// With no " in argument of [ndex ...]
$(this).html( $(this).html().replace(/\[ndex\s+([^"]*?)\]/mg,'<span class="ndex-to-replace" title="$1"></span>') );
// With no ' in argument of [ndex ...]
//$(this).html( $(this).html().replace(/\[ndex\s+([^']*?)\]/mg,'<span class="ndex-to-replace" title='$1'></span>') );
});
// Make the ajax calls
/* ... */
});
My advice is to keep the ajax calls to a minimum. Do the search in the first place, an on another round replace every object with the corresponding data.
$(document).ready(function() {
var pattern = /\[ndex\s+(.*?)\]/mg;
var documentText = $(document.body).text();
var matches = documentText.match(pattern);
$.ajax({
url:'call.php',
method:'POST',
data: matches,
success: function(data){
//replace every matched element with the corresponding data
});
});
You'll have to modify your call.php to take this into account though, but you are saving lots of calls to the server and thus time

Need help with setting multiple array values to null in a loop - javascript

I have been working on creating a custom script to help manage a secret questions form for a login page. I am trying to make all the seperate select lists dynamic, in that if a user selects a question in one, it will no longer be an option in the rest, and so on. Anyways, the problem I am having is when I try to set the variables in the other lists to null. I am currently working with only 3 lists, so I look at one list, and find/delete matches in the other 2 lists. Here is my loop for deleting any matches.
for(i=0; i<array1.length; i++) {
if(array2[i].value == txtbox1.value) {
document.questions.questions2.options[i] = null
}
if(array3[i].value == txtbox1.value) {
document.questions.questions3.options[i] = null
}
}
This works fine if both the matches are located at the same value/position in the array. But if one match is at array1[1] and the other match is at array3[7] for example, then only the first match gets deleted and not the second. Is there something I am missing? Any help is appreciated. Thanks!
I don't see too many choices here, considering that the position in each array can vary.
Do it in separate loops, unless of course you repeat values in both arrays and share the same position
EDTI I figured out a simple solution, it may work, create a function. How about a function wich recives an array as parameter.
Something like this:
function finder(var array[], var valueToFound, var question) {
for (i=0; i<array.lenght; i++) {
if (array[i].value == valueToFound) {
switch (question) {
case 1: document.questions.questions1.options[i] = null;
break;
}
return;
}
}
}
I think i make my point, perhaps it can take you in the right direction
My bet is that the code isn't getting to array3[7] because either it doesn't exist or that array2 is too short and you're getting a JavaScript exception that's stopping the code from doing the check. Is it possible that array2 and array3 are shorter than array1?
It is more code, but I would do it like this:
var selectedvalue == txtbox1.value;
for(i=0; i<array2.length; i++) { // iterate over the length of array2, not array1
if(array2[i].value == selectedvalue) {
document.questions.questions2.options[i] = null;
break; // found it, move on
}
}
for(i=0; i<array3.length; i++) {
if(array3[i].value == selectedvalue) {
document.questions.questions3.options[i] = null;
break; // you're done
}
}

How can I optimize my routine to build HTML tiles from an array?

As my last question was closed for being "too vague" - here it is again, with better wording.
I have a "grid" of li's that are loaded dynamically (through JavaScript/jQuery), the Array isn't huge but seems to take forever loading.
So, SO people - my question is:
Am I being stupid or is this code taking longer than it should to execute?
Live demo: http://jsfiddle.net/PrPvM/
(very slow, may appear to hang your browser)
Full code (download): http://www.mediafire.com/?xvd9tz07h2u644t
Snippet (from the actual array loop):
var gridContainer = $('#container');
var gridArray = [
2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
2,2,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,2,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,2,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
];
function loadMap() {
var i = 0;
while (i <= gridArray.length) {
var gridHTML = $(gridContainer).html();
$(gridContainer).html(gridHTML+'<li class="node"></li>');
i++;
}
$('li.node').each(function() {
$(gridArray).each(function (i, val) {
if (val == '0') { gridTile = 'grass.jpg' };
if (val == '1') { gridTile = 'mud.jpg' };
if (val == '2') { gridTile = 'sand.gif' };
$($('ul#container :nth-child('+i+')'))
.css({ 'background-image': 'url(img/tiles/'+gridTile });
});
});
}
The loop where you set the background images is the real problem. Look at it: you're looping through all the <li> elements that you just got finished building from the "grid". Then, inside that loop — that is, for each <li> element — you go through the entire "grid" array and reset the background. Each node will end up being set to the exact same thing: the background corresponding to the last thing in the array over and over again to the exact same background.
The way you build the HTML is also very inefficient. You should loop through the grid and build up a string array with an <li> element in each array slot. Actually, now that I think of it, you really should be doing the first and second loops at the same time.
function loadMap() {
var html = [], bg = ['grass', 'mud', 'sand'];
for (var i = 0, len = gridArray.length; i < len; ++i) {
html.push("<li class='node " + bg[gridArray[i]] + "'></li>");
}
$(gridContainer).html(html.join(''));
}
Now you'll also need some CSS rules:
li.grass { background-image: url(grass.jpg); }
li.mud { background-image: url(mud.jpg); }
li.sand { background-image: url(sand.gif); }
It'd probably be farm more efficient to build up the complete HTML for the array and then assign it to the .html property of the container, rather than assigning each individual li:
var gridHTML = $(gridContainer).html();
while (i <= gridArray.length) {
gridHTML = gridHTML+'<li class="node"></li>';
i++;
}
$(gridContainer).html();
Next, why are you looping over both of these? The outer loop is probably completely unnecessary, because your inner loop already uses nth-child to select the proper node.
$('li.node').each(function() {
$(gridArray).each(function (i, val) {

Categories