Why is this code so slow? - javascript

jsc.tools.road.correctType = function() {
for(row = jsc.data.selection.startX - 1; row <= jsc.data.selection.endX + 1; row++) {
for(col = jsc.data.selection.startY - 1; col <= jsc.data.selection.endY + 1; col++) {
if(jsc.data.cells[row-1][col].type != "road" && jsc.data.cells[row+1][col].type != "road" && jsc.data.cells[row][col].type == "road") {
jsc.ui.addClassToCell("horz", row, col);
}
else {
jsc.ui.removeClassFromCell("horz", row, col);
}
if(jsc.data.cells[row][col-1].type != "road" && jsc.data.cells[row][col+1].type != "road" && jsc.data.cells[row][col].type == "road") {
jsc.ui.addClassToCell("vert", row, col);
}
else {
jsc.ui.removeClassFromCell("vert", row, col);
}
}
}
};
// Elsewhere
jsc.ui.addClassToCell = function(class, x, y) {
$("#" + x + "-" + y).addClass(class);
};
jsc.ui.removeClassFromCell = function(class, x, y) {
$("#" + x + "-" + y).removeClass(class);
};
The code above runs very slowly. I can't figure out why. It's using jQuery 1.3.2. Any way to optimize it a bit?
EDIT: The code is part of a javascript game I am making as a personal project. It's basically a Simcity clone. This piece of code checks the neighbouring cells for each part of the road, and changes the class (and in turn the background image) to the correct one to make the road images line up right, e.g. horizontal, vertical and junction(no class) road images.
EDIT 2: A few more details to provide some context.
The jsc.data.cells is an 200 x 200 array. Each array element is an object with properties like so (default shown): {type: null, developed: false, powered: false, watered: false, hasTransport: false, wealth: 0, quality: 0} .
It's counterpart is in the UI, which is basically a giant table. (200 x 200 again). Each cell has a number of CSS classes added to it throughout the program to change the background image (e.g. .road to change it to road, .com.developed to make it a developed commercial zone). The table cells each have an id of the form #x-y which is what jsc.ui.addClassToCell, and jsc.ui.removeClassFromCell edit.
EDIT 3: Fixed the IDs starting with numbers. Trying out some of the optimizations now.

A short estimate using O() notation:
for(row) ... O(N)
for(col) ... O(N)
$().addClass/removeClass ... O(N^2)
the $() is even called twice within the nested for.
so you end up with O(N^4)
You can optimize this by caching the calculated classes in the as property of jsc.data.cells[row][col], e.g.
jsc.data.cells[row][col].horz = 1; // don't set class "horz" if not present
jsc.data.cells[row][col].vert = 1;
and use the cached data when you create the cells inside the HTML table, rather than calling $() for each cell.

Normally you can significantly optimize loops like these;
for( var x = 0; x < someMethod(); x++ ) {
//... do stuff
}
By exchanging them out with something like this
var x = someMethod();
while( x-- ) {
//...do stuff
}
Though it becomes slightly different semantically, it normally works quite well as long as you're not dependent upon order in your looping (order is opposite)
Even when you cannot change the order, you will also significantly improve your code by merely moving the someMethod call OUT of your actual loop, since in many JS implementations it will be called once for every iteration...

Depending on the size of your selection, you might be doing a whole lot of condition checks and DOM edits.
By commenting out the content of addClassToCell and removeClassFromCell and comparing run times you can find out whether the condition checking or the dom editing takes the most time and thus which one is the best candidate for optimising.

I can only give some tips, but don't know if they help much. Have no possibility to test your code.
1-st: declare variables in local function scope. I mean the row and col variables, which you declared as global (missing var statement). Access to global variables takes longer (AFAIK) than to local scope vars.
var row = jsc.data.selection.startX-1;
var col = jsc.data.selection.startY-1;
2-nd: cache references to common objects. Here, you can store reference for jsc.data and/ord jsc.data.selection and jsc.data.cells. IIRC, the access to an object property is linear.
jsc.tools.road.correctType = function() {
var data = jsc.data, selection = data.selection, cells = jsc.data.cells, ui.jsc.ui;
for(var row = selection.startX - 1, endX = selection.endX + 1, endY = selection.endY + 1; row <= endX; ++row) {
for(var col = selection.startY - 1; col <= endY; ++col) {
if(cells[row-1][col].type != "road" && cells[row+1][col].type != "road" && cells[row][col].type == "road") {
ui.addClassToCell("horz", row, col);
} else {
ui.removeClassFromCell("horz", row, col);
}
if(cells[row][col-1].type != "road" && cells[row][col+1].type != "road" && cells[row][col].type == "road") {
ui.addClassToCell("vert", row, col);
} else {
ui.removeClassFromCell("vert", row, col);
}
}
}
};
I also moved the declaration of endY variable to the outer loop, so it won't be computed with every access to inner loop.
-- edit
hope you know, that ID attribute values cannot start with a number, like you have, eg. #2-3

