Speeding up this Javasctipt string manipulation - javascript

I am changing the content of about 5000 HTML tags and as I read here doing 5000+ html rendering changes is slow and it is better just to redraw the HTML once,
Therefor I have created a function that loads the entire HTML into a JavaScript string and then goes through the text(looking for a label tag), changes the content of the tags and eventually redraws the HTML once.
With high hopes, this was also a failure and takes around 30 seconds on a 1000 tags test.
My function basically reverse counts all the visible label DIVs on the screen and adds numbering to them,
Here's the code, what am I doing wrong please? (here's an isolated jsfiddle: http://jsfiddle.net/tzvish/eLbTy/8/)
// Goes through all visible entries on the page and updates their count value
function updateCountLabels() {
var entries = document.getElementsByName('entryWell');
var entriesId = document.getElementsByName('entryId');
var entriesHtml = document.getElementById('resultContainer').innerHTML;
var visibleEntries = new Array();
var countEntries = 0 , pointer = 0;
// Create a list of all visible arrays:
for (i = 0; i < entries.length; i++) {
if ($(entries[i]).is(":visible")) {
countEntries++;
visibleEntries.push(entriesId[i].value);
}
}
// Update html of all visible arrays with numbering:
for (i = 0; i < visibleEntries.length; i++) {
currentId = visibleEntries[i];
// Get the position of insertion for that label element:
elementHtml = '<div id="entryCount'+currentId+'" class="entryCountLabel label label-info">';
pointer = entriesHtml.indexOf(elementHtml) + elementHtml.length;
pointerEnd = entriesHtml.indexOf("</div>",pointer);
entriesHtml = entriesHtml.substring(0, pointer) + countEntries + entriesHtml.substring(pointerEnd);
countEntries--;
}
// apply the new modified HTML:
document.getElementById('resultContainer').innerHTML = entriesHtml;
}

From here:
Loops are frequently found to be bottlenecks in JavaScript. To make a loop the most efficient, reverse the order in which you process the items so that the control condition compares the iterator to zero. This is far faster than comparing a value to a nonzero number and significantly speeds up array processing. If there are a large number of required iterations, you may also want to consider using Duff’s Device to speed up execution.
So change:
for (i = 0; i < entries.length; i++)
To
for (i = entries.length - 1; i ==0 ; i--)
Also notice following paragraph from mentioned link:
Be careful when using HTMLCollection objects. Each time a property is accessed on one of these objects, it requires a query of the DOM for matching nodes. This is an expensive operation that can be avoided by accessing HTMLCollection properties only when necessary and storing frequently used values (such as the length property) in local variables.
So change:
for (i = entries.length - 1; i == 0; i--)
To:
var len = entries.length;
for (i = len - 1; i ==0 ; i--)

Related

Using array to store elements doesn't let you change the properties

const divArr = [];
for (let i = 0; i < 5; i++) {
document.getElementById("h").innerHTML += `<div id=${i}>hello</div>`;
divArr.push(document.getElementById(`${i}`));
}
console.log(divArr);
let divArrIndex = 0;
setInterval(() => {
document.getElementById(`${divArrIndex}`).style.backgroundColor = 'green';
if (divArrIndex > 4) {
divArrIndex = 0;
}
divArrIndex += 1;
}, 1000 / 1);
<div id="h">alsdjf;lkajsdf</div>
The code above successfully turns all the divs green.
But, when I use
divArr[divArrIndex].style.backgroundColor = "green"
instead of
document.getElementById(`${divArrIndex}`).style.backgroundColor='green';
I only get the last div green.
Why?
codepen: https://codepen.io/sai-nallani/pen/KKopOXZ?editors=1111
By reassignment to innerHTML, you are destroying and recreating the contents of #h in each iteration of the loop. You create #0, then discard it and create #0 and #1, then discard those and create #0, #1, #2... So the elements you push into the array don't exist any more in your document (though references to them in divArr still keep the garbage collector from destroying them outright).
When you change the colour of divArr[0], you are changing the colour of an element that only exists in memory, but is not in DOM any more. However, #4 is still the original #4, it has not been discarded, since you have performed no further assignments to innerHTML.
One solution is to gather all the divs after you have constructed them all. You can use another loop, but the easiest way would be:
const divArr = Array.from(document.querySelectorAll('#h > div'));
(Depending on what you are doing with it next, you may not need Array.from since NodeList should suffice for many purposes.)
Another solution is to construct elements in their own right, not by changing the parent's innerHTML:
const hEl = document.querySelector('#h');
for (let i = 0; i < 5; i++) {
const divEl = document.createElement('div');
divEl.textContent = 'Hello';
divEl.id = i;
hEl.appendChild(divEl);
divArr.push(divEl);
}
This way, every element is created and added to #h without affecting any other elements already there.

