Why is this jquery so slow? - javascript

I'm using a jquery plugin which fixes the headers on a html table that I generate. Unfortunately the performance of the plugin is very slow and I've narrowed it down to the following code:
var $tbl = $(this);
var $tblhfixed = $tbl.find("thead");
$tblhfixed.find("th").each(function ()
$(this).css("width", $(this).width());
});
This is taking about 40 seconds on a table with 2,000 rows in ie. Does anyone know why it's so slow and more importantly how can I make this faster? I've tried many other plugins and this is the only one which works how I want it to. Thanks for any help

I guess you faced with the same problem that I had some time ago. It called a "Recalculate layout" or something.
Try to separate this script onto two loops, like this:
var $tbl = $(this);
var $tblhfixed = $tbl.find("thead");
var widths = [];
// 1.
$tblhfixed.find("th").each(function ()
widths.push($(this).width());
});
// 2.
$tblhfixed.find("th").each(function (index, element)
$(this).css("width", widths[index]);
});
First one will calculate all the widths. Second one will apply them to TH's
UPD:
You may increase performance by placing this code between 1. and 2.:
$tblhfixed.hide();
and show it again after 2.:
$tblhfixed.show();

the culprit is probably the .each.
The reason is that when you iterate using .eachinstead of a normal loop, you call a function for each iteration. a function call has a pretty big overhead in this case, since a new callstack has to be created for each iteration.
To make it faster change
$tblhfixed.find("th").each(function ()
$(this).css("width", $(this).width());
});
to
var elms = $tblhfixed.find("th");
for(var i=0, elm;elm = elms[i];i++) {
elm.css("width", elm.width());
}

First, you should use find() only when you need to pass through all nested nodes. Right here you can use children().
Second, each time $(this) creates new instance of jQuery object, while you can create it once:
var $this = $(this);
Each time $(this).width() is recalculated. Make sure that you need it to be recalculated. Or do:
var tableWidth = $this.width();
And third, according to #Martin Jespersen, each iteration the function object is created.
Also you don't need jQuery here at all. You can access DOM directly:
var tableWidth = ...; // get the table width the way you want
var thead = this.tHead;
var thRow = thead.rows[0];
for (var i = 0; i < thRow.length; ++i) {
thRow.cells[i].style.width = tableWidth + "px";
}

you should not repeat $(this) inside your function passed into .each(). wrapping an element has non-trivial overhead, which is not ok when you have 20k elements. you want to eliminate as much work as possible inside the .each() call, or eliminate it altogether.
Also, why query find() twice, when you can do this instead, which should give you the same results:
$ths = $('table thead th'); //or tableid, or something
$ths.css('width', $ths.width());

it appears that $.width() is 99 times slower than the native get(0).clientWidth, check out this test: http://jsperf.com/jq-width-vs-client-width

Related

jQuery selector - select new elements too after append/load within variable

