I'm looking to address a performance issue I'm having with a very large DOM. In essence, this a word-processing style app inside the browser using contenteditable divs.
Suppose I have a structure like this:
<div class="header">...</div>
<div class="actor">...</div>
<div class="director">...</div>
<div class="producer">...</div>
<div class="writer">...</div>
<div class="executive">...</div>
<div class="studio">...</div>
<div class="footer">...</div>
I then have some code which ends up returning (for example):
<div class="writer">...</div>
as a jQuery object. I then need to retrieve all of this object's surrounding divs between header and footer as a selection and then further filter this list using a class e.g. 'actor'.
Currently, I have the following code, which works correctly:
// Find header
var header_object = object.prevUntil(".header").last().prev();
// Select all objects between header and footer, and then filter
var object_list = header_object.nextUntil(".footer", ".actor");
// Iterate through object_list
object_list.each(function()
{
// Run additional code on the objects
});
The only problem is that due to the app being a word processor of sorts, the DOM structure is often very large (e.g. over 5000 elements) and executing this code locks up the browser for an unacceptable amount of time (over 10 - 30 seconds).
As such, I'm looking for a way to customize the code I have to make it more efficient / improve performance.
I should also point out that the HTML structure above is not (header - 5000 elements - footer), rather it is 200 x (header - elements - footer). As such, each traversal operation is only maybe 25 elements from header to footer, but it has to run many times.
Any suggestions? Many thanks!
You could enhance performance by not using jQuery, and creating your own functions that are more specific to your use case.
function getClosest(el, klass, dir) {
while (el && (!el.classList.contains(klass))) {
el = el[dir ? 'previousElementSibling' : 'nextElementSibling'];
}
return el;
}
function getbetween(from, to, filterKlass) {
var list = [];
while(from && to && from !== to) {
if ((from = from.nextElementSibling) !== to) {
filterKlass ? (from.classList.contains(filterKlass) ? list.push(from) : Infinity) : list.push(from);
}
}
return list;
}
var object = $('.writer');
var element = object.get(0);
var header_object = getClosest(element, 'header', true);
var footer_object = getClosest(element, 'footer', false);
var object_list = getbetween(header_object, footer_object, 'actor');
object_list.forEach(function(element) {
console.log(element);
});
FIDDLE
Traversing the next element sibling directly, and checking for classes, should be much faster than using nextUntil
Related
I have a cshtml view file with the following code:
<ul id="ul1">
<li id="ul1li1"></li>
<li id="ul1li2"></li>
<li id="ul1li3"></li>
</ul>
And an action "x" which returns me html code with which is several divs one after the other.
something like :
<div class="xdiv"></div>
<div class="xdiv"></div>
<div class="xdiv"></div>
<div class="xdiv"></div>
<div class="xdiv"></div>
Id like to use javascript function in order to put the divs I get from the action "X", into the lis in a circular ascending order.
I couldn't find the right solution, and so far my javascript function is something like this :
function fillLis()
{
var i = 0;
divs = #Url.Action("X");
for(ind in divs){
div = divs[ind];
i = i%3 + 1;
var currentli = document.getElementById('ul1li'+i);
currentli.innterHTML += div.innerHTML;
}
};
Because of some bad html coding, I have to do it that way.
This isn't doing what you think:
divs = #Url.Action("X");
It's just going to resolve to a string (and a syntax error), such as:
divs = /Home/X;
What you want to do is make an AJAX request to that action. (Note: Since you're using ASP.NET, I'm assuming that jQuery is an option.) Something like this:
$.get('#Url.Action("X")', function (data) {
// update your UI
});
In that callback function, data will contain the response from the server. Which, in this case, should be the HTML. At this point it looks like you want to loop over the div elements. So you can probably wrap it in a jQuery object and loop over that. Something like this:
$.get('#Url.Action("X")', function (data) {
var divs = $('div', data);
// at this point "divs" is a list of div HTML elements in the response
});
Your existing code should work on the divs array, perhaps with some minor adjustments through debugging. But essentially that's how you'd get the HTML elements you're looking for from the server in order to use those elements in your client-side code.
Something like that?
Array.prototype.forEach.call(#Url.Action("X"), function(xdiv, i) {
document.getElementById("ul1li"+i).appendChild(xdiv);
});
If not then please update your question with more detailed informations about what you're trying to do.
Some quick notes about your code:
function fillLis()
{
var i = 0;
divs = #Url.Action("X");
for(ind in divs){
div = divs[ind];
// This will always be equal to 1
i = i%3 + 1;
// You say you want to "put the divs [...] into the list", but it seems like you're trying to transfer only the content of your divs
var currentli = document.getElementById('ul1li'+i);
currentli.innterHTML += div.innerHTML;
}
};
I know that I can display/hide content whether the browser is IE or not or even the version of IE. I was wondering if I can use other expressions too such as
<!--[if 1 == 0]-->
This should be hidden
<!--[endif]-->
The reason behind this is that I'm sending auto generated E-Mails and for me it would be easier to insert such comments in the template E-Mail instead of creating multiple templates.
if you have a template system, then make this in your template. Anyway when you render the template you calculate the condition, but instead of printing "0 == 1" or "0 == 0", use the template's ability to print or not to print the following paragraph
I know this would look like a long answer but I just wanted to divide the code into small functions each does its own job -kind of-, first select each element with a class name of hasComment in an array using querySelectorAll then pass this array to updateHTML() function, loop through its element and call returnComment() function for each item in the array.
The returnComment() function first call hasComment() function on the element passed to it, and using .replace() to get the exact string. Function hasComment() loop through the child nodes of the element and if the nodeType of the child node is 8 it then it's a comment, we return the text between the comment <!-- and -->.
This .replace(/\[|\]/ig, ''); omits the brackets to get value of either show or hide which according to it we "hide" or "show" the child .contentDiv div.
JS Fiddle
var commentDivs = document.querySelectorAll('.hasComment');
updateHTML(commentDivs);
function updateHTML(arr) {
for (var i = 0; i < arr.length; i++) {
var childDiv = arr[i].querySelector('.contentDiv'),
showIt = returnComment(arr[i]);
if (showIt == 'show') {
childDiv.style.display = 'block';
console.log('Div-' + (i + 1) + ': shown');
} else if (showIt == 'hide') {
childDiv.style.display = 'none';
console.log('Div-' + (i + 1) + ': hidden');
}
}
}
function returnComment(element) {
var comment = hasComment(element);
comment = comment.replace(/\[|\]/ig, '');
return comment;
}
function hasComment(element) {
for (var i = 0; i < element.childNodes.length; i++) {
if (element.childNodes[i].nodeType == 8) {
return element.childNodes[i].data;
}
}
}
<div class="hasComment">
<!--[hide]-->
<div class="contentDiv">Div -1: This should be hidden</div>
</div>
<hr>
<div class="hasComment">
<!--[hide]-->
<div class="contentDiv">Div -2: Again, This should be hidden</div>
</div>
<hr>
<div class="hasComment">
<!--[show]-->
<div class="contentDiv">Div -3: But this should be shown</div>
</div>
----------
Notes:
Wrapping the all contents of each .hasComment elements making controlling the content easier.
The above solution only work on the very top level of .hasComment element children, so if you have other comments inside .contentDiv these comments won't be affected.Demo Fiddle
You could probably use [if 1==0] for "templating" like in your code then use eval() or more complex regex to check upon it, but IMHO I think using show and hide look easier and mostly less bugs as you this over and over through your document.
More details about nodeType:
https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType
https://developer.mozilla.org/fr/docs/Web/API/Node/nodeType
http://www.w3schools.com/xml/dom_nodetype.asp
Since you are developing for email clients, no this isn't possible. You need to figure out how different clients can be targeted. Then set the display property via CSS to whatever is affected.
Ideally, your emails shouldn't need any kind of crazy logic like this. It is a smell that your email is bad. Not to mention, anything you put in the email itself is viewable, all someone needs to do is turn off HTML rendering or view the source.
I have following problem. Let's say I have DOM like this.
<div class="results">
<div class="result">
<div class="title">Aaa</div>
</div>
<div class="result filtered-out">
<div class="title">Aab</div>
</div>
<div class="result">
<div class="title">Aac</div>
</div>
<div class="result">
<div class="title">Aad</div>
</div>
<div class="result">
<div class="title">Aae</div>
</div>
</div>
and an input field like this
<input type="text" id="search">
And now I try to filter the results with a simple function defined by this
var searchBox = $(this);
searchBox.keyup(function(){
var searchBox = $(this);
var items = $(".results .result:not(.filtered-out)");
items.each(function(){
var title = $(this).find(".title").html();
if(title.toLowerCase().indexOf(searchBox.val().toLowerCase())!== -1)
$(this).show();
else
$(this).hide();
});
});
So the problem is that the list of results is quite long something between 100 and 200 elements and whenever I type something into the search input the code executes very long. Maybe around 2-3 seconds. Is there any other approach to solve this "lag"? Thank you for any advices!
EDIT Maybe something like delayed script execution or asynchronous script execution (like in ajax)?
It's generally not a good idea to use the DOM as a datasource, it's not meant for it and is therefore slow. Personally I would recommend using a small MVVM library or something similar so you don't have to manually manage the DOM yourself.
I've used Vue.js below, but you could just as well use any similar solution. Keeping your data in the code will allow you to operate on it a lot faster since you don't have to re-request it all the time and you avoid doing a lot of work for modifications. All operations below are done on 1000 objects:
var items = [];
for (var i = 0; i < 1000; i++) {
items.push({
title: 'Item #' + i
});
}
var v = new Vue({
el: '#list',
data: {
items: items,
input: ""
},
computed: {
filteredItems: function() {
var value = ("" || this.input).trim().toLowerCase();
if (!value.length) return this.items;
return this.items.filter(function(item) {
return item.title.toLowerCase().indexOf(value) !== -1;
});
}
}
});
ol {
list-style: none;
padding: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/0.12.16/vue.min.js"></script>
<div id="list">
<input placeholder="Search" v-model="input" />
<ol>
<li v-repeat="filteredItems">{{title}}</li>
</ol>
</div>
When searching the dom through many elements it is recommended to use javascript as opposed to jQuery if speed is what you are after. jQuery has it's purpose but for large amounts of dom searching using javascripts getElementById or querySelector / querySelectorAll is going to be much much faster. If you check this jsPerf example you can see that the jQuery selector operates roughly 94% slower than the comparable getElementById.
You should try using some logging to figure out which part is taking the longest. If you find that it's the items selector (with the psuedo-not), you could try to optimize that, however I don't see anything about the filtered-out class so I'm not sure exactly what that does.
Here's some simple optimizations though:
var searchBox = $(this);
searchBox.keyup(function(){
var $searchBox = $(this);
var searchBoxVal = $searchBox.val().toLowerCase();
var items = $(".results .result:not(.filtered-out)");
items.each(function(){
var $item = $(this);
var title = $item.find(".title").html();
if (title.toLowerCase().indexOf(searchBoxVal) !== -1)
$item.show();
else
$item.hide();
});
});
My guess the lag is because you are performing the search based on the DOM elements, and at the same time manipulating them with hiding/ showing.
I suppose the DOM is populated from some data source? If so it'll be better to perform the search/ filter from that data source, then use the filtered data set to populate the DOM again. (And even if you don't have the data source at first, you can build one by reading the original DOM)
I read the question Scroll to a particular element w/ jQuery and in the question, the original HTML code already has ids to the paragraph elements. However, in my case, I generate the element (list element) dynamically and create ID on runtime. How would I scroll to that particular element with or without jQuery?
To give more detail about my problem:
I am creating a phonegap project to get the list of contacts in the iPhone and display a scrolling list (I use the iscroll plugin) in a div. I categorize the first names A-E, F-J, K-O, P-T, U-Z and group them. If the user touches F-J on the side (as you find in iPhone contacts app), the div should scroll to the first contact that belongs to group F-J..
<div id ="tablediv">
<div id="scroller"></div>
</div>
<div id="sorter">
<span id="gr1">A-E</span>
<span id="gr2">F-J</span>
</div>
Javascript:
var g1 = ['a','b','c','d','e']; //contact's first name starting with these chars
var g2 = ['f','g','h','i','j']; //contact's first name starting with these chars
var idg1=null, idg2=null; // first id of the contact which was in g1, g2
//Array is an array of objects
//example: array = [ {'fname':'x','lname':'y','number':'123'},{..},{..}];
function generateTable(array) {
gpDiv = document.getElementById("scroller");
pDiv = document.createElement("ul");
pDiv.id = "thelist";
for(var i=0;i<array.length;i++) {
cDiv = document.createElement("li");
cDiv.id = 'cd'+i; //id created dynamically
cDiv.textContent = array[i].fname+"\u000a"+array[i].lname;
var ch0 = array[i].fname[0].toLowerCase();
if($.inArray(ch0,g1)!=-1 && idg1==null) {
idg1 = cDiv.id;
document.getElementById('gr1').addEventListener('click',function(){goToG1(idg1);},false);
}
if($.inArray(ch0,g2)!=-1 && idg2==null) {
idg2 = cDiv.id;
document.getElementById('gr2').addEventListener('click',function(){goToG2(idg2);},false);
}
pDiv.appendChild(cDiv);
}
gpDiv.appendChild(pDiv);
}
function goToG1(id) {
$('#tablediv').scrollTop($('#'+id).offset().top);
}
function goToG2(id) {
$('#tablediv').scrollTop($('#'+id).offset().top);
}
The above code doesn't work, as I think since the ids are allotted at runtime, I am not able to scroll to that particular element. Please help
Hmmm, All I needed to do was this.
function goToG1(id) {
document.getElementById(id).scrollIntoView();
}
It appears to me that the ids still work even though they are allotted at run time.
You code worked more or less - you were however using code:
$("#tablediv").scrollTop(...)
Instead of
$(document).scrollTop(...)
Other than that it works - see here: http://jsbin.com/umatuj/2/edit
I am using Safalra's javascript to create a collapsible list. The script works across several browsers with no problem. However, when I apply the javascript to my own list, it fails to act as expected when I use IE (I'm using 7 at the moment). It simply writes the list, without the expand and contract images.
I copied the Safalra's javascript precisely, so I assume the error must be in my own list. This is how I generated my list:
<body onLoad="makeCollapsible(document.getElementById('libguides'));">
<ul id="libguides">
<script type="text/javascript" src="http://api.libguides.com/api_subjects.php?iid=54&more=false&format=js&guides=true&break=li"></script>
</ul>
(Yes, I do close the body tag eventually.) When I run this in IE, it tells me that line 48 is causing the problem, which appears to be:
node.onclick=createToggleFunction(node,list);
Here's the entire function:
function makeCollapsible(listElement){
// removed list item bullets and the sapce they occupy
listElement.style.listStyle='none';
listElement.style.marginLeft='0';
listElement.style.paddingLeft='0';
// loop over all child elements of the list
var child=listElement.firstChild;
while (child!=null){
// only process li elements (and not text elements)
if (child.nodeType==1){
// build a list of child ol and ul elements and hide them
var list=new Array();
var grandchild=child.firstChild;
while (grandchild!=null){
if (grandchild.tagName=='OL' || grandchild.tagName=='UL'){
grandchild.style.display='none';
list.push(grandchild);
}
grandchild=grandchild.nextSibling;
}
// add toggle buttons
var node=document.createElement('img');
node.setAttribute('src',CLOSED_IMAGE);
node.setAttribute('class','collapsibleClosed');
node.onclick=createToggleFunction(node,list);
child.insertBefore(node,child.firstChild);
}
I confess I'm too much of a javascript novice to understand why that particular line of code is causing the error. I looked at some of the other questions here, and was wondering if it might be a problem with setAttribute?
Thanks in advance.
Edited to add:
Here's the code for the createToggleFunction function. The whole of the script is just these two functions (plus declaring variables for the images).
function createToggleFunction(toggleElement,sublistElements){
return function(){
// toggle status of toggle gadget
if (toggleElement.getAttribute('class')=='collapsibleClosed'){
toggleElement.setAttribute('class','collapsibleOpen');
toggleElement.setAttribute('src',OPEN_IMAGE);
}else{
toggleElement.setAttribute('class','collapsibleClosed');
toggleElement.setAttribute('src',CLOSED_IMAGE);
}
// toggle display of sublists
for (var i=0;i<sublistElements.length;i++){
sublistElements[i].style.display=
(sublistElements[i].style.display=='block')?'none':'block';
}
}
}
Edited to add (again):
Per David's suggestion, I changed all instances of setAttribute & getAttribute...but clearly I did something wrong. IE is breaking at the 1st line (which is simply the doctype declaration) and at line 49, which is the same line of code where it was breaking before:
node.onclick=createToggleFunction(node,list);
Here's the first function as written now:
function makeCollapsible(listElement){
// removed list item bullets and the sapce they occupy
listElement.style.listStyle='none';
listElement.style.marginLeft='0';
listElement.style.paddingLeft='0';
// loop over all child elements of the list
var child=listElement.firstChild;
while (child!=null){
// only process li elements (and not text elements)
if (child.nodeType==1){
// build a list of child ol and ul elements and hide them
var list=new Array();
var grandchild=child.firstChild;
while (grandchild!=null){
if (grandchild.tagName=='OL' || grandchild.tagName=='UL'){
grandchild.style.display='none';
list.push(grandchild);
}
grandchild=grandchild.nextSibling;
}
// add toggle buttons
var node=document.createElement('img');
node.src = CLOSED_IMAGE;
node.className = 'collapsibleClosed';
node.onclick=createToggleFunction(node,list);
child.insertBefore(node,child.firstChild);
}
child=child.nextSibling;
}
}
And here's the second function:
function createToggleFunction(toggleElement,sublistElements){
return function(){
// toggle status of toggle gadget
// Use foo.className = 'bar'; instead of foo.setAttribute('class', 'bar');
if (toggleElement.className == 'collapsibleClosed') {
toggleElement.className = 'collapsibleOpen';
toggleElement.src = OPEN_IMAGE;
} else {
toggleElement.className = 'collapsibleClosed';
toggleElement.src = CLOSED_IMAGE;
}
// toggle display of sublists
for (var i=0;i<sublistElements.length;i++){
sublistElements[i].style.display=
(sublistElements[i].style.display=='block')?'none':'block';
}
}
}
Internet Explorer (until version 8, and then only in best standards mode) has a very broken implementation of setAttribute and getAttribute.
It effectively looks something like this:
function setAttribute(attribute, value) {
this[attribute] = value;
function getAttribute(attribute, value) {
return this[attribute];
}
This works fine iif the attribute name matches the property name, and the property takes a string value.
This isn't the case for the class attribute, where the matching property is className.
Use foo.className = 'bar'; instead of foo.setAttribute('class', 'bar');
node.onclick=createToggleFunction(node,list);
That is probably not what you want. Does createToggleFunction return a function? If it doesn't, then I bet you meant this:
node.onClick = function() { createToggleFunction(node, list); };
If my guess is right then the way you have it will set the onClick event handler to be the result of createToggleFunction, not a function like it needs to be.