I have some jquery that works fine, but I'd like to highly optimize it. Basically I'm
doing standard appending list items to unordered lists. Can anyone recommend the fastest
way to optimise the following code e.g. createDocumentFragment ?
for (key in data) {
li = $('<li><span class="item">' + data[key]["Name"] + '</span><img src=' + options.deleteIcon + ' alt="remove" class="delete"/></li>');
$('.item', li).data('ID', data[key]["Id"]);
$(list).append(li);
}
var sb = new Array();
for (key in data) {
sb.push('<li><span class="item" id="', data[key]['Id'], '">', data[key]["Name"], '</span><img src=', options.deleteIcon, ' alt="remove" class="delete"/></li>')}
$(list).append(sb.join(""));
I would suggest reducing the number of writes to the DOM to just one. By that, I mean storing the list into a temporary variable and then appending the entire list in a single operation. Also, instead of using .attr to set the ID of each element, you can use concatenation as you have used it to set the text of each LI.
var tmpList = '';
for (key in data) {
li = '<li><span class="item" id="' + data[key]['Id'] + '">' + data[key]["Name"] + '</span><img src=' + options.deleteIcon + ' alt="remove" class="delete"/></li>';
tmpList += li;
}
// if you are appending to an existing list, use append
// if you have just built one up from scratch, just use `.html`
$(list).append(tmpList);
I would recommend reading this:
Optimizing JavaScript For Execution Speed
From the article:
Unlike other programming languages,
JavaScript manipulates web pages
through a relatively sluggish API, the
DOM. Interacting with the DOM is
almost always more expensive than
straight computations. After choosing
the right algorithm and data structure
and refactoring, your next
consideration should be minimizing DOM
interaction and I/O operations.
For a start, you could make one large chunk of li elements and then append them in a single operation instead of appending them one at time.
Thanks everyone, the one point everyone seems to miss is that I'm associating the Id with the DOM element ... using the jquery.data method. This isn't to be confused the the ID attribute for the span element. Because of this I'm not sure how the concatenation would work as I understand that I need to have a reference to the DOM element to set the data on it. Is this correct?
Related
I use Javascript to dynamically create a lot of elements. Divs, images, spans, etc.
Here is an example piece of code that my JS would run:
infoCell.innerHTML = "Submitted by " + "<a href='/user/" + this.poster + "'><img src='" + this.poster_avatar_src + "' class='avatarimg'> <span style='color:blue'>" + this.poster + "</span> </a>in " + "<span style='color:blue; font-weight: 900;'><a href='/h/" + href + "'>" + this.topic + "</a></span>"
This was written early in my JS development, but now I realize that it can very quickly become very insecure, as almost all of the javascript variables being inserted into the HTML are written by the user with no limitations to character usage, etc.
How can I go through my javascript and change all of these so they still function, but without worrying about users inserting script into my site?
I am fine rewriting a lot but I would like to not do this again. I have about 90 innerHTML DOM modifications in my main JS file (typescript).
you could try to use a combination of document.createElement and HTMLElement.append
an example for the first <a> tag:
function makeElem (tagname, properties) {
let elem = document.createElement(tagname);
for (const key in properties) {
elem[key] = properties[key];
}
return elem;
}
infoCell.append("Submitted by ");
let a = makeElem("a", {href:'/user/"' + this.poster + '"'});
a.replaceChildren(makeElem("img", {'src':this.poster_avatar_src, 'className':'avatarimg'}), makeElem("span", {'textContent':this.poster,'style':'color:blue;'}));
infoCell.append(a);
this might not be the easiest but it should work, the reason for the "makeElem" function is purely convenience and you don't necessarily need it
There are a few approaches.
One is to use a sanitizer to translate all of the dynamic values into properly escaped strings before interpolation - but you'd have to be sure you get it right, otherwise there could still be problems.
Another way is to construct the element structure, then insert the dynamic strings at the appropriate points, eg:
const cell = document.createElement('div');
cell.innerHTML = `
Person info
<div class="name"></div>
<div class="age"></div>
`;
cell.querySelector('.name').textContent = name; // where name is dynamic
cell.querySelector('.age').textContent = age; // where age is dynamic
But this can be tedious if you have a lot of dynamic values to insert.
A third way (and one that I'd recommend for serious applications) is to use a framework to handle it for you. For example, in React, the above "cell" could be made like:
const Cell = ({ name, age }) => (
<div>
Person info
<div class="name">{name}</div>
<div class="age">{age}</div>
</div>
);
It takes some learning and getting used to, but once you get going it's a lot easier to read and write than other approaches.
I have a block of jquery that builds a form with more than 40 element using a loop to .append() div elements that load .ajax() json.
9 out of 10 times the form renders as expected. However randomly some of the elements will suffer from two types of errors
the dom element will be created but not displayed (does not show in source)(on deeper investigation, it seems the elements are always generated, just wrong parent (2))
the dom element is placed as a child to the wrong parent.
for example below, id3 should be attached to delta[14] but is instead generated at beta[3] OR id3 should be at gamma[6] but does not display at all
the format of build is
hard code
<div id="alpha">
<div id="beta"></div>
<div id="gamma"></div>
<div id="delta"></div>
</div>
doAppendStuff(beta, ajaxUrletc1);
doAppendStuff(beta, ajaxUrletc2);
...
doAppendStuff(gamma, ajaxUrletc10);
doAppendStuff(gamma, ajaxUrletc11);
...
doAppendStuff(delta, ajaxUrletc20);
doAppendStuff(delta, ajaxUrletc21);
dynamic
function randId(baseID) {
return baseID+"_"+Math.round(new Date().getTime() + (Math.random() * 555));
}
var id1= randId("myIdOne");
var id2= randId("myIdTwo");
var id3= randId("myIdThree");
function doAppendStuff(elemId, ajaxUrletc){
$('#' + elemId).append(
'<div id="' + id1 + '" >' +
'<div id="' + id2 + '">' +
'<select id="' + id3 + '"></select>' +
'</div>' +
'</div>'
);
... // log id1, id2,id3
... //do .ajax stuff + callback on id3
I have added callbacks to each loop to ensure that the .append is fired and no errors are generated complaining that the element does not exist.
Running a trace I can see the dynamic id for each element is being generated.
The code itself works as expected as the other 9/10 times it renders as expected.
notes
The random errors apply to different elements each time. no particular logic on what element fails.
I have separated the ajax calls from the dom element creation so there should be no bottleneck on the element creation
All ajax calls are initiated as expected in the correct order. Some take longer but the other elements generate as expected without waiting. All data is successfully returned.
Is there any know issues with generating multiple dom elements by calling the same function repeatedly OR is there a listener I could add to ensure the element is correctly generated in the desired position before proceeding to the next call via a callback.
UPDATE
After adding logging of the id1,id2,id3, the logs confirm that the correct dynamic ids are being assigned. It seems however that either the var in memory is being replaced with a previous value or the js engine is placing in the wrong generated position due to timing.
UPDATE 2
After some more debugging, we changed the random string and upped the number from
return baseID+"_"+Math.round(new Date().getTime() + (Math.random() * 555));
to
return baseID+"_"+Math.round(new Date().getTime() + (Math.random() * 99999));
and the problem has not reoccurred. So it looks like it could be either a random ID collision with the same string being generated twice or somehow reused when the function is reinitialised. The interesting thing is the ID's are not sequential, it will often skip several rows before reusing the same ID.
So we have cured the problem, but still do not understand what caused the issue, any thoughts are welcome.
If you want a unique value, then we're talking about GUID's.
Create GUID / UUID in JavaScript?
However, if you would have placed an underscore or other value between the time and random number, I'd suspect you'd have seen fewer collisions.
time + random = some future period in time.
Imagine getting a random 100 and then 100 ms later getting a random 0.
"abc1506110581013_100" != "abc1506110581113_0"
while
"abc1506110581113" == "abc1506110581113"
I know that the empty method removes all children in the DOM element.
In this example however, why does removing the empty method result in duplicate entries:
and putting it in results in a normal page:
var renderNotesList = function()
{
var dummyNotesCount = 10, note, i;
var view = $(notesListSelector);
view.empty();
var ul = $("<ul id =\"notes-list\" data-role=\"listview\"></ul>").appendTo(view);
for (i=0; i<dummyNotesCount; i++)
{
$("<li>"+ "" + "<div>Note title " + i + "</div>" + "<div class=\"list-item-narrative\">Note Narrative " + i + "</div>" + "" + "</li>").appendTo(ul);
}
ul.listview();
};
I don't know why empty() doesn't work but I found this
... so until this is sorted everyone should just use:
el.children().remove(); instead of el.empty();
( jQuery.empty() does not destroy UI widgets, whereas jQuery.remove() does (using UI 1.8.4) )
Without seeing how your JavaScript is being used in your page, I suspect that you must be calling the renderNotesList() function twice and thus generating to unordered lists.
When you use the .empty() method, you are removing the first ul list, so you only see one instance. Without the call to .empty(), you retain both.
However, I can't say where or how this is happening in you web page without seeing more, but at least you now have some idea of what to look for.
Demo Fiddle
I built a demo using your JavaScript, but I was sort of guessing as to how you are using it.
Fiddle: http://jsfiddle.net/audetwebdesign/UVymE/
Footnote
It occurred to me that the function ul.listview() may actually be appending a second copy of the ul to the DOM. You need to check the code or post it for further review.
I'm iterating over a large number of dom elements in Javascript, and I'd like to add/remove classes as appropriate.
What's the most efficient add/remove class operation I can use?
Small class strings: JSPerf.
Bigger class strings: JSPerf.
Bigger class strings, repeated class names: JSPerf.
Remove
The fastest reliable way (small/medium size):
var clss = 'classToRemove';
elem.className = (' '+elem.className+' ').split(' ' + clss + ' ').join(' ');
If you know for sure that the string does not contain multiple occurrences of the same class name, you can better use the string.replace method (any size):
var clss = 'classToRemove';
elem.className = (' '+elem.className+' ').replace(' ' + clss + ' ', ' ');
The other alternatives:
Using a RegExp in combination with replace - Slow in all cases
Using indexOf to find the position, and use string concatenation (substring) to remove the class (repeat this in a loop to remove all possible duplicates).
Slowest for large strings with repetition, not slow neither fast in other cases.
Add (without doubt):
var clss = 'classToAdd';
element.className += ' ' + clss;
The bottleneck is in retrieving the elements, not adding/removing class names.
el.className = "another_class"; //Simple
Depending on the structure of your DOM, you can optimize element retrieval by practicing the following:
Getting a particular wrapper element via getElementById
Getting related sub-elements with getElementsByTagName or childNodes (depending which is more appropriate for the situation)
If referenced recursively, saving references to the accessed elements
Typically, a framework will retrieve elements much more slowly than a vanilla Javacript method, but if you save references by dumping accessed elements in a local array, the difference is near negligible.
Edit: getElementsByClassName is another way to retrieve elements, but it isn't as well supported as the above methods yet.
Again, if we could see your code, we could give a more direct answer.
Where supported, element.classList is around 1000 times faster than Rob_W's best suggestions (no ie8/9 support, although a fallback from Mozilla exists).
http://jsperf.com/best-way-to-remove-class/6
Rob_W's tests were flawed because they were not using the DOM-- just really testing the performance of various string replacements (see tests).
Without jQuery:
function addClass(domElement, class) {
var classes = domElement.getAttribute("className");
if (!classes.indexOf(class))
classes += " " + class;
domElement.setAttribute("className", classes);
}
function removeClass(domElement, class) {
domElement.setAttribute("className", domElement.getAttribute("className").replace(class, ""));
}
Coming to jQuery from a functional background, I am fond (perhaps unreasonably so) of elegant chains of functions. I often find myself dealing with arrays of elements, such as those that may result from $.map, and my ability to manipulate these arrays in the DOM seems quite limited. Here's some sample code that runs through the results of a Google search, rendering the result titles:
var newResultsDiv = $('<div id="results" />');
$.each(searcher.results, function() {
newResultsDiv.append('<p>' + this.title);
});
$("#searchresults").append(newResultsDiv);
I find this excessively verbose. Ideally, I would do something along these lines instead:
$.map(searcher.results, function(elem) {
return $('<p>' + elem.title);
}).wrapAll('<div id="results" />').appendTo('#searchresults');
I've tried this out, along with several variants using different forms of append and wrap. They all appear to be incapable of handling the plain-old-Javascript array that jQuery.map spits out. They're designed to work with jQuery's own set collection. At least, that's my guess, as messing around with these functions in the Firebug console seems to confirm the problem.
I am hoping that someone here has the wisdom to show me an elegant way to do what I'm trying to do. Is there one?
Using the $.map method you presented, you could return the actual DOM element instead of a jQuery object. This is done by grabbing the [0] index item in the jQuery object. Then wrap the entire $.map with $().
This works because jQuery will accept an array of DOM elements.
Your <p> creation was a little off. I changed it to pass an object literal to set the text. Otherwise, you would need to concatenate the ending tag as well.
Finally, you would need to traverse up to the wrapper #results you created using .parent().
$($.map(searcher.results, function(elem) {
return $('<p>',{text:elem.title})[0];
})).wrapAll('<div id="results" />').parent().appendTo('#searchresults');
EDIT: IF you don't mind the look of it, you could do this as well:
$('<div id="results" />').append(
$.map(searcher.results, function(elem) {
return $('<p>',{text:elem.title})[0];
})).appendTo('#searchresults');
Hm...not sure if this will work, but give it a try:
$("<div id='results'>" + $.map(searcher.results, function(elem) {
return '<p>' + elem.title + '</p>';
}).join("") + "</div>").appendTo('#searchresults');
OK, just tested it here:
http://jsfiddle.net/xxxST/1/
And it seems to work. I should mention, however, that while this is perhaps not very "verbose" in terms of number of lines, it's a bit opaque with regard to its clarity. It's a jQuery function result, joined together to a string, inside a jQuery wrapper with an appendTo function running on it. I think it would be much more readable if you were to do something like this:
var resultString = $.map(searcher.results, function(elem) {
return '<p>' + elem.title + '</p>';
}).join("");
$('#searchresults').append("<div id='results'>" + resultString + "</div>");
But that's just my opinion.