Hi All,
I have a few hundred lines of jQuery code that pull a list of items and subitems from a database, build a series of divs to display them, and add them to a list.
I can show the code if necessary, though I can't imagine anyone will want to slog through it all, but the issue I am having is that one of my functions is taking about 6 seconds when it really shouldn't be.
It's a function to write the list from the returned data. So it takes a template div, adds data to it from a JavaScript object (about 15 fields), and appends it to another div to make the list.
Strangely, the same function runs once at init (as part of a larger block of code), and does fine, but if I run it again, by itself, it takes about 6 seconds.
If I use profile() and profileEnd() with firebug, it says it took about 1.1 seconds, however, if I use time() and TimeEnd(), it says (Correctly) about 6.7 seconds.
This function does not fetch any data from the server - it only uses data that was previously fetched from the server, so I am pretty sure we're not waiting on any external resources. Also, my processor usage is 100% for firefox for the 6 seconds it's running.
Anyone know any real Gotcha areas for jQuery? I remember once I had an issue like this that ended up being due to a .find() that wasn't detailed enough.
I will be trying to narrow down where the issue lives, so that I can show the code, but certainly open to any general ideas from jQuery gurus out there.
Thanks!
Edit Two
It appears to have something to do with jQuery's .data().
Don't know why yet, but this code:
// looping...
console.time('main');
// SortVal is already in the data, so no need to add it here.
Parent.data('SortChildrenBy', SectionsAttr[i]);
Parent.data('SortChildrenDir', 'asc');
console.timeEnd('main');
results in about 100ms per loop just for those two lines of code, while this code:
console.time('a');
Parent.data({'SortVal':Parent.data('SortVal'),'SortChildrenBy':SectionsAttr[i], 'SortChildrenDir':'asc'});
console.timeEnd('a');
results in about 1 ms per loop.
Perhaps because the first needs to compare to existing data and the second just wipes data and inserts?
Edit
Per Andrew, here is the method that seems to be taking up most of the time.
this.InsertTask = function(Position, task){
console.time("InsertTask");
/*
Find the 'path' where the task will be inserted, based on the sort.
Each sort item will be a section of the list, or a subsection of a section, with the tasks inserted as the leaves.
ie: Sorted by DueDateTS, ClientID, UserID
the task would go in <div id='...[DueDateTS]'> <div id='...[ClientID]'> <div id='...[UserID]'> </div> </div> </div>
REMEMBER WE ARE NOT ACTUALLY SORTING HERE, SO DON'T USE UP RESOURCES, JUST ADD THEM TO THE CORRECT SECTION.
THEY WILL BE SORTED LATER AFTER THEY ARE ALL ADDED.
*/
var SortItem = [];
var SectionsAttr = [];
var SectionsVal = [];
var SectionsLabel = [];
var TaskElID = '';
// Build the sections based on the order by.
for ( i in this.data.order.by ) {
SortItem['attr'] = this.data.order.by[i].split(':')[0];
SortItem['dir'] = this.data.order.by[i].split(':')[1];
SortItem['label'] = this.data.order.by[i].split(':')[2];
// Remove any characters that will give us problems using this as part of an html attribute (id in particular) later.
SectionsAttr[i] = SortItem['attr'];
SectionsVal[i] = task[SortItem['attr']];
SectionsLabel[i] = task[SortItem['label']];
// Force the values to string.
SectionsAttr[i] = SectionsAttr[i]+"";
SectionsVal[i] = SectionsVal[i]+"";
SectionsLabel[i] = SectionsLabel[i]+"";
// Remove any characters from the attribute and value that will give us problems using this as part of an html attribute (id in particular) later.
// Remember tha the label is NOT sanitized and should only be used in HTML TEXT.
SectionsAttr[i] = SectionsAttr[i].replace(/[^0-9a-zA-Z-_.]/,'');
SectionsVal[i] = SectionsVal[i].replace(/[^0-9a-zA-Z-_.]/,'');
}
TaskSectionID ='tl_section_'+SectionsAttr.join('-')+'_'+SectionsVal.join('-');
// Build the actual section elements for the taskel.
// Each will be added, and then be the parent to it's sub sections, if there are any.
// The initial parent is the tasklist itself.
var Parent = $('#tasklist');
var ElID ='';
var ElIDAttr = '';
var ElIDVal = '';
var CurrentEl = null;
var Level = 0;
for (i in SectionsAttr) {
// @todo - SOMETHING WRONG WITH THIS SECTION. SEEMS LIKE THE FIRST HEADER UNDER R&G GOES TO THE WRONG PARENT, WHEN IT'S SORTED BY CLIENT/USER ONLY.
// Count how many levels down we are.
Level++;
// Build the attribute list to be used in the element name for uniqueness.
if ( ''!=ElIDAttr ) {
ElIDAttr +='-';
ElIDVal +='-';
}
ElIDAttr +=SectionsAttr[i];
ElIDVal +=SectionsVal[i];
ElID = 'tl_section_'+ElIDAttr + "_" + ElIDVal;
// If the section doesn't have an element (div) then create it.
if ( 0==Parent.children('#'+ElID).length ) {
// Set the sort directive for this level, stored in the parent.
// The actual value will be stored in the level item.
Parent.data('SortChildrenBy', SectionsAttr[i]);
Parent.data('SortChildrenDir', 'asc');
// Make the section container in the parent.
var SectionContainer = $('<div>', {
id : ElID,
class : 'tl_section',
})
.data('SortVal', SectionsLabel[i])
.appendTo(Parent);
//Parent.append( SectionContainer );
// Make the header, which will be used for the section summary.
HLevel = 4 < Level ? 4 : Level;// 4 is the max level for css...
var SectionContainerHeader = $('<h'+HLevel+'>', {
id : ''+ElID+'_Header',
class : 'tl_section_header tl_section_h'+HLevel,
text : SectionsLabel[i]
});
SectionContainer.append(SectionContainerHeader);
if ( !this.settings.showheaders ) {
SectionContainerHeader.hide();
}
} else {
// The section container is previously created (by another task in it or one of it's sub section containers.)
SectionContainer = $('#'+ElID);
}
// The section container is considered the parent for the rest of this method, as we are focused on the actual task container being inserted into it.
Parent = SectionContainer;
}
Parent = SectionContainer = null;
TaskSectionEl = $('#'+TaskSectionID);
/*
* Validate the position where this task is to be inserted.
* The inserted task will take the place of an existing task at that position, and push it and all children after it down by one.
* They are indexed from zero. Invalid or out of range goes to the end, but should not generate an error, as the position null or -1 may be used to mean 'append to the end'.
*/
// The max position is equal to the number of tasks.
var MaxPosition = $(TaskSectionEl).children('.tasklist_item').length;
// If the position is an invalid index (not between 0 and max position), then set it to the max position and append it to the list.
// In the event that max position is 0, then so will be position.
if ( ( Position < 0 ) || ( MaxPosition < Position ) || ( Position != parseInt(Position) ) ) {
Position = MaxPosition;
}
/*
* Create a new task from the template, and append it ot the list in it's proper position.
* Be sure not to make it visible until we are done working on it, so the page won't have to reflow.
*/
// Copy the template, make a jquery handle for it.
// Leave it invisible at this point, so it won't cause the page to reflow.
TaskEl = $('#template_container .task')
.clone()
.attr('id', 'tasklist_item_'+task.TaskID)
.data('TaskID', task.TaskID)
.removeClass('task')
.addClass('tasklist_item');
// Hide the container.
HeaderTemp = SectionContainerHeader.detach();
// Insert the new task
if ( 0 == Position ) {
$(TaskSectionEl).prepend( TaskEl );
} else {
$(TaskSectionEl).children('.tasklist_item').eq(Position-1).after( TaskEl );
}
HeaderTemp.prependTo(TaskSectionEl);
// The title area.
var TaskTitle = TaskEl.find('.task_title')
.attr('id', 'task_title_'+task.TaskID)
.text( task.Title );
// Add the context menu to pending tasks.
if ( 0 == task.Completed ) {
ContextMenuPendingTask(TaskTitle);
}
// The subtitle area.
// Hide the body.
TaskEl.find('.header_collapse').hide();
TaskEl.find('.task_bodycontainer').hide();
// The exp/collapse buttons.
TaskEl.find('.header_expand').bind('click', function(){
global_this.ToggleTask( $(this).closest('.tasklist_item'), 'exp' );
});
TaskEl.find('.header_collapse').bind('click', function(){
global_this.ToggleTask( $(this).closest('.tasklist_item'), 'col' );
});
// Show the date and time.
TaskEl.find('.task_duedate')
.text(task.DueDateV + (null === task.DueTimeV ? "" : " "+task.DueTimeV+""));
// Tweak for due status.
switch( task.DueStatus ){
case 1:/* Nothing if pending.*/break
case 0:TaskEl.find('.task_duedate').addClass('task_status_due');break;
case -1:TaskEl.find('.task_duedate').addClass('task_status_pastdue');break;
}
// The other subtitle items.
TaskEl.find('.task_clientname').html(task.ClientName).before('<span> • </span>');
//$('#'+TaskIdAttrib+' .task_ownername').html(task.OwnerName);
// If the user is different from the owner.
if ( task.OwnerID != task.UserID ) {
TaskEl.find('.task_username').html(task.UserName).before('<span> • </span>');
}
// SubTask Count
/*
if ( 0 < task.SubTaskCount ) {
$('#'+TaskIdAttrib+' .task_subtaskcount').html("("+task.SubTaskCount+" Subtasks)");
}
*/
// Note Count
if ( 0 < task.NoteCount ) {
TaskEl.find('.task_notecount').html("("+task.NoteCount+" notes)");
}
// Body.
TaskEl.find('.task_body').html(task.Body);
// If the task is marked done.
if ( 0 == task.Completed ) {
// The task done checkbox.
TaskEl.find('.task_header .task_done').bind('change', function(){
if ($(this).attr('checked')) {
global_this.MarkTaskDone($(this).closest('.tasklist_item').data('TaskID'));
}
})
} else {
// The task done checkbox.
TaskEl
.find('*')
.andSelf()
.css({
color: '#aaaaaa',
})
.find('.task_header .task_done')
.remove()
.end()
.find('.task_header .task_title')
.after('<br /><span>Done by '+task.CompletedByName+' on '+task.CompletionDateV+'</span>')
.end()
.find('.subtask_header .subtask_done')
.remove()
.end()
;
}
for ( var i in task.SubTasks ) {
subtask = task.SubTasks[i];
ParentSTIDAttrib = null === subtask.ParentST ? TaskEl.attr('id')+' > .task_bodycontainer' : 'SubTask_'+subtask.ParentST;
SubTaskEl = $('#template_container .subtask')
.clone()
.attr('id', 'SubTask_'+subtask.SubTaskID)
.data('SubTaskID', subtask.SubTaskID)
.appendTo('#'+ParentSTIDAttrib+' > .task_subtaskcontainer');
SubTaskEl.find('.subtask_title').html(subtask.Title);
if ( 0 != subtask.DueDiff ) {
SubTaskEl.find('.subtask_duediff').html(" (Due "+subtask.DueDiffDateV+")");
}
if ( task.OwnerID != subtask.UserID ) {
SubTaskEl.find('.subtask_username').html(subtask.UserName).before('<span> - </span>');
}
if ( 0 == subtask.Completed ) {
// The sub task done checkbox.
SubTaskEl.find('.subtask_header > .subtask_done').bind('change', function(){
if ($(this).attr('checked')) {
global_this.MarkSubTaskDone($(this).closest('.subtask').data('SubTaskID'));
}
});
} else {
SubTaskEl
.find('.subtask_header > .subtask_done')
.attr('disabled', 'disabled')
.attr('checked', 'checked')
.end()
.find('.subtask_header .subtask_title')
.after('<span>Done by '+subtask.CompletedByName+' on '+subtask.CompletionDateV+'</span> ')
.end();
}
}
for (var i in task.Notes) {
NoteEl = $('#template_container .note')
.clone()
.attr('id', 'Note_'+task.Notes[i].NoteID)
.appendTo( TaskEl.find('.task_notecontainer') );
NoteEl.find('.note_date').html(task.Notes[i].NoteDateV);
NoteEl.find('.note_user').html(task.Notes[i].UserName);
NoteEl.find('.note_body').html(task.Notes[i].Note);
}
// Put the client id into the templates data.
TaskEl.data('TaskID', task.TaskID);
console.timeEnd("InsertTask");
return;
}