Minimizing DOM access for jquery.append heavy site - javascript

Here's a bit of code that represents the general style I've been coding my site:
!function(){
window.ResultsGrid = Class.extend(function(){
this.constructor = function($container, options){
this.items = [];
this.$container = $($container);
this.options = $.extend({}, defaultOptions, options);
this.$grid = $(
'<div class="results-grid">\
<ul></ul>\
</div>'
)
.css("margin", -this.options.spacing / 2);
this.$list = this.$grid.find("ul");
this.$container.append(this.$grid);
};
this.setItems = function(datas) {
this.$grid.addClass("display-none");
this.clear();
for (var k in datas) this.addItem(datas[k]);
this.$grid.removeClass("display-none");
};
this.addItem = function(data) {
var width = this.options.columnWidth;
var height = this.options.rowHeight;
var padding = this.options.spacing / 2;
if (this.options.columns > 0) width = (this.$container.width() - this.options.columns * this.options.spacing) / this.options.columns;
if (this.options.rows > 0) height = (this.$container.height() - this.options.rows * this.options.spacing) / this.options.rows;
if (this.options.ratio > 0) height = width / this.options.ratio;
var item = new (this.options.class)(this.$list, {
data: data,
type: this.options.type,
width: width,
height: height,
padding: padding
});
this.items.push(item);
};
this.clear = function() {
for (var k in this.items) this.items[k].destroy();
this.items.length = 0;
};
this.destroy = function() {
this.clear();
this.$grid.find("*").off();
this.$grid.remove();
}
});
var defaultOptions = {
class: ResultsItem.Game,
type: ResultsItem.Game.COMPACT,
columns:1,
rows:0,
spacing: 10,
rowHeight: 80,
ratio: 0,
columnWidth: 0
};
}();
This is something I use for lists of items, it's just a base class so it looks fairly pointless.
On my homepage I have a few of these 'ResultsGrids' and in total I have about 100 items to be added. Each of these items calls append, addClass, css, etc. to their representative jquery object about 5 times, so that's a lot of HTML fiddling before it ever renders.
Problem is, there's quite a noticable time delay as I've just come to understand I'm accessing the DOM an unnecessary amount of times by calling methods like jquery.append for each item.
The obvious solution is to do one big append for each ResultsGrid by concatenating the html strings of each item, but I wonder if there's a middle ground between this and my current approach which will perform just as well, otherwise I'll have to rewrite a lot of code.
I like to start with a $("") and append bit by bit, but obviously this isn't good performance wise because it's constantly recalculating stuff, but I don't need to know the width, height and position of everything every step of the way. Ideally I'd like to tell it to not do anything to the DOM until I tell it to. If there's no way to do this with jquery, then I'd like a library that will allow me to do this.
I've had a brief look at js templating libraries but they don't look very enticing. Angular especially.

I don't have the big picture of what you're trying to do, but, from what I understand, you could try using jquery's $.detach() function.
this.setItems = function(datas) {
//store the parent of this grid
this.parent = this.$grid.parent();
this.$grid.detach();
this.$grid.addClass("display-none");
this.clear();
//here you add your html/etc to the grid
for (var k in datas) this.addItem(datas[k]);
//reattach the grid to the parent
this.parent.append(this.$grid);
this.$grid.removeClass("display-none");
};
Doing DOM manipulation on detached elements should be much faster and provide you with an in-between solution like you were thinking.
This is a performance tip that you could find here, along with some other useful ones.

Related

Style elements through JS efficiently - best practice

I am building a tool for creating banners at a quicker and more streamlined way, instead of creating each individually.
Doing so, I have a number of different formats (such as 300x300px, 150x600px, 970x250px) and so forth.
The projects takes the container size as input, and should style and resize all elements based on the size.
Currently, it is very inefficient styling all properties individually.
An example is outlines below, however this list grows even longer for larger/more advanced banners with multiple elements.
To the question
What is the "best practice" way of styling a lot of different elements programatically, rather than setting each property individually through the style property?
As file size is key, any 3rd party libraries are not an option.
Current (inefficient) way of doing it
I am doing it this way to reduce file size, as I cannot have the styles for say 20 formats incorporated in the same file as it would grow too large.
let bannerSize = getBannerSize(window.innerWidth, window.innerHeight);
let firstFrame = document.getElementById("#frame-1");
let firstHeading = document.getElementById("#frame-1 h1");
let firstSubHeading = document.getElementById("#frame-1 h2");
let button = document.getElementById("#frame-1 button");
let secondFrame = document.getElementById("#frame-2");
if (bannerSize == 'square') {
firstFrame.style.top = 0;
firstFrame.style.left = 0;
firstFrame.style.width = '100%';
firstHeading.style.top = 50 + 'px';
firstHeading.style.left = 50 + 'px';
firstHeading.style.fontSize = 28 + 'px';
firstSubHeading.style.top = 120 + 'px';
firstSubHeading.style.left = 50 + 'px';
firstSubHeading.style.fontSize = 18 + 'px';
} else if (bannerSize == 'portrait') {
...
} else if (bannerSize == 'landscape') {
...
}
The output would then be as follows, where the same file can be used for multiple iframes and sizes:
One one way to handle this would be through a generic function such as :
function styleElem(selector, style){
(selector) ? selector.style.cssText = style : '';
return
}
It will apply inline styles to your adslots (much more easy to read/use programmatically)
Usage :
var square = document.getElementById("#frame-1")
styleElem(square , "width:300px;height:300px;margin:50px");//apply your custom css styles
var skyscraper= document.getElementById("#frame-2")
styleElem(skyscraper, "width:150px;height:600px;margin:50px");