Internet Explorer 11 very slow appending elements to DOM

I have a webpage which loads data via an ajax call and then populates a "table". It actually uses a 'div' element as a row and more 'div' elements as the cells. For the purposes of testing I am not applying any styles or classes to these divs, they are simply: <div><div>cell1</div><div>cell2</div></div>. During testing I am loading 2000 rows with 5 columns in each.
I have tried using three different methods similar to these
function test(method) {
var totalRows = 2000;
var totalCols = 6;
var rows = [];
var r, c, row, cell;
for (r = 0; r < totalRows; r++) {
row = [];
rows.push(row);
for (c = 0; c < totalCols; c++) {
row.push(r + ':' + c + ' Just some text');
}
}
var container = document.getElementById('container');
var output = document.getElementById('output');
var div = document.createElement('div');
container.innerHTML = '';
var start = new Date().getTime();
switch (method) {
case 1:
for (r = 0; r < totalRows; r++) {
row = div.cloneNode(false);
container.appendChild(row);
for (c = 0; c < totalCols; c++) {
cell = div.cloneNode(false);
cell.appendChild(document.createTextNode(rows[r][c]));
row.appendChild(cell);
}
}
break;
case 2:
var outer = document.createElement('div');
var frag = document.createDocumentFragment();
for (r = 0; r < totalRows; r++) {
row = div.cloneNode(false);
frag.appendChild(row);
for (c = 0; c < totalCols; c++) {
cell = div.cloneNode(false);
cell.appendChild(document.createTextNode(rows[r][c]));
row.appendChild(cell);
}
}
container.appendChild(frag);
break;
case 3:
var els = [];
for (r = 0; r < totalRows; r++) {
els.push('<div>');
for (c = 0; c < totalCols; c++) {
els.push('<div>');
els.push(rows[r][c]);
els.push('</div>');
}
els.push('</div>');
}
// ignore string creation
start = new Date().getTime();
container.innerHTML = els.join('');
break;
}
var end = new Date().getTime();
output.innerHTML = 'time: ' + (end - start);
}
<input type="button" value="Method 1 (Direct)" onclick="test(1)"><br>
<input type="button" value="Method 2 (Fragment)" onclick="test(2)"><br>
<input type="button" value="Method 3 (innerHTML)" onclick="test(3)"><br>
<div id="output"></div>
<div id="container">
</div>
Method 1) create elements and insert directly into the DOM;
Method 2) create a document fragment and append into that. At the end insert the fragment into the DOM;
Method 3) create a text HTML representation and set the innerHTML.
In the first two methods I am cloning elements rather than creating them.
I am using plain JavaScript for this part. The examples run very quickly using Firefox, all under 40 ms. Using IE 11, the first two methods run in about 2000 ms, whereas the third method runs in 150 ms. This would be acceptable for my needs.
The problem arises when I try these methods in my actual web application. The "table" is nested in a much more complicated structure. Unfortunately it is too complicated for me to provide a working example. The table itself retains exactly the same structure. When using Firefox the table is displayed in about 1 second (after the data has been fetched from the server). This is acceptable. IE however, takes over 20 seconds and it makes little difference which of the three methods I use.
Using method 2 (appending to a document fragment first) I can see in the profiler that it takes under a second to prepare the fragment but 23 seconds to append the fragment to the DOM. The profiler shows lots of "DOM event (DOMNodeInserted)" events followed by "Garbage collection". There are perhaps 40 of these. Note: this profile is from the application not the code snippet.
The screen shot shows these entries.
The profiler labels the GC entries as JavaScript garbage collection (rather than some internal IE stuff). My questions are:
Why would it be firing the DOMNodeInserted event?
What is the GC worknig on (None of the variable are going out of scope at this point)?
Why does the profiler show 3 second gaps every second or third event (the times of the individual bits don't add up)?
Could my code be improved?
I have tried every optimization I can think of including: setting the style display to none while building it; cloning nodes instead of creating them; and declaring variable outside of the loops. As far as I can deduce I don't have any event listeners attached to this part of the DOM.
It turns out it was the profiler or more specifically having F12 tools open that caused it to slow down. My initial code was slow so I used the profile while optimising it. I will leave the question up in case anyone else has a similar problem.

Google Script for a Sheet - Maximum Execution time Exceeded

I'm writing a script that's going to look through a monthly report and create sheets for each store for a company we do work for and copy data for each to the new sheets. Currently the issue I'm running into is that we have two days of data and 171 lines is taking my script 369.261 seconds to run and it is failing to finish.
function menuItem1() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet1 = ss.getSheetByName("All Stores");
var data = sheet1.getDataRange().getValues();
var CurStore;
var stores = [];
var target_sheet;
var last_row;
var source_range
var target_range;
var first_row = sheet1.getRange("A" + 1 +":I" + 1);
//assign first store number into initial index of array
CurStore = data[1][6].toString();
//add 0 to the string so that all store numbers are four digits.
while (CurStore.length < 4) {CurStore = "0" + CurStore;}
stores[0] = CurStore;
// traverse through every row and add all unique store numbers to the array
for (var row = 2; row <= data.length; row++) {
CurStore = data[row-1][6].toString();
while (CurStore.length < 4) {
CurStore = "0" + CurStore;
}
if (stores.indexOf(CurStore) == -1) {
stores.push(CurStore.toString());
}
}
// sort the store numbers into numerical order
stores.sort();
// traverse through the stores array, creating a sheet for each store, set the master sheet as the active so we can copy values, insert first row (this is for column labels), traverse though every row and when the unique store is found,
// we take the whole row and paste it onto it's newly created sheet
// at the end push a notification to the user letting them know the report is finished.
for (var i = stores.length -1; i >= 0; i--) {
ss.insertSheet(stores[i].toString());
ss.setActiveSheet(sheet1);
target_sheet = ss.getSheetByName(stores[i].toString());
last_row = target_sheet.getLastRow();
target_range = target_sheet.getRange("A"+(last_row+1)+":G"+(last_row+1));
first_row.copyTo(target_range);
for (var row = 2; row <= data.length; row++) {
CurStore = data[row-1][6].toString();
while (CurStore.length < 4) {
CurStore = "0" + CurStore;
}
if (stores[i] == CurStore) {
source_range = sheet1.getRange("A" + row +":I" + row);
last_row = target_sheet.getLastRow();
target_range = target_sheet.getRange("A"+(last_row+1)+":G"+(last_row+1));
source_range.copyTo(target_range);
}
}
for (var j = 1; j <= 9; j++) {
target_sheet.autoResizeColumn(j);
}
}
Browser.msgBox("The report has been finished.");
}
Any help would be greatly appreciated as I'm still relatively new at using this, and I'm sure there are plenty of ways to speed this up, if not, I'll end up finding a way to break down the function to divide up the execution. If need be, I can also provide some sample data if need be.
Thanks in advance.
The problem is calling SpreadsheepApp lib related methods like getRange() in each iteration. As stated here:
Using JavaScript operations within your script is considerably faster
than calling other services. Anything you can accomplish within Google
Apps Script itself will be much faster than making calls that need to
fetch data from Google's servers or an external server, such as
requests to Spreadsheets, Docs, Sites, Translate, UrlFetch, and so on.
Your scripts will run faster if you can find ways to minimize the
calls the scripts make to those services.
I ran into the same situation and, instead of doing something like for(i=0;i<data.length;i++), I ended up dividing the data.length into 3 separate functions and ran them manually each time one of them ended.
Same as you, I had a complex report to automate and this was the only solution.