Use a memoizer or a local cache to store the jQuery objects you have already created. That will reduce the numer of calls of the $ function.
var cache = {}, selector;
for (/* … */) {
selector = "#" + x + "-" + y;
if (!cache[selector]) {
cache[selector] = $(selector);
}
// cache[selector] refers to the same object as $("#" + x + "-" + y)
}

Related

Changing an element's className via onClick in React triggers a rerender, but conditional className construction does not?

My app is using an array of div elements to display a grid. The array is stored in Context so other components can access the data but I'm not sure if that matters in this case. I have an onClick function that changes the CSS class of a clicked-on element, so that it has a colored background. This works fine, and toggles properly.
I want to remove all selections if the grid is changed to a smaller size. I tried a few approaches and got some strange behavior as a result, culminating in some very confusing behavior from this code:
selectTile(tilesArray, event) {
event.preventDefault();
let tempArray = [...tilesArray];
if(event.target.className === "grid-tile"){
event.target.className = "grid-tileb";
tempArray.push(event.target.id);
} else {
event.target.className = "grid-tile";
tempArray.splice(tempArray.indexOf(event.target.id),1);
}
this.setState({selectedTiles: tempArray})
}
makeGrid(x, y, tilesize, visible, hex){
const gridsDataArray = JSON.parse(localStorage.getItem('grids'));
const index = JSON.parse(localStorage.getItem('currentGrid'));
let clearSelection = false;
if(x < gridsDataArray[index].dims[0] || y < gridsDataArray[index].dims[1]){
clearSelection = true;
}
let columnStr = "";
let tileArray = [];
const widthStr = tilesize.toString() + "px"
if(clearSelection){
this.setState({selectedTiles:[]})
}
for (let i = 0; i < y; i++) {
for (let j = 0; j < x; j++) {
if(i===0) columnStr = columnStr + "auto ";//x loops over columns so this runs once for all columns.
let div = (
<div
id={"x" + j.toString() + "y" + i.toString()}//for example at coordinates 5,6 id is x5y6. starts at 0.
key={"x" + j.toString() + "y" + i.toString()}
className={(this.state.selectedTiles.indexOf("x" + j.toString() + "y" + i.toString()) < 0 ?
"grid-tile" :
"grid-tileb")}
style={{
width: widthStr,
height: widthStr,
border: "1px solid rgba(0, 0, 0," + (visible ? "0.6)" : "0.0)")
}}
onClick={(event) => this.selectTile(this.state.selectedTiles, event)}
>
</div>
)
tileArray.push(div);
}
}
let iColumnStr = "";
for (let i= 0; i < 330/tilesize; i++){
iColumnStr = iColumnStr + "auto ";
}
return {
columns: columnStr,
imageColumns: iColumnStr,
tiles: tileArray,
name: gridsDataArray[index].name,
bgurl: gridsDataArray[index].bgurl
};
}
If I select some tiles, then make the grid smaller, the previously selected tiles remain colored, but the selectedTiles array is empty. If I change the grid again in any way, the selected tiles lose the coloring. So okay, maybe that's because setState is async and the conditional statement is using the old state, right? Well, I tried className={(clearSelection || this.state.selectedTiles.indexOf("x" + j.toString() + "y" + i.toString()) < 0 ? instead and that doesn't work. In fact, it works LESS-- the selected tiles never lose coloring even after changing the grid twice. That scenario also happens if I put
if(clearSelection){
this.setState({selectedTiles:[]})
}
after the for loop instead of before it. That seems to indicate that it's NOT an async problem, as well. Plus, if I reduce the dimensions of the grid so that the colored elements are not part of the grid anymore, they're properly removed, and uncolored elements are added once I make the grid bigger again as you'd expect.
Why are my elements not rerendering properly? I'm not even sure why selectTile triggers a rerender in the first place, since if I remove the setState line from it, it still works. I don't need any conditional className construction in the for loop, either.
I think I can circumvent this issue entirely by running a loop across the selectedTiles array after(or before) I call makeGrid and directly changing the class names that way instead of from inside makeGrid, but I'd still like to know why this behavior occurs.
edit: Can't change the class names by retrieving from the tile array via the coordinates contained in the selectedTiles array(though the code to do so was fun to figure out); JSX objects don't like to be manipulated directly. Instead, I ran a loop across selectedTiles and only removed coordinates that were outside the new grid. If the tiles are outside the grid they are fully reset anyway when the grid is made larger again, so the only leftover data is in selectedTiles.
Okay, I think I found the issue with your code here.
You tried it to make it as state independent as you can, and that caused the issue here.
Firstly, I included your grid metadata to state and assigned it to the starting values from localStorage, so that it is accessible from inside the Grid
this.state = {
x: topGridData.dims[0],
y: topGridData.dims[1],
tilesize: topGridData.dims[2],
visible: topGridData.visible,
hex: topGridData.hex,
selectedTiles: [],
...this.makeGrid(topGridData.dims[0], topGridData.dims[1], topGridData.dims[2],
topGridData.visible, topGridData.hex)
};
Then I modified your setGrid function, so that it sets grid values as soon as they change
setGrid(x, y, tilesize, visible, hex) {
this.setState({
x,
y,
tilesize,
visible,
hex,
...this.makeGrid(x, y, tilesize, visible, hex)
});
}
And now we go for the faulty function that was causing the problem. The problem with this function, was that it was setting className via the DOM, outside React, so React was not able to react (pun not intented) properly to the element change.
So firstly I got rid of these two as they are not needed:
event.target.className = "grid-tileb";
event.target.className = "grid-tile";
Then I've attached a callback to a setState, so that Grid is rebuilt properly, everytime your selectedTiles array is changed
this.setState({
selectedTiles: tempArray
}, () => this.setState(this.makeGrid(this.state.x, this.state.y, this.state.tilesize, this.state.visible, this.state.hex)));
And this is how the whole selectTile function looks like:
selectTile(tilesArray, event) {
event.preventDefault();
let tempArray = [...tilesArray];
if (event.target.className === "grid-tile") {
tempArray.push(event.target.id);
} else {
tempArray.splice(tempArray.indexOf(event.target.id), 1);
}
this.setState({
selectedTiles: tempArray
}, () => this.setState(this.makeGrid(this.state.x, this.state.y, this.state.tilesize, this.state.visible, this.state.hex)))
}
I left your clearing of the state, because it is important here, selectedTiles from state must be cleared
if (clearSelection) {
this.setState({
selectedTiles: []
});
}
The last thing that I added is new condition that is actually taken from your original code
className = {
(clearSelection || this.state.selectedTiles.indexOf("x" + j.toString() + "y" + i.toString()) < 0 ?
"grid-tile" :
"grid-tileb")
}
And that's all, it works as you originally intented!.