div slider goes beyond the borders(array length)

my div slider ignores the borders that i've made, can't find the mistakes.
P.S. right/left functions are called in tag attribute onclick:'slider.right()'. I'm just learning, I know that the elegance of the code is far from being ideal.
let elArr = [];
let pusher = elArr.push(document.querySelectorAll('#scr>div'));
let elements = Array.from(elArr[0]);
let slider = {
frame: 0,
set: function(element){
var container = document.getElementById('scr');
container = element.style.visibility='visible';
},
init: function(){
this.set(elements[this.frame]);
},
left: function(){
elements[this.frame].style.visibility='hidden';
this.frame--;
if(this.frame<0)
this.frame = elements.length - 1;
this.set(elements[this.frame]);
},
right: function(){
elements[this.frame].style.visibility='hidden';
this.frame++;
if(this.frame>elements.length)
this.frame = 0;
this.set(elements[this.frame]);
}
};
window.onload = function(){
slider.init();
}
One great guy just helped me, so there is an incorrect condition
if(this.frame>elements.length)
it drops 'frame' value, when current value is bigger then the number of elements in the array, but taking in consideration the fact, that indexation of the elements starts with 0, when frame is equal to elements.length - value becomes undefined, so frame value doesn't drop.
So, we need to change condition to a comparison == or to make it >=;
thnx to user:186999 for the decision

Maintaining relative ordering when adding nodes to a layered graph in Dagre

I have created a trivial Dagre example to dynamically add nodes upon clicking existing nodes in the graph. However, the rerendering creates a different relative ordering within the same layer. Is there anyway around this problem?
Fiddle is available here: http://jsfiddle.net/gke2dann/
Thanks in advance.
// Create a new directed graph
var g = new dagreD3.Digraph();
/* populate graph... see fiddle */
var renderer = new dagreD3.Renderer();
var layout = dagre.layout();
var render = function() {
layout.run(g);
renderer.run(g, d3.select("svg g"));
};
render();
svg.onclick = function(evt) {
var nodeId = evt.target.__data__;
for (var i = 0; i <= Math.random() * 10; ++i) {
var newNodeId = nodeId + "_sub" + i;
g.addNode(newNodeId, { label: "Bla" });
g.addEdge(null, newNodeId, nodeId);
}
render();
};
PS: Also, is there anyway to make the graph update use those fancy d3 transitions?
I found a solution to both my questions. By reducing the amount of iterations spent on minimizing the amount of edge crossings the ordering stays consistent and deterministic (but of course not ideal in all cases). A transition can be easily enabled by the designated function on the renderer.
var renderer = new dagreD3.Renderer();
// Disable the edge crossing minimization ordering
renderer.layout(dagre.layout().orderIters(-1));
// Enable a transition
renderer.transition(function(selection) {
return selection.transition().duration(500);
});
Dagre is really a great library. Updated fiddle for your enjoyment available here: http://jsfiddle.net/gke2dann/1/

jQuery - piece of code causing conflict problems on pages the selector doesn't exist