Optimizing JavaScript autocorrect implementation

I modified the jQuery Autocomplete implementation to generate autocorrect suggestions too. I used Levenshtein distance as the metric to decide closest matches.
My code runs at each keypress after the jQuery autocomplete does not have any more suggestions. Here's the code that I wrote:
// Returns edit distance between two strings
edit_distance : function(s1, s2) {
// Auxiliary 2D array
var arr = new Array(s1.length+1);
for(var i=0 ; i<s1.length+1 ; i++)
arr[i] = new Array(s2.length+1);
// Algorithm
for(var i=0 ; i<=s1.length ; i++)
for(var j=0 ; j<=s2.length ; j++)
arr[i][j] = 0;
for(var i=0 ; i<=s1.length ; i++)
arr[i][0] = i;
for(var i=0 ; i<=s2.length ; i++)
arr[0][i] = i;
for(var i=1 ; i<=s1.length ; i++)
for(var j=1 ; j<=s2.length ; j++)
arr[i][j] = Math.min(arr[i-1][j-1] + (s1.charAt(i-1)==s2.charAt(j-1) ? 0 : 1), arr[i-1][j]+1, arr[i][j-1]+1);
// Final answer
return arr[s1.length][s2.length].toString(10);
},
// This is called at each keypress
auto_correct : function() {
// Make object array for sorting both names and IDs in one go
var objArray = new Array();
for(var i=0 ; i<idArray.length ; i++) {
objArray[i] = new Object();
objArray[i].id = idArray[i];
objArray[i].name = nameArray[i];
}
// Sort object array by edit distance
var out = this;
companyObjArray.sort (
function(a,b) {
var input = jQuery("#inputbox").val().toLowerCase();
var d1 = a.name.toLowerCase();
var d2 = b.name.toLowerCase();
return out.editDistance(input,d1) - out.editDistance(input,d2);
}
);
// Copy some closest matches in arrays that are shown by jQuery
this.suggestions = new Array();
this.data = new Array();
for(var i=0 ; i<5 ; i++) {
this.suggestions.push(companyObjArray[i].name);
this.data.push(companyObjArray[i].id);
}
}
All names have IDs associated with them, so before sorting I'm just creating an object array out of them, and sorting the array.
Since the list to search from is in thousands, its slow. I found a data structure called a BK-tree, which can speed it up, but I'm can't implement it right now. I'm looking for optimization suggestions to speed this up. Any suggestions are welcome. Thanks in advance.
EDIT. I decided to use Sift3 as my string distance metric instead of Levenshein distance, it gives more meaningful results and is faster.
There are quite a few things you can optimize here, but most of it boils down to this: Only do each of your 'heavier' calculations once. You are doing a lot of work on each keypress, in every sort comparison, etc., and caching those values will help a lot.
However, for a significant added performance boost, there is another, quite nifty optimization trick you can use. It is used by every major search engine, including on-site search engines on sites such as Amazon.com.
It takes advantage of the fact that you really don't need to sort the whole list, since all you're displaying are the top 10 or 12 items. Whether the rest of the thousands of list items are in the correct order doesn't matter in the least. So all you really have to keep track of while running through the list, are the top 10 or 12 items you've seen so far, which, as it turns out, is a lot faster than full sorting.
Here's the idea in pseudocode:
The user types a character, creating a new search term
We define an empty shortlist array of length 12 (or however many suggestions we want)
Loop through the full list of words (we will call it the dictionary):
Calculate the (Levenshtein) edit distance between the dictionary word and the search term
If the distance is lower (better) than the current threshold, we:
Add the word to the top of the shortlist
Let the bottom word in the shortlist 'overflow' out of the list
Set our threshold distance to match the word now at the bottom of the shortlist
When the loop has finished, we have a shortlist containing some of the best words, but they are unsorted, so we sort just those 12 words, which will be really fast.
One small caveat: Depending on the dataset and the number of elements in your shortlist, the shortlist may differ slightly from the actual top elements, but this can be alleviated by increasing the shortlist size, e.g. to 50, and just removing the bottom 38 once the final sort is done.
As for the actual code:
// Cache important elements and values
var $searchField = $('#searchField'),
$results = $('#results'),
numberOfSuggestions = 12,
shortlistWindowSize = 50,
shortlist,
thresholdDistance;
// Do as little as possible in the keyboard event handler
$searchField.on('keyup', function(){
shortlist = [];
thresholdDistance = 100;
// Run through the full dictionary just once,
// storing just the best N we've seen so far
for (var i=0; i<dictionarySize; i++) {
var dist = edit_distance(this.value, dictionary[i]);
if (dist < thresholdDistance) {
shortlist.unshift({
word: dictionary[i],
distance: dist
});
if (shortlist.length > shortlistWindowSize) {
shortlist.pop();
thresholdDistance = shortlist[shortlistWindowSize-1].distance;
}
}
}
// Do a final sorting of just the top words
shortlist.sort(function(a,b){
return a.distance - b.distance;
});
// Finally, format and show the suggestions to the user
$results.html('<p>' + $.map(shortlist, function(el){
return '<span>[dist=' + el.distance + ']</span> ' + el.word;
}).slice(0,numberOfSuggestions).join('</p><p>') + '</p>').show();
});
Try the method out in this 12.000 word demo on jsFiddle

