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)
Related
I have a JSON array which has 9000 records and it is displayed using list li. The HTML object is created using jQuery :
$j.each(DTOList, function(index,obj) {
var $li = $j("<li/>");
var $button = $j("<button/>", { type: "button" , onClick:"location.href='playfile.html?messageId="+obj.id+"&operation=play&to="+obj.to+"&from="+obj.from+"'" });
var $parentDiv = $j("<div/>", { class: "buttonMargin" });
var $numDiv = $j("<div/>", { class: "num" ,text:obj.to});
var $nameDiv = $j("<div/>", { class: "name" ,text:obj.from});
var $timeDiv = $j("<div/>", { class: "time" , text:obj.time});
$parentDiv.append($numDiv);
$parentDiv.append($nameDiv);
$parentDiv.append($timeDiv);
$parentDiv.append($j("<hr>"));
$j("#datalist").append($li.append($button.append($parentDiv)));
});
Here is an example of li created by the above code:
<li>
<button type="button"
onclick="location.href='playfile.html?messageId=1165484222&operation=play&to=Fax Line&from=abc'">
<div class="buttonMargin">
<div class="num">Fax Line</div>
<div class="name">def</div>
<div class="time">Jan 04,2018 12:02:44 AM</div>
<hr>
</div>
</button>
<li>
The problem here is, the above code takes at least 1.5 mins to load and till then my HTML page is blank. Is there any way to improve the above code to increase performance?
Look at this performance test. Use for loop instead of .each() function.
Likely the main problem here is the DOM operation performed in this line
$j("#datalist").append($li.append($button.append($parentDiv)));
triggering document reflows over and over....
So as a first improvement try to generate all items first (without appending it into the DOM) and then append them afterwards with one single operation.
Something like this:
var elements = [];
$j.each(DTOList, function(index,obj) {
.....
elements.push($li);
});
$j("#datalist").append(elements);
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
Using knockout.js, I have a list of items that I filter on search using ko.utils.arrayFilter.
The filtering part works ok, but then when I delete the search term and the list repopulates as the full list, some of the DOM elements lose their id. How can I make them not lose their id? What am I missing?
In particular, the items with the id "tabsDiv1", "tabsDiv2", etc lose their ids.
Thanks in advance for your help, I'm stumped!
full git repo here with example github pages site: https://github.com/andrewcockerham/UdacityFENDP5NeighborhoodMap
var ViewModel = function() {
var self = this;
this.placeList = ko.observableArray([]);
this.filter = ko.observable("");
this.filteredItems = ko.computed(function() {
var filter = self.filter().toLowerCase();
if (!filter) {
return this.placeList();
} else {
return ko.utils.arrayFilter(self.placeList(), function(item) {
return item.name().toLowerCase().indexOf(filter) !== -1;
});
}
}, this);
The problem is that you're setting the ids of the tabs manually on load instead of letting knockout set it. When knockout renders the page, it will use the current state of the dom elements as templates for your tabs. In this case, your elements had blank ids. I see you apparently had knockout set the id at one point but you commented it out. You should go back and use knockout to add the id if you really needed it.
<div class="tabsDiv col-md-12 col-sm-12" id=""
style="display: none; padding-left: 0px; padding-right: 0px;
border-style: solid;border-width:1px;"
>
<!-- data-bind="attr: { id: 'tabsDiv' + $index() }" -->
...
<script>
window.onload = function() {
// get all tabDiv elements and add id with index to each one
var tabsDivArray = document.getElementsByClassName("tabsDiv");
for (var i = 0; i < tabsDivArray.length; i++) {
var tabsDiv = tabsDivArray[i];
tabsDiv.id = 'tabsDiv' + i.toString();
};
...
This might not be the only problem, but it's a start. You're mixing up a lot of knockout and manual dom manipulation and jquery. You really should try to limit the mixing up to keep it manageable. It would be a tough read for anyone else who came and looked at this.
I'm trying to implement multiple scrollbars with the plugin Tinyscrollabr.js
http://baijs.nl/tinyscrollbar/
To implement the scrollbars, i use a function scrollify like in this article :
http://www.eccesignum.org/blog/making-tinyscrollbarjs-easier-to-implement
HTML :
<ul id="myList">
<li id="scrollbar1" class="col">
<h2>Title 01</h2>
<div class="scrollBox"><!--Scrollable Content here--></div>
</li>
<li id="scrollbar2 class="col">
<h2>Title 02</h2>
<div class="scrollBox"><!--Scrollable Content here--></div>
</li>
<li id="scrollbar3 class="col">
<h2>Title 03</h2>
<div class="scrollBox"><!--Scrollable Content here--></div>
</li>
</ul>
Javascript :
function scrollify(element,options) { // '#element', {list:of,key:values}
var opt = options || {}
$(element).children().wrapAll('<div class="viewport"><div class="overview"></div></div>');
$(element).prepend('<div class="scrollbar"><div class="track"><div class="thumb"><div class="end"></div></div></div></div>');
$(element).tinyscrollbar(options);}
$scrollbar1 = $('#scrollbar1 .scrollBox') ;
$scrollbar2 = $('#scrollbar2 .scrollBox');
$scrollbar3 = $('#scrollbar3 .scrollBox');
$scrollbar4 = $('#scrollbar4 .scrollBox');
$(function() {
scrollify($scrollbar1);
scrollify($scrollbar2);
scrollify($scrollbar3);
scrollify($scrollbar4);
})
I would to make this more simple.
For example, i would to be able to make this :
$(function() {
scrollify('.scrollBox');
})
But tinyscrollbar need an id. With a class, it's load the first scrollbar and not the others. Firebug return this error message "f.obj[0] is undefined"
Sorry if my question is stupid, but how can I do for applying tinyscrollbar to a list of elements with a class ?
And then, after some actions how to update all this scrollbars with the function $allScrollbars.tinyscrollbar_update();
Thanks for help, I'm just beginning with javascript and i'm trying to learn.
I would count the number of elements with the class:
var scrollCount = $(".scrollbox").size();
Then use an iterating loop to call each of your IDs:
for (i=0; i<5; i++) {
scrollify($('#scrollbar' + i));
}
Also I would recommend using DIVs instead of the list setup you have, use the example from the link you shared as a starting point :)
Thanks KneeSkrap3r for your answer. It's a good solution to make this but i'm trying to do something in the case i' don't know the numbers of element to scroll.
I think I've found with something like this (it's a part from the first jquery plugin i'm trying to do ) where $el is all elemnts with the class"scrollbox".
$el.each(function(index)
{
var $scrolls = $(this);
function scrollify(element,options)
{ // '#element', {list:of,key:values}
var opt = options || {}
$(element).children().wrapAll('<div class="viewport"><div class="overview"></div></div>');
$(element).prepend('<div class="scrollbar"><div class="track"><div class="thumb"><div class="end"></div></div></div></div>');
$(element).tinyscrollbar(options);
}
scrollify($scrolls);
// Update heights
$(window).resize(function()
{ $scrolls.tinyscrollbar_update('relative');
});
})
Like this, it's seems to work but i don't know if i'm using good practice of javascript.
For the Html markup, I told the li elements for div, it's better for the semantic.
Thanks for tips ;-)
I looked at the post jQuery: Loop iterating through numbered selectors? and it didn't solve my problem, and didn't look like it was truly an answer that works.
I have a list of <h3> tags that are titles to questions, and there are answers below in a <p>. I created classes for each Q & A like so:
<h3 class="sec1">Question:</h3><p class="view1">Answer...</p>
<h3 class="sec2">Question:</h3><p class="view2">Answer...</p>
<h3 class="sec3">Question:</h3><p class="view3">Answer...</p>
I used the following jQuery loop to reduce redundacy for my 21 questions.
$(document).ready(function () {
for (var i = 1; i < 21; i++) {
var link = ".sec" + i;
var content = ".view" + i;
$(link).click(function () {
$(content).toggle("fast");
});
}
});
But it isn't working for all Q & A sets, only the last one. i.e.: It works for the first set if I set the max value to 2 (only looping once). Please advise. Thanks
While I agree with #gaffleck that you should change your approach, I think it is worth while to explain how to fix the current approach.
The problem is that the click function does not get a copy of the content variable but instead has a reference to that same variable. At the end of the loop, the value is .view20. When any element is clicked it read that variable and gets back .view20.
The easiest way to solve this is to move the code into a separate function. The content variable within this function is a new variable for every call of the function.
function doIt(i){
var link = ".sec" + i;
var content = ".view" + i;
$(link).click(function () {
alert(content);
});
}
$(document).ready(function () {
for (var i = 1; i < 21; i++) {
doIt(i);
}
});
http://jsfiddle.net/TcaUg/2/
Notice in the fiddle, if you click on a question the alert has the proper number. Optionally, you could make the function inline, though I find the separate function in most cases to be a bit cleaner.
http://jsfiddle.net/TcaUg/1/
A much easier way to do this, would be this:
$(document).ready(function(){
$("h3").click(function(){
$(this).next("p").toggle("fast");
});
});
This is also safer in that you can add/remove questions and answers in the future and you won't have to update the function.
Wrap your questions in a more logical structure to create a proper scope for your questions-block:
<div id="questions">
<div class="question">
<h3 class="sec1">Question:</h3><p class="view1">Answer...</p>
</div>
<div class="question">
<h3 class="sec2">Question:</h3><p class="view2">Answer...</p>
</div>
<div class="question">
<h3 class="sec3">Question:</h3><p class="view3">Answer...</p>
</div>
</div>
Now iterate through it like this:
$(function() {
$('#questions .question h3').click(function(){
$(this).parent().find('.answer').toggle('fast');
});
});