Okay so I am developing a WordPress theme. On the single post page I have a comments div which floats down the page using some jquery. I am also running a modal popup form to log in. This is completely fine on the single page when the #commentWrapper (selector for the jquery floating effect) exists. However on pages where there is no #commentWrapper to float, the modal form doesn't work. I pinned down the problem to this line in my general jQuery call (by removing each line and testing).
Call in general.js, very last call:
jQuery('#commentWrapper').stickyfloat({ duration: 300, easing : 'easeInQuad' });
Actual plugin it refers to:
$.fn.stickyfloat = function(options, lockBottom) {
var $obj = this;
var parentPaddingTop = parseInt($obj.parent().css('padding-top'));
var startOffset = $obj.parent().offset().top;
var opts = $.extend({ startOffset: startOffset, offsetY: parentPaddingTop, duration: 200, lockBottom:true }, options);
$obj.css({ position: 'absolute' });
if(opts.lockBottom){
var bottomPos = $obj.parent().height() - $obj.height() + parentPaddingTop; //get the maximum scrollTop value
if( bottomPos < 0 )
bottomPos = 0;
}
$(window).scroll(function () {
$obj.stop(); // stop all calculations on scroll event
var pastStartOffset = $(document).scrollTop() > opts.startOffset; // check if the window was scrolled down more than the start offset declared.
var objFartherThanTopPos = $obj.offset().top > startOffset; // check if the object is at it's top position (starting point)
var objBiggerThanWindow = $obj.outerHeight() < $(window).height(); // if the window size is smaller than the Obj size, then do not animate.
// if window scrolled down more than startOffset OR obj position is greater than
// the top position possible (+ offsetY) AND window size must be bigger than Obj size
if( (pastStartOffset || objFartherThanTopPos) && objBiggerThanWindow ){
var newpos = ($(document).scrollTop() -startOffset + opts.offsetY );
if ( newpos > bottomPos )
newpos = bottomPos;
if ( $(document).scrollTop() < opts.startOffset ) // if window scrolled < starting offset, then reset Obj position (opts.offsetY);
newpos = parentPaddingTop;
$obj.animate({ top: newpos }, opts.duration );
}
});
};
If I add an if command to see if the selector exists, it all works. I would like to know what the problem is for future website however.
Well, your stickyfloat() method assumes many things, like always being called on a jQuery object that contains at least one element, or that element always having a parent. For instance, consider the following code:
var $obj = this;
// ...
var startOffset = $obj.parent().offset().top;
If the jQuery object your method is called on is empty, or if its first element has no parent (the method was called on $("html")), the code will fail because parent().offset() will be null.
If you want your method to be more robust, you should not assume anything about the object it's called on. A good first step is to make the method chainable, which is always beneficial to your users and will get rid of the first problem. The recommended way of doing that is:
$.fn.stickyfloat = function(options, lockBottom) {
return this.each(function() {
var $obj = $(this);
// The rest of your code.
});
};
Since the code now runs sequentially on each element (if any) through an anonymous function, testing for the parent element's existence can be dealt with by returning early:
var $obj = $(this);
var $parent = $obj.parent();
if (!$parent.length) {
return; // No parent, continue with next element, if any.
}
// Parent element is safe to use.
var parentPaddingTop = parseInt($parent.css('padding-top'));
var startOffset = $parent.offset().top;

Is There An Efficient jQuery Hittest?

I am creating a simple game with HTML, CSS, and JavaScript (jQuery). There is main ship, where all of the particles (bullets) originate from. They are each just divs. Then, enemy divs are places randomly throughout the screen.
I am looking for an efficient way to test if each particle is hitting a particular enemy. I have something that starts to work out fine, but gets bogged down incredibly fast. I am new to js, so my code is pretty messy and probably inefficient in many other ways. Any suggestions would be greatly appreciated!
Here is my section that creates enemies and tests for hitting:
var createEnemy = function(){
var xRandom = Math.floor(Math.random() * (containerW-50));
var yRandom = Math.floor(Math.random() * (containerH-50));
var newEnemy = $('<div class="enemy"></div>');
$(newEnemy).css({'left':xRandom,'top':yRandom}).appendTo('#container').fadeTo(200, .7);
var hitTest = setInterval(function(){
var enemy = $(newEnemy);
var particle = $('.particle');
var enemyT = enemy.offset().top;
var enemyB = enemy.height()+enemyT;
var enemyL = enemy.offset().left;
var enemyR = enemy.width()+enemyL;
var particleT = particle.offset().top;
var particleB = particle.height();
var particleL = particle.offset().left;
var particleR = particle.width();
if(particleT >= enemyT-particleB && particleT <= enemyB && particleL >= enemyL-particleR && particleL <= enemyR){
enemy.hide();
var removeEnemy = setTimeout(function(){
newEnemy.remove();
clearInterval(hitTest, 0);
},500);
}
}, 20);
}
var enemyInt = setInterval(createEnemy, 1000);
Is getting something like this to run smoothly in a browser realistic? Does my code just need some changes? You will probably need more context so:
EDIT 1/12/2012: Game Link Removed // Not Relevant
NOTE: This works best in Chrome and Safari at the moment.
EDIT 3/22/2011: Changed enemy fadeOut() to hide() so that you can see exactly when an enemy disappears (it is sometimes delayed). The hitTest only seems to trigger when you click on the actual enemy, so if it passes through, it is not being triggered.Also, I forgot to clearInterval on hitTest. This seemed to boost performance dramatically, but still isn't quite there.
If you want the best performance, drop jQuery and use native JavaScript.
If that isn't an option, profile the slowest parts and use native DOM there, e.g.
var newEnemy = $('<div class="enemy"></div>');
...becomes...
var newEnemy = document.createElement('div');
newEnemy.className = 'enemy';

Categories

Resources