using javascript shuffled integers to populate sequential html areas

I'm trying to change the contents of sequential divs with randomly-ordered contents. It's not working so well.
I have about 25 <div> tags that, because of other javascripts regulating touch behaviour, are unable to be put in a randomised order using the usual methods. So instead, I'm trying to have the contents of those <div>s randomised by setting different innerHTML values when the document is loaded.
I have a script that provides random integers as I need, but then I'm having trouble getting it to populate the ordered divs.
Here's what I'm using that isn't working:
ArrayList<Integer> list = new ArrayList<Integer>(25);
for(int i = 0; i < 25; i++)
{
list.add(i);
}
Collections.shuffle(list);
Object[] randomNumbers = (Object[])list.toArray();
function testit() {
for(int i = 0; i < 25; i++)
{
var a = document.getElementById('if'+i);
a.innerHTML = randomNumbers[i];
}
}
What I'd like to see is that this takes all my <div>s which have id's such as "if1" "if2" and so one and then set the content of that <div> with the randomised numbers.
I know document.getElementById('if'+i) is surely the wrong way to get ID "if1" then "if2", but I'm not really sure how else it can be done.
Basically I just need a way to populate 25 absolutely-positions <div>s with 25 chunks of HTML content but in a random order. I'd think that could be done by pairing sequential numbers to randomly ordered numbers and tying those into <div> id's. Normally I'd just do this with PHP and be done already but I need it to work as an offline mobile webapp so I'm trying to do it with JS and CSS. I'm definitely open to other approaches.
edit:
If the code above is Java and not Javascript, that's the problem. Then I guess I'm looking for alternative ways, in Javascript, to produce random, non-repeating integers and link those to sequential integers, all between 1 and 25
Here is a sample implementation using javascript:
function shuffle(arr){
//simple shuffle algorithm - you can also use other
//we will get simple base index as basis for swapping elements
//we will swap the array elements arr.length times
var index = Math.floor(Math.random() * arr.length);
for(var i=1;i<arr.length; i++) {
var t = arr[i];
arr[i] = arr[index];
arr[index] = t;
}
return arr;
}
//create an array
var numbers =[];
for(var i=0;i<10;i++)
numbers.push(i);
//call the shuffle function
//call this many times to reshuffle elements
numbers = shuffle(numbers);
//document.write(numbers.join(','));
//test
for(var i=0;i<numbers.length;i++)
{
var a = document.getElementById('if'+i);
a.innerHTML = numbers[i];
}

Categories