Reputation: 4803
I have a page which groups contacts by letter in a grid. For my larger sets of contacts (1000-1500) the grid loads unbearably slow (minutes). But, the brower pretty much locks up so people think it's crashed.
According to the chrome profiler I'm spending 91% of the time in my recent worst enemy .InnerHTML().
From what I can tell I'm not sending out unnecessary notifications when filling the contact observables.
I've tried to distill the page down to a simple form in this jsFiddle.
http://jsfiddle.net/DCLaR/105/ (Credit to Ryan Rahlf for fixing this)
It looks like the simple version fills ok, so there must be something else in the mix.
I'm going to go ahead and post my full page html even though it's kind of a mess.
Started on a fiddle of actual page code, doesn't quit run, maybe too complicated to be stuffing in there: http://jsfiddle.net/DCLaR/108/
Maybe someone can spot something off or that is known to cause performance issues.
I saw This question which is similar but didn't get a solution from it.
The HTML
<button id="btnSaveOrg" data-bind="click: $root.SaveAll">Save All</button><span data-bind="html: SaveStatus"></span>
<table class="orgTable">
<tr>
<td>
<ul id="orgList" data-bind="template: { name: 'orgChartTemplate', foreach: DepartmentHierarchy }" style="display:none"></ul>
<div id="orgChart2" class="orgChart"></div>
</td>
<td data-bind="with: SelectedDepartment">
<div class="departmentContainer">
<span class="departmentTitle" data-bind="text: DepartmentName" ></span>
<div class="buttonBar">
<a title="Edit Department" data-bind="click: $root.BeginEdit, visible: $root.InDisplayMode">
<img src="/Images/iconEdit.png" alt="Edit" />
</a>
<a title="Add Department" data-bind="click: $root.BeginAdd, visible: $root.InDisplayMode">
<img src="/Images/iconAdd.png" alt="Add" />
</a>
</div>
<div class="departmentInnerContainer">
<div id="departmentTabs" data-bind="kendoTabStrip: {}">
<ul>
<li class="k-state-active" data-bind="visible: IsRoot">All Company Contacts</li>
<li class="k-state-active" data-bind="visible: IsNotRoot">Department Contacts</li>
<li data-bind="visible: IsNotRoot">Associate Contacts</li>
</ul>
<div id="allContacts">
<div class="buttonBar">
Find: <input type="text" data-bind="value: $root.CurrentDepartmentContacts.SearchStringText" /><a data-bind=" click: $root.CurrentDepartmentContacts.Search"><img src="/Images/iconSearch.png" /></a>
</div>
<div data-bind="template: { name: 'ContactListTemplate', foreach: $root.CurrentDepartmentContacts }"></div>
</div>
<div id="currentContacts">
<div class="buttonBar">
Find: <input type="text" data-bind="value: $root.CurrentDepartmentContacts.SearchStringText" /><a data-bind=" click: $root.CurrentDepartmentContacts.Search"><img src="/Images/iconSearch.png" /></a>
</div>
<div data-bind="template: { name: 'ContactListTemplate', foreach: $root.CurrentDepartmentContacts }"></div>
</div>
<div id="assocContacts">
<div>
<input type="checkbox" data-bind="checked: $root.ShowAssociatedContacts" /> Show Contacts tied to departments<br />
<input data-bind="value: $root.ContactSearchText" /><a data-bind="click: $root.SearchAllContacts "><img src="/Images/iconSearch.png" /></a>
<br />
<button data-bind="click: $root.CheckAllContacts">Check All</button> <button data-bind="click: $root.UncheckAllContacts">Uncheck All</button>
</div>
<!-- ko if: $root.AllContactsSearchResult().length>0 -->
<div class="contactsContainerOrg" data-bind="foreach: $root.AllContactsSearchResult">
<div>
<input type="checkbox" data-bind="checked: IsChecked" /><span data-bind="html: ContactName"></span>
</div>
</div>
<!-- /ko -->
<!-- ko if: $root.AllContactsSearchResult().length==0 -->
<div class="contactsContainerOrg">
<div>
No Contacts Found
</div>
</div>
<!-- /ko -->
<button data-bind="click: $root.AssociateSelectedContacts, enable: $root.AssociatedContactsChecked">Associate Contacts</button>
</div>
</div>
</div>
</div>
</td>
</tr>
</table>
<div id="divAddEditDepartment" style="display:none">
<span data-bind="template: { name: 'editDeptTemplate', foreach: EditingDepartment }"></span>
</div>
<script type="text/html" id="OldDept">
<div class="departmentContainer">
<button id="Button1" data-bind="click: $root.AddDepartment, visible: $root.InDisplayMode">Add Department</button>
<fieldset class="departmentFieldSet">
<legend>Selcted Department</legend>
<span data-bind="template: { name: 'viewDeptTemplate', foreach: SelectedDepartment }, visible: InDisplayMode"></span>
<span data-bind="template: { name: 'editDeptTemplate', foreach: SelectedDepartment }, visible: InEditMode"></span><br /><br />
<b>Contacts</b><br />
Search: <input type="text" data-bind="value: $root.ContactsVM.SearchString" /><br />
<div data-bind="template: { name: 'ContactListTemplate', foreach: $root.ContactsVM }"></div>
</fieldset>
</div>
</script>
<asp:HiddenField ID="hdnCustomerId" Value="7" runat="server"/>
<!-- Tempaltes -->
<script type="text/html" id="orgChartTemplate">
<li data-bind="attr: { DeptId: CustomerDepartmentId }">
<div class="innerNode" data-bind="css: { selectedNode: IsSelected }">
<img src="/Images/Contacts/CompanyOrg.png" /><span style="font-weight:bold;display:block" data-bind="html: DepartmentName"></span> <br /><br />
</div>
<ul data-bind="template: { name: 'orgChartTemplateChild', foreach: Children }"></ul>
</li>
</script>
<script type="text/html" id="orgChartTemplateChild">
<li data-bind="attr: { DeptId: CustomerDepartmentId }">
<div class="innerNode" data-bind="css: { selectedNode: IsSelected }">
<span style="font-weight:bold" data-bind="html: DepartmentName"></span> <br /><br />
<span data-bind="html: ContactCountDisplay"></span>
</div>
<ul data-bind="template: { name: 'orgChartTemplateChild', foreach: Children }"></ul>
</li>
</script>
<script type="text/html" id="viewDeptTemplate">
<div class="noWrap"> <label>Department Name:</label><div style="width: 12px; display: inline-block;" /><span data-bind="text: DepartmentName"></span></div> <br />
<span data-bind="visible: $root.IsNew">
<span class="noWrap"> <label>Parent Department:</label><span style="width: 5px; display: inline-block;"/><span data-bind="text: ParentDepartmentName"> </span></span>
</span>
<br /><br />
<button data-bind="click: $root.BeginEdit">Edit</button>
</script>
<%--Edit template markup--%>
<script type="text/html" id="editDeptTemplate" >
<div class="noWrap"> <label>Department Name:</label><div style="width: 12px; display: inline-block;" /><input type="text" data-bind="value: DepartmentName" required="required"> <span id="reqDepartmentError" style="color: red; display: none;">*</span></div><br />
<div class="noWrap"><label>Parent Department:</label><div style="width: 5px; display: inline-block;"/><select data-bind="options: $root.GetParentCustomerDepartmentOptions(), optionsText: 'DepartmentName', optionsValue: 'CustomerDepartmentId', value: ParentCustomerDepartmentId" ></select></div><br />
</script>
<script type="text/html" id="ContactListTemplate">
<div id="Div1" class="contactsContainerOrg" data-bind="foreach: ContactGroups">
<div class="contactGroupHeader" data-bind="text: GroupName, attr: { groupName: GroupName }"></div>
<div class="contactGroupContainer">
<div data-bind="template: { name: $root.GetContactTemplate, foreach: Contacts }">
</div>
</div>
</div>
<div data-bind="if: ShowGroupBy">
<span data-bind="foreach: AllLetterContactGroups">
<span class="groupList" data-bind="text: GroupName, click: $root.ScrollToGroup, style: { fontWeight: HasContacts() ? '900' : '300', fontSize: HasContacts() ? '12px' : '10px' }"></span>
</span>
</div>
</script>
<script type="text/html" id="ContactLine">
<div class="contactListItem" data-bind="text: ContactName, click: $root.CurrentDepartmentContacts.SelectContact, css: { contactListItemSelected: IsSelected() == true }"></div>
</script>
<script type="text/html" id="ContactDetails">
<div class="contactDetails">
<div>
<h2>
<span data-bind="text: ContactName" style="color: #054b73"></span>
</h2>
<label>Office Phone: </label>
<span data-bind="text: OfficePhone"></span>
<label>ext:</label>
<span data-bind="text: OfficePhoneExt"></span>
<br />
<label>Fax: </label>
<span data-bind="text: Fax"></span>
<br />
<label>Email: </label>
<span data-bind="text: Email"></span>
</div>
</div>
</script>
<script type="text/javascript">
var customerId = $("[id$=hdnCustomerId]").val();
var orgModel = new CustomerOrgVM(customerId, "orgList", "orgChart2", 'divAddEditDepartment');
function LoadCustomerOrgChart() {
orgModel.LoadDepartments();
ko.bindingHandlers.kendoTabStrip.animation = false;
ko.applyBindings(orgModel);
$w.Util.WaitAll(function () {
// Build the Hierarchy after all departments and contacts have been loaded
orgModel.BuildHierarchy();
orgModel.IsLoaded(true);
// Load Contacts Async after hierarchy is displayed
orgModel.LoadContacts();
});
}
</script>
The Javascript
Upvotes: 2
Views: 1323
Reputation: 1812
First, your fiddle isn't rendering the nested template because you forgot your quotes around the inner template name. Your code :
template: { name : ContactLine, foreach: Contacts }
should have the name in quotes like this :
template: { name : "ContactLine", foreach: Contacts }
With that fixed, 1500 items can be a sizable amount of rendering depending on what's going on in the template, although I've worked on projects where we're pushing about that much data in a table without too much trouble. Our slow-down was actually mapping from raw JSON to the view models; rendering was acceptably fast.
With that said, it's not too hard to add paging, even if you want to keep all that data on the client-side for quick sorting. If your bottleneck is actually the render and not mapping, then paging might help.
Another approach would be to build a custom binder that renders your list out in chunks of 25 or 100. Your app could then be responsive to the users so they know it's working, while silently continuing to render in the background.
I hope this helps!
Upvotes: 1