Identifying edge cases of a one-dimensional array in Javascript

I'm creating a 2-dimensional heat map which has functionality when you click on any pixel. It grabs data associated with the index of every pixel (including adjacent pixels) and plots it. It currently looks like this:
The problem that I'm encountering is when I click on a left or right edge pixel, since it grabs data from adjacent pixels, it can retrieve data from the opposite side of the graph since it is all within a one-dimensional array. I am trying to create a conditional which checks if the clicked pixel is an edge case, and then configures the magnified graph accordingly to not show points from the other side of the main graph. This is the code I have so far:
// pushes all dataMagnified arrays left and right of i to magMainStore
var dataGrabber = function(indexGrabbed, arrayPushed) {
// iterates through all 5 pixels being selected
for (var b = -2; b <= 2; b++) {
var divValue = toString(i / cropLength + b);
// checks if selected index exists, and if it is not in the prior row, or if it is equal to zero
if (dataMagnified[indexGrabbed + b] != undefined && (& divValue.indexOf(".")!=-1)) {
dataMagnified[indexGrabbed + b].forEach(function(z) {
arrayPushed.push(z);
})
}
}
};
I am trying to get the same result as if I had a two dimensional array, and finding when the adjacent values within a single array is undefined. This is the line where I'm creating a conditional for that
if (dataMagnified[indexGrabbed + b] != undefined && (& divValue.indexOf(".")!=-1)) {
The second condition after the and is my attempts so far trying to figure this out. I'm unsure if I can even do this within a for loop that iterates 5 times or if I have to create multiple conditions for this. In addition, here's an image displaying what I'm trying to do:
Thank you!
Your approach looks overly complex and will perform rather slowly. For example, converting numbers to strings to be able to use .indexOf() to find a decimal point just for the sake of checking for integer numbers doesn't seem right.
A much simpler and more elegant solution might be the following function which will return the selection range bounded by the limits of the row:
function getBoundedSelection(indexGrabbed, selectionWidth) {
return dataMagnified.slice(
Math.max(Math.floor(indexGrabbed/cropLength) * cropLength, indexGrabbed - selectionWidth),
Math.min(rowStartIndex + cropLength, indexGrabbed + selectionWidth)
);
}
Here, to keep it as flexible as possible, selectionWidth determines the width of the selected range to either side of indexGrabbed. This would be 2 in your case.
As an explanation of what this does, I have broken it down:
function getBoundedSelection(indexGrabbed, selectionWidth) {
// Calculate the row indexGrabbed is on.
var row = Math.floor(indexGrabbed/cropLength);
// Determine the first index on that row.
var rowStartIndex = row * cropLength;
// Get the start index of the selection range or the start of the row,
// whatever is larger.
var selStartIndex = Math.max(rowStartIndex, indexGrabbed - selectionWidth);
// Determine the last index on that row
var rowEndIndex = rowStartIndex + cropLength;
// Get the end index of the selection range or the end of the row,
//whatever is smaller.
var selEndIndex = Math.min(rowEndIndex, indexGrabbed + selectionWidth);
// Return the slice bounded by the row's limits.
return dataMagnified.slice(selStartIndex, selEndIndex);
}
So I discovered that since the results of the clicked position would create a variable start and end position in the for loop, the only way to do this was as follows:
I started the same; all the code is nested in one function:
var dataGrabber = function(indexGrabbed, arrayPushed) {
I then create a second function that takes a start and end point as arguments, then passes them as the for loop starting point and ending condition:
var magnifyCondition = function (start, end) {
for (var b = start; b <= end; b++) {
if (dataMagnified[indexGrabbed + b] != undefined) {
dataMagnified[indexGrabbed + b].forEach(function (z) {
arrayPushed.push(z);
})
}
}
};
After that, I created 5 independent conditional statements since the start and end points can't be easily iterated through:
if (((indexGrabbed - 1) / cropLength).toString().indexOf(".") == -1) {
magnifyCondition(-1, 2);
}
else if ((indexGrabbed / cropLength).toString().indexOf(".") == -1) {
magnifyCondition(0, 2);
}
else if (((indexGrabbed + 1) / cropLength).toString().indexOf(".") == -1) {
magnifyCondition(-2, 0);
}
else if (((indexGrabbed + 2) / cropLength).toString().indexOf(".") == -1) {
magnifyCondition(-2, 1);
}
else {
magnifyCondition(-2, 2);
}
};
Lastly, I pass the index grabbed (i of the on clicked function) and an arbitrary array where the values get stored.
dataGrabber(i, magMainStore);
If there's a better way instead of the if statements, please let me know and I'd be happy to organize it better in the future!

Merge table rows based on row text and class

I have found this function here and it almost perfectly suits my needs. The function iterates through the first column of a table, and row merges similar values. Then, it iterates through the next column, and row merges those values as well, while taking into account the merges of the column before it. In this way, it perfectly suits what I need.
function MergeCommonRows2(table, firstOnly) {
var firstColumnBrakes = [];
for (var i = 1; i <= table.find('th').length; i++) {
var previous = null, cellToExtend = null, rowspan = 1;
table.find("td:nth-child(" + i + ")").each(function (index, el) {
if (previous == $(el).text() && $(el).text() !== "" && $.inArray(index, firstColumnBrakes) === -1) {
$(el).addClass('hidden');
cellToExtend.attr("rowspan", (rowspan = rowspan + 1));
} else {
if (firstOnly == 'first only') {
if (i === 1)
firstColumnBrakes.push(index);
} else {
if ($.inArray(index, firstColumnBrakes) === -1)
firstColumnBrakes.push(index);
}
rowspan = 1;
previous = $(el).text();
cellToExtend = $(el);
}
});
}
}
However, I wish to edit the function to consider classes as well. In other words, I wish to edit the function to first check the classes of the two rows being merged. And, even if they have the same value, if they have different classes, to NOT merge the rows. I have tried to edit the above function to do as much, but to no avail.
UPDATE
My attempts to edit the code were quite crude. Essentially, I put code before he if statement that adds a hidden class and said, to summarize, if previous.getClass = current.getClass, only then go onto the if statement that hides the class.
Here is the fiddle. I want to click the merge button and, since the "click me" cells have a different classes between them, I don't want them to merge.

Prevent touching corners (JS Game)

How can I prevent this map generator from creating touching corners like this:
-X
X-
Or
X-
-X
Here is a simplified example of the generator: http://jsfiddle.net/fDv9C/2/
Your question answers itself, almost.
Here's the fiddle: http://jsfiddle.net/qBJVY/
if (!!grid[y][x] && !!grid[y+1][x+1] && !grid[y+1][x] && !grid[y][x+1]) {
good=false;
grid[y+1][x]=2;
}
It simply checks for the combinations you do not want and patches them up. It always adds a grid point so as not to disconnect any parts of the map.
This in turn may lead to another situation where the issue may occur, but if it changed anything (that is, if it found a problem), it will simply check again. This can be optimized, for instance by recursively adjusting whatever was changed, but usually it only needs 1 or 2 passes. There's a limiter on there to not allow more than 100 passes, just in case there is some unforeseen circumstance in which it cannot fix it (I can't think of such a situation, though :) ).
Because of the way that you are creating board it's very difficulty to do this checking during generation. I create simple function that check board after. It's using flood algorithm. Here is the fiddle http://jsfiddle.net/jzTEX/8/ (blue background is original map, red background is map after checking)
Basically we create second array grid2. After filling grid we run recursively floodV function
function floodV(x,y) {
var shiftArray = [[0,1],[0,-1],[1,0],[-1,0]];
grid2[y][x]=1;
for(var k=0;k<4;k++) {
var x1=x+shiftArray[k][0];
var y1=y+shiftArray[k][1];
if(grid[y1][x1] == 1 && grid2[y1][x1] == 0 && checkV(x1,y1)) {
grid2[y1][x1] = 1;
floodV(x1,y1);
}
}
}
with the check function
function checkV(x,y) {
var checkVarr = [[-1,-1], [-1,1], [1,1], [1,-1]];
for(var k=0;k<4;k++) {
if(grid[y+checkVarr[k][0]][x+checkVarr[k][1]] == 1 && grid[y+checkVarr[k][0]][x] == 0 && grid[y][x+checkVarr[k][1]] == 0 && grid2[y+checkVarr[k][0]][x+checkVarr[k][1]] == 1)
return false;
}
return true;
}
This isn't perfect because we can sometimes throw away big parts of the map but if we try to start adding new elements we have to check whole map again (in worths case).
This is what I did: http://jsfiddle.net/fDv9C/13/
Where's the magic happening? Scroll down to lines 53 through 58:
var bottom = y_next + 1;
var left = x_next - 1;
var right = x_next + 1;
var top = y_next - 1;
if (grid[top][left] || grid[top][right] ||
grid[bottom][left] || grid[bottom][right]) continue;
In short your touching corner points can only occur at the computed next position. Hence if any one of the four corner neighbors of the next position exists, you must compute another next position.
You may even decrement the counter i when this happens to get as many paths as possible (although it doesn't really make a big difference):
var bottom = y_next + 1;
var left = x_next - 1;
var right = x_next + 1;
var top = y_next - 1;
if (grid[top][left] || grid[top][right] ||
grid[bottom][left] || grid[bottom][right]) {
i--;
continue;
}
See the demo here: http://jsfiddle.net/fDv9C/12/
Edit: I couldn't resist. I had to create an automatic map generator so that I needn't keep clicking run: http://jsfiddle.net/fDv9C/14/

More efficient comparison of numbers

I have an array which is part of a small JS game I am working on I need to check (as often as reasonable) that each of the elements in the array haven't left the "stage" or "playground", so I can remove them and save the script load
I have coded the below and was wondering if anyone knew a faster/more efficient way to calculate this. This is run every 50ms (it deals with the movement).
Where bots[i][1] is movement in X and bots[i][2] is movement in Y (mutually exclusive).
for (var i in bots) {
var left = parseInt($("#" + i).css("left"));
var top = parseInt($("#" + i).css("top"));
var nextleft = left + bots[i][1];
var nexttop = top + bots[i][2];
if(bots[i][1]>0&&nextleft>=PLAYGROUND_WIDTH) { remove_bot(i); }
else if(bots[i][1]<0&&nextleft<=-GRID_SIZE) { remove_bot(i); }
else if(bots[i][2]>0&&nexttop>=PLAYGROUND_HEIGHT) { remove_bot(i); }
else if(bots[i][2]<0&&nexttop<=-GRID_SIZE) { remove_bot(i); }
else {
//alert(nextleft + ":" + nexttop);
$("#" + i).css("left", ""+(nextleft)+"px");
$("#" + i).css("top", ""+(nexttop)+"px");
}
}
On a similar note the remove_bot(i); function is as below, is this correct (I can't splice as it changes all the ID's of the elements in the array.
function remove_bot(i) {
$("#" + i).remove();
bots[i] = false;
}
Many thanks for any advice given!
Cache $("#" + i) in a variable; each time you do this, a new jQuery object is being created.
var self = $('#' + i);
var left = parseInt(self.css("left"));
var top = parseInt(self.css("top"));
Cache bots[i] in a variable:
var current = bots[i];
var nextleft = left + current[1];
var nexttop = top + current[2];
Store (cache) the jQuery object of the DOM element within the bot representation. At the moment it's been created every 50ms.
What I mean by this is that for every iteration of the loop, you're doing $('#' + i). Every time you call this, jQuery is building a jQuery object of the DOM element. This is far from trivial compared to other aspects of JS. DOM traversal/ manipulation is by far the slowest area of JavaScript.
As the result of $('#' + i) never changes for each bot, why not store the result within the bot? This way $('#' + i) gets executed once, instead of every 50ms.
In my example below, I've stored this reference in the element attribute of my Bot objects, but you can add it your bot (i.e in bots[i][3])
Store (cache) the position of the DOM element representing the bot within the bot representation, so the CSS position doesn't have to be calculated all the time.
On a side note, for (.. in ..) should be strictly used for iterating over objects, not arrays. Arrays should be iterated over using for (..;..;..)
Variables are extremely cheap in JavaScript; abuse them.
Here's an implementation I'd choose, which incorporates the suggestions I've made:
function Bot (x, y, movementX, movementY, playground) {
this.x = x;
this.y = y;
this.element = $('<div class="bot"/>').appendTo(playground);
this.movementX = movementX;
this.movementY = movementY;
};
Bot.prototype.update = function () {
this.x += this.movementX,
this.y += this.movementY;
if (this.movementX > 0 && this.x >= PLAYGROUP_WIDTH ||
this.movementX < 0 && this.x <= -GRID_SIZE ||
this.movementY > 0 && this.y >= PLAYGROUND_HEIGHT ||
this.movementY < 0 && this.y <= -GRIDSIZE) {
this.remove();
} else {
this.element.css({
left: this.x,
right: this.y
});
};
};
Bot.prototype.remove = function () {
this.element.remove();
// other stuff?
};
var playground = $('#playground');
var bots = [new Bot(0, 0, 1, 1, playground), new Bot(0, 0, 5, -5, playground), new Bot(10, 10, 10, -10, playground)];
setInterval(function () {
var i = bots.length;
while (i--) {
bots[i].update();
};
}, 50);
You're using parseInt. As far as I know, a bitwise OR 0 is faster than parseInt. So you could write
var left = $("#" + i).css("left") | 0;
instead.
Furthermore, I wouldn't make use of jQuery functions to obtain values like these every 50 ms, as there's always a bit more overhead when using those (the $ function has to parse its arguments, etc.). Just use native JavaScript functions to optimize these lines. Moreover, with your code, the element with id i has to be retrieved several times. Store those elements in a variable:
var item = document.getElementById(i);
var iStyle = item.style;
var left = iStyle.left;
…
(Please note that I'm not a jQuery expert, so I'm not 100% sure this does the same.)
Moreover, decrementing while loops are faster than for loops (reference). If there's no problem with looping through the elements in reverse order, you could rewrite your code to
var i = bots.length;
while (i--) {
…
}
Use offset() or position() depending on if you need coordinates relative to the document or the parent. position() is most likely faster since browsers are efficient at finding offsets relative to the parent. There's no need for parsing the CSS. You also don't need the left and top variables since you only use them once. It may not be as readable but you're going for efficiency:
var left = $("#" + i).position().left + bots[i][1];
var top = $("#" + i).position().top + bots[i][2];
Take a look here for a great comparison of different looping techniques in javascript.
Using for...in has poor performance and isn't recommended on arrays. An alternative to looping backwards and still using a for loop is to cache the length so you don't look it up with each iteration. Something like this:
for(var i, len = bots.length; i < len; i++) { ... }
But there are MANY different ways, as shown in the link above and you might want to test several with your actual application to see what works best in your case.

Categories