I have:
$elements = $('.elements');
$element = $('.element');
function appendText(element){
element.append('<em> appended text</em>');
}
appendText($element);
$('button').on('click', function(){
$elements.append('<span class="element">Span Element Appended after load</span>');
appendText($element);
});
The appendText function, after button click, appends only to the initial element and that is due to JS cache I presume.
I know that I can do appendText($('element')); and the problem will be solved, but I don't want to change all my code now.
Is there any way to make jQuery consider this $element variable as not a cached element and look into the full DOM each time I call that variable?
Please find the jsfiddle if you wish to play or understand better: http://jsfiddle.net/adyz/733Xd/
If you add this:
$element = $('.element:last-child')
before
appendText($element);
I think will solve your problem
jsFindle here: http://jsfiddle.net/733Xd/5/.
Best regards!
That is an expensive thing to do. I would advise against it for performance reasons.
I did this pluggin in the beggining of last year https://github.com/fmsf/jQuery-obj-update
It doesn't trigger on every call, you have to request the update yourself:
$element.update();
The code is small enough to be pasted on the answer:
(function ( $ ) {
$.fn.update = function(){
var newElements = $(this.selector),i;
for(i=0;i<newElements.length;i++){
this[i] = newElements[i];
}
for(;i<this.length;i++){
this[i] = undefined;
}
this.length = newElements.length;
return this;
};
})(jQuery);
I think below one will solve your problem
appendText($element); //here you always referring to the node which was there initial.
http://jsfiddle.net/s9udJ/
Possible Solution will be
$(function(){
$elements = $('.elements');
$element = $('.element');
function appendText(element){
element.append('<em> appended text</em>');
}
appendText($element);
$('button').on('click', function(){
$elements.append('<span class="element">Span Element Appended after load</span>');
appendText($elements.find('span').last());
});
})
I don't think what you're asking is easily possible - when you call $element = $('.element'); you define a variable which equals to set of objects (well, one object). When calling appendText($element); you're operating on that object. It's not a cache - it's just how JS (and other programming languages) works.
The only solution I can see is to have a function that will update the variable, every time jquery calls one of its DOM manipulation methods, along the lines of this:
<div class='a'></div>
$(document).ready(function()
{
var element = $('.a');
$.fn.appendUpdate = function(elem)
{
// ugly because this is an object
// also - not really taking account of multiple objects that are added here
// just making an example
if ($(elem).is(this.selector))
{
this[this.length] = $(this).append(elem).get(0);
this.length++;
}
return this;
}
element.appendUpdate("<div class='a'></div>");
console.log(element);
});
Then you can use sub() to roll out your own version of append = the above. This way your variables would be up to date, and you wouldn't really need to change your code. I also need to say that I shudder about the thing I've written (please, please, don't use it).
Fiddle

jquery offset method doesn't always work / exist

Good morning and happy new year everyone!
I've run into a snag on something and need to figure out a solution or an alternative, and I don't know how to approach this. I actually hope it's something easy; meaning one of you all have dealt with this already.
The problem is that I'm doing rollovers that contain information. They're divs that get moved to the absolute location. Now I've tried this with jquery 1.6 - 1.9.1. Of course this has to work in multiple browsers.
What needs to happen is on rollover show a div, and when you rollout of that div, make it hide.
...
// .columnItem is class level and works
$(".columnItem").mouseleave(function() {
$(this).css("display", "none");
});
...
$(".column").mouseenter(function() {
var currentItem = $(this)[0]; // this is where the problem is
// hide all .columnItems
$(".columnItem").css("display", "none");
// i get this error: Object #<HTMLDivElement> has no method 'offset' (viewing in chrome console)
var offsetTop = currentItem.offset().top;
var columnInfoPanel = $("#column" + currentItem.innerText);
});
So the immediate thought of some would be don't use $(this)[0]. Instead, I should use $(this), and you are correct! Where the other problem comes into play is by removing the array index, currentItem.innerText is now undefined.
The only thing I can think of is I'll have to mix both, but it seems like there should be a way to use the selector and get both options.
What have you all done?
Thanks,
Kelly
Replace:
var currentItem = $(this)[0];
With:
var currentItem = $(this).eq(0);
This creates a new jQuery object containing only the first element, so offset will work.
Then you can use either currentItem[0].innerText or currentItem.text(), whichever you prefer.
Skip the [0] at the beginning as you are saying.
But then change the last line to:
var columnInfoPanel = $("#column" + currentItem[0].innerText);
De-referencing the jQuery selector gives you the DOM-object.
If you want to stick to pure jQuery, the .text() / .html() methods will give you the same functionality.

jQuery width() not working

I'm trying to get the width of the first div of the specific class "span4" on my Bootstrap site, but the script simply fails to execute the second line where I call width(). Here's what I have:
var span = $('div.span4').first();
spanWidth = span.width();
The strange part of this is that I have similar working code immediately after that works fine when I remove the above two lines and set spanWidth to a constant:
elements = $('a.backlink');
elements.each(function() {
var a = $(this);
if (a.width() > spanWidth) {
var aText = a.text();
var lastIndex = aText.lastIndexOf(' ');
var aTruncated = aText.substring(0, lastIndex);
a.text(aTruncated + '...');
}
});
Any idea what might be causing this? I've tried a lot of different ways to format those two lines differently, such as switching to an each() method, condensing to one line, and using [0] and get(0) instead of first().
Try to set the span's display to inline-block.
#SimonM's comment led me to try replacing my implied global spanWidth with an explicit global window.spanWidth, and now everything works. Thank you!

In jQuery, how to efficiently add lots of elements?

I currently have a sketch for a truthtable generator. While it works fine, it is rather slow. Each combination of boolean values I have added to a <table> using jQuery. For each value, a <td> element is created by jQuery and then added to the <table>. Moreover, I'm using jQuery UI for a nice looking buttonset instead of radio buttons.
In my posted code extract, table is an array containing each boolean combination. Perhaps my code is a little inscrutable but what it basically comes down to is that with 4 boolean variables (16 possibilities), 96 <td> elements are created with classes added and data attributes set. In the second part, three groups of three radio buttons are created and converted into a jQuery UI buttonset.
Using a timer I figured out that it takes approximately 0.4 seconds before everything is filled up. Not that big of a deal, but it is certainly noticeable and does not have a positive effect on the user as each time he enters a different boolean formula it takes half a second to load.
$table = $('#table');
$.each(table, function(k, v) {
$tr = $('<tr>').addClass('res').data('number', k);
$.each(v[0], function(k2, v2) {
$td = $('<td>').text(v2).addClass(v2 ? 'green notresult' : 'red notresult');
for(var i = 0; i < 4; i++) {
$td.data(i, i === k2);
}
$tr.append($td);
});
$tr.append($('<td>').addClass('spacing'));
$table.append(
$tr.append(
$('<td>').text(v[1]).addClass(v[1] ? 'green result' : 'red result')
)
);
});
// ... here is some code that's not slowing down
$radiobuttonsdiv = $('#radiobuttonsdiv');
for(var i = 0; i < 4; i++) {
var $radiobase = $('<input>').attr('type', 'radio')
.attr('name', 'a'+i)
.click(handleChange);
// .label() is a custom function of mine which adds a label to a radio button
var $radioboth = $radiobase.clone().val('both').label();
var $radiotrue = $radiobase.clone().val('true').label();
var $radiofalse = $radiobase.clone().val('false').label();
var $td1 = $('<td>').addClass('varname').html(i);
var $td2 = $('<td>').attr('id', i);
$td2.append($radioboth, $radiotrue, $radiofalse).buttonset();
var $tr = $('<tr>').append($td1, $td2);
$radiobuttonsdiv.append($tr);
}
My questions are:
How could table-filling using jQuery be optimized? Or is a table perhaps not the best solution in this scenario?
Is it perhaps possible to suspend drawing, since that might be slowing everything down?
Try to avoid using .append in a loop, especially if you're adding a lot of elements. This is always a performance killer.
A better option is to build up a string with the markup and do a single (or as few as possible) .append when your loop is finished.
I see that you're using .data, which makes things a bit more complicated, but another option is to use the metadata plugin to attach structured markup to existing tags.
To defer rendering, you could try creating a new table without adding it to the DOM like:
var myDisconnectedTable = $('<table></table>')
Then adding your rows to that table:
myDisconnectedTable.append(...)
Then append your table to the div you want it in:
$('#idOfMyDiv').append(myDisconnectedTable)
I don't know that it will help, but it's worth a shot.

get a total of jquery's .each()

I'm using jquery's .each() to iterate over a group of li's. I need a total of all the li's matched. Is the only way to create a count variable outside the .each() and increment this inside the .each()? It doesn't seem very elegant.
var count;
$('#accordion li').each(function() {
++count;
});
Two options:
$('#accordion li').size(); // the jQuery way
$('#accordion li').length; // the Javascript way, which jQuery uses
Since jQuery calls length under the hood, it's faster to use that instead of the size() call.
Well, I just saw this question, and you already accepted an answer, but I'm going to leave one anyway.
The point of the question seems to be concerned with incrementing a counter.
The fact is that jQuery's .each() method takes care of this for you. The first parameter for .each() is an incrementing counter, so you don't need to do it yourself.
$('#accordian li').each(function(index) {
// index has the count of the current iteration
console.log( index );
});
So as you can see, there is an elegant solution built in for you.
$('#accordion li').length;
Ok so the best way to do this is as follows: firstly map the wrapped set to a variable so you never have to do the sizzle dom lookup again:
var $myListItems = $('#accordian li');
Note: my preference is to put $ at the beginning of any vars that are a jQuery wrapped set, hence $myListItems as opposed to myListItems
Then set a var outside the each function that has the length of the wrapped set:
var myLength = $myListItems.length;
Note the lack of a $ here as the var is not a jQuery object.
Now run the each function with a variable for the index.
$myListItems.each(function(index){
// myLength has the length
// index has the current, 0 based index.
});
You now have what you've asked for in the OP with only one look up and so need to fumble in the each function with having to know the contents of the wrapped set just to know the length of it. the beauty of this over a counter is that on every iteration of the each you already know the length.
You can try this
const count = $('#accordion li').each(function() {
// Do something
}).length;
It works because .each() returns a jQuery object which has a length property.
One of the big issues, which I have not found a fix for yet, is if you have two sets of the same thing you will iterate through.
For instance, 2 table heads that you are iterating through number of column headers. If you use length on ( $item.children().find('thead tr th').each... ) for your each clause and you have 2 tables it will double the amount of your length that you are trying to walk through potentially [5 column headers in each table will return length of 10].
You could do both on a id name and have those different but if adding dynamically then this can become a headache.
The only way I found once inside the foreach is to use $(this):
$item.children().find('thead tr th').each(function(i, th) {
// I am working with a table but div could be used, or something else.
// I have 2 tables with 5 column headers, the below len return
var len = $(this).closest('table').find('thead tr th').length;
if (i < len) {
// ...your code here
}
});

Categories

Resources