createElement, appendChild performance worst in loop - javascript

I have a A-Tag with a innerText and want to replace that with 2 new Tags, one DIV-Tag with the text of the A's innerText and the other tag is a 'I'-Tag which is appended after the DIV-Tag:
before:
<a>hello world</>
after:
<a>
<div>hello world</div>
<i></i>
</a>
I have two alternatives which both works:
1. Way:
var items = document.querySelectorAll('#mytags a');
for (var i = 0; i < items.length; i++) {
node_a = items[i];
var txt_a = node_a.innerText;
var txt_span = document.createElement('div');
txt_span.innerHTML = txt_a;
node_a.innerText = '';
node_a.appendChild(txt_span);
var icon_node = document.createElement('i');
node_a.appendChild(icon_node);
}
2. Way:
var items = document.querySelectorAll('#mytags a');
[].forEach.call(items, function(a) {
var txt_a = a.innerText;
var txt_span = document.createElement('div');
txt_span.innerHTML = txt_a;
a.innerText = '';
a.appendChild(txt_span);
var icon_node = document.createElement('i');
a.appendChild(icon_node);
});
Both works, however, the performance is really bad when having only about 50 A-Tags. How can I make this faster?
For example, when rendering in Safari or Firefox, there is a time lag of 1-2 seconds when rendering the content of the I-tags. The content of the I-Tag is associated by a css class:
#mytags a>i:after{
font-family: 'FontAwesome';
content: '\f054';
position: absolute;
height: 100%;
right: 0;
top: 0;
width: 20px;
}
The question is, are my loops the fastest way or what is the reason for the time lag? I have no more than 50 A-Tags.

Related

How to add hundreds of shapes on page without slowing page load

I am trying to add hundreds of little "squares"/shapes on a page using JS. However, whether using SVG or divs, the page load is very slow. Is there a more efficient way to create multiple shapes on a page without slowing down the page load?
Here is an example in JSFiddle, which has both svg and div examples
Here is the JS:
var num = 700
for (i=0; i < num; i++){
let el = '<div class="els"></div>';
let elSVG = '<svg class="els"></svg>';
let container = document.getElementById("test");
container.innerHTML = container.innerHTML + elSVG
}
Instead of concatenating HTML text to the innerHTML each time, append an <svg> element. Also, you should only query for #test (aka container) once; outside of your loop.
const
container = document.getElementById('test'),
num = 700;
const createSvg = () => {
const svg = document.createElement('SVG');
svg.classList.add('els');
return svg;
};
for (let i = 0; i < num; i++) {
container.append(createSvg());
}
body {
background-color: #111
}
.els {
display: inline-block;
width: 10px;
height: 10px;
margin-right: 16px;
background-color: #EEE;
}
<div id="test"></div>
Update: As Danny mentioned, you could append all the SVG elements to a DocumentFragment and then append said fragment to the container afterwards.
const fragment = new DocumentFragment();
for (let i = 0; i < num; i++) {
fragment.append(createSvg());
}
container.append(fragment);
You will always slow page load, it can not be done without slowing down.
But you can be smart in creating content.
innerHTML and append will trigger Browser reflow/repaint for every insertion
Use a DocumentFragment to built all HTML in memory, then inject the DocumentFragment once.
https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment
You might also want to look into <template>,
a cloned template parses the HTML only once
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template
<style>
body {
background-color: black
}
.els {
height: 2px;
width: 1px;
background-color: white;
margin-right: 1px;
display: inline-block;
}
</style>
<div id="$Container">
</div>
<script>
console.time();
let fragment = new DocumentFragment();
let num = 4 * 700;
for (i = 0; i < num; i++) {
let el = document.createElement("div");
el.classList.add("els");
el.appendChild(document.createElement("svg"))
.classList.add("els");
fragment.append(el);
}
$Container.append(fragment);
console.timeEnd();
</script>

Reducing dimensions on each iteration

I got this thing over here:
function duplicateElement() {
const holder = document.querySelector('.holder');
let elements = document.querySelectorAll('.item');
let firstEl = elements[0];
//let lastEl = elements[elements.length - 1];
console.log(elements.length);
let dimensions = firstEl.offsetWidth;
let reducedDimensions = dimensions / 1.45;
elements.forEach(element => {
let duplicate = element.cloneNode(true);
holder.appendChild(duplicate);
element.style.width = `${reducedDimensions}px`;
element.style.height = `${reducedDimensions}px`;
});
}
let callCount = 1;
let repeater = setInterval(function() {
if (callCount < 5) {
duplicateElement();
callCount += 1;
} else {
clearInterval(repeater);
}
}, 5000);
.bg-black {
background: black;
}
.item {
border-radius: 50%;
}
.square::after content: "";
display: block;
padding-bottom: 100%;
}
<div class="holder">
<div class="bg-black square item"></div>
</div>
What this does is that it runs the function duplicateElements n times.
In duplicateElements() i double the number of elements in each iteration. The parent starts with just 1 item, which become 2, 4, 8, 16, etc (depending on callCount). This works as i want.
Additionally, i want to reduce the size of every element. So i check for the dimensions of the first item, divide the number by a factor and apply the new dimensions as width and height to every item.
That however does not work, as it applies the new dimensions only to the newly duplicated items, leaving the items created in the previous iteration with their old values. (You can check this in the console, as it applies that values as well)
What i am missing here? My train of thought was that since i check for all items with let elements = document.querySelectorAll('.item') anew in EVERY ITERATION, i would also apply my new dimensions to each element.
console.log(elements.length); gives me the correct number of items in each iteration so why don't the dimensions apply?
Hey, thanks!
The problem is: you select all elements before the loop in
let elements = document.querySelectorAll('.item');
Which is correct, but then inside the loop you duplicate the current element and append it to the DOM before setting the dimensions, so this duplicated element is not getting the new dimensions before beign appended...
So, the solution is: just move the part that duplicate the element to below the part that set the dimensions, see below, inside the loop (I reduced to 1 second to faster example)
function duplicateElement() {
const holder = document.querySelector('.holder');
let elements = document.querySelectorAll('.item');
let firstEl = elements[0];
console.clear()
console.log(elements.length);
let dimensions = firstEl.offsetWidth;
let reducedDimensions = dimensions / 1.45;
elements.forEach(element => {
element.style.width = `${reducedDimensions}px`;
element.style.height = `${reducedDimensions}px`;
let duplicate = element.cloneNode(true);
holder.appendChild(duplicate);
});
}
let callCount = 1;
let repeater = setInterval(function() {
if (callCount < 5) {
duplicateElement();
callCount += 1;
} else {
clearInterval(repeater);
}
}, 1000);
.bg-black {
background: black;
}
.item {
border-radius: 50%;
}
.square::after {
content: "";
display: block;
padding-bottom: 100%;
}
<div class="holder">
<div class="bg-black square item"></div>
</div>
Also, I fixed the CSS, there was missing a {

Javascript for onclick function

I'm trying to practice my scripting by making a Battleship game. As seen here.
I'm currently trying to make the board 2D. I was able to make a for-loop in order to make the board, however, due to testing purposes, I'm just trying to make the board, upon clicking a square, it turns red... But, the bottom square always lights up. I tried to debug it by making the c value null, but then it just stops working. I know it's not saving the c value properly, but I'm wondering how to fix this.
Do I have to make 100 squares by my self or can I actually have the script do it?
maincansize = 400;
document.getElementById("Main-Canvas").style.height = maincansize;
document.getElementById("Main-Canvas").style.width = maincansize;
document.getElementById("Main-Canvas").style.position = "relative";
var ize = maincansize * .1;
for (var a = 0; a < 10; a++) {
for (var b = 0; b < 10; b++) {
var c = document.createElement("div");
var d = c;
c.onclick = function() {
myFunction()
};
function myFunction() {
console.log("A square was clicked..." + c.style.top); d.style.backgroundColor = "red";
}
c.style.height = ize;
c.style.width = ize;
c.style.left = b * ize;
c.style.top = a * ize;
c.style.borderColor = "green";
c.style.borderStyle = "outset";
c.style.position = "absolute";
console.log(ize);
document.getElementById('Main-Canvas').appendChild(c);
} //document.getElementById('Main-Canvas').innerHTML+="<br>";
}
#Main-Canvas {
background-color: #DDDDDD;
}
<div>
<div id="header"></div>
<script src="HeaderScript.js"></script>
</div>
<div id="Main-Canvas" style="height:400;width:400;">
</div>
Here's your code with some fixes:
adding 'px' to style assignment
passing the clicked element to myFunction
var maincansize = 400;
document.getElementById("Main-Canvas").style.height = maincansize;
document.getElementById("Main-Canvas").style.width = maincansize;
document.getElementById("Main-Canvas").style.position = "relative";
var ize = maincansize * .1;
for (var a = 0; a < 10; a++) {
for (var b = 0; b < 10; b++) {
var c = document.createElement("div");
c.onclick = function(ev) {
myFunction(ev.currentTarget);
};
function myFunction(el) {
console.log("A square was clicked...");
el.style.backgroundColor = "red";
}
c.style.height = ize+'px';
c.style.width = ize+'px';
c.style.left = (b * ize)+'px';
c.style.top = (a * ize)+'px';
c.style.borderColor = "green";
c.style.borderStyle = "outset";
c.style.position = "absolute";
document.getElementById('Main-Canvas').appendChild(c);
}
}
#Main-Canvas {
background-color: #DDDDDD;
}
<div id="Main-Canvas" style="height:400;width:400;">
</div>
Here's a solution with major revamps. Since you're using a set width for the container element of your board cells you can float the cells and they will wrap to the next line. Absolute positioning tends to be a bit of a bugger. If you want 10 items per row it's as easy as:
<container width> / <items per row> = <width>
Using document fragments is faster than appending each individual element one at a time to the actual DOM. Instead you append the elements to a document fragment that isn't a part of the DOM until you append it. This way you're doing a single insert for all the cells instead of 100.
I moved some of the styling to CSS, but could easily be moved back to JS if you really need to.
function onCellClick() {
this.style.backgroundColor = 'red';
console.log( 'selected' );
}
var main = document.getElementById( 'board' ),
frag = document.createDocumentFragment(),
i = 0,
len = 100;
for ( ; i < len; i++ ) {
div = document.createElement( 'div' );
div.addEventListener( 'click', onCellClick, false );
frag.appendChild( div );
}
main.appendChild( frag );
#board {
width: 500px;
height: 500px;
}
#board div {
float: left;
box-sizing: border-box;
width: 50px;
height: 50px;
border: 1px solid #ccc;
}
<div id="board"></div>

Scroll to position WITHIN a div (not window) using pure JS

PURE JS ONLY PLEASE - NO JQUERY
I have a div with overflow scroll, the window (html/body) never overflows itself.
I have a list of anchor links and want to scroll to a position when they're clicked.
Basically just looking for anchor scrolling from within a div, not window.
window.scrollTo etc. don't work as the window never actually overflows.
Simple test case http://codepen.io/mildrenben/pen/RPyzqm
JADE
nav
a(data-goto="#1") 1
a(data-goto="#2") 2
a(data-goto="#3") 3
a(data-goto="#4") 4
a(data-goto="#5") 5
a(data-goto="#6") 6
main
p(data-id="1") 1
p(data-id="2") 2
p(data-id="3") 3
p(data-id="4") 4
p(data-id="5") 5
p(data-id="6") 6
SCSS
html, body {
width: 100%;
height: 100%;
max-height: 100%;
}
main {
height: 100%;
max-height: 100%;
overflow: scroll;
width: 500px;
}
nav {
background: red;
color: white;
position: fixed;
width: 50%;
left: 50%;
}
a {
color: white;
cursor: pointer;
display: block;
padding: 10px 20px;
&:hover {
background: lighten(red, 20%);
}
}
p {
width: 400px;
height: 400px;
border: solid 2px green;
padding: 30px;
}
JS
var links = document.querySelectorAll('a'),
paras = document.querySelectorAll('p'),
main = document.querySelector('main');
for (var i = 0; i < links.length; i++) {
links[i].addEventListener('click', function(){
var linkID = this.getAttribute('data-goto').slice(1);
for (var j = 0; j < links.length; j++) {
if(linkID === paras[j].getAttribute('data-id')) {
window.scrollTo(0, paras[j].offsetTop);
}
}
})
}
PURE JS ONLY PLEASE - NO JQUERY
What you want is to set the scrollTop property on the <main> element.
var nav = document.querySelector('nav'),
main = document.querySelector('main');
nav.addEventListener('click', function(event){
var linkID,
scrollTarget;
if (event.target.tagName.toUpperCase() === "A") {
linkID = event.target.dataset.goto.slice(1);
scrollTarget = main.querySelector('[data-id="' + linkID + '"]');
main.scrollTop = scrollTarget.offsetTop;
}
});
You'll notice a couple of other things I did different:
I used event delegation so I only had to attach one event to the nav element which will more efficiently handle clicks on any of the links.
Likewise, instead of looping through all the p elements, I selected the one I wanted using an attribute selector
This is not only more efficient and scalable, it also produces shorter, easier to maintain code.
This code will just jump to the element, for an animated scroll, you would need to write a function that incrementally updates scrollTop after small delays using setTimeout.
var nav = document.querySelector('nav'),
main = document.querySelector('main'),
scrollElementTo = (function () {
var timerId;
return function (scrollWithin, scrollTo, pixelsPerSecond) {
scrollWithin.scrollTop = scrollWithin.scrollTop || 0;
var pixelsPerTick = pixelsPerSecond / 100,
destY = scrollTo.offsetTop,
direction = scrollWithin.scrollTop < destY ? 1 : -1,
doTick = function () {
var distLeft = Math.abs(scrollWithin.scrollTop - destY),
moveBy = Math.min(pixelsPerTick, distLeft);
scrollWithin.scrollTop += moveBy * direction;
if (distLeft > 0) {
timerId = setTimeout(doTick, 10);
}
};
clearTimeout(timerId);
doTick();
};
}());
nav.addEventListener('click', function(event) {
var linkID,
scrollTarget;
if (event.target.tagName.toUpperCase() === "A") {
linkID = event.target.dataset.goto.slice(1);
scrollTarget = main.querySelector('[data-id="' + linkID + '"]');
scrollElementTo(main, scrollTarget, 500);
}
});
Another problem you might have with the event delegation is that if the a elements contain child elements and a child element is clicked on, it will be the target of the event instead of the a tag itself. You can work around that with something like the getParentAnchor function I wrote here.
I hope I understand the problem correctly now: You have markup that you can't change (as it's generated by some means you have no control over) and want to use JS to add functionality to the generated menu items.
My suggestion would be to add id and href attributes to the targets and menu items respectively, like so:
var links = document.querySelectorAll('a'),
paras = document.querySelectorAll('p');
for (var i = 0; i < links.length; i++) {
links[i].href=links[i].getAttribute('data-goto');
}
for (var i = 0; i < paras.length; i++) {
paras[i].id=paras[i].getAttribute('data-id');
}

How can I know when there is a new line on a Wheel of Fortune Board?

I the following code here in which you can play a Wheel of Fortune-like game with one person (more of my test of javascript objects).
My issue is that when the screen is small enough, the lines do not seem to break correctly.
For example:
Where the circle is, I have a "blank" square. The reason why I have a blank square is so that when the screen is big enough, the square serves as a space between the words.
Is there a way in my code to efficiently know if the blank square is at the end of the line and to not show it, and then the window gets resized, to show it accordingly?
The only thought I had was to add a window.onresize event which would measure how big the words are related to how big the playing space is and decide based on that fact, but that seems very inefficient.
This is my code for creating the game board (starts # line 266 in my fiddle):
WheelGame.prototype.startRound = function (round) {
this.round = round;
this.lettersInPuzzle = [];
this.guessedArray = [];
this.puzzleSolved = false;
this.currentPuzzle = this.puzzles[this.round].toUpperCase();
this.currentPuzzleArray = this.currentPuzzle.split("");
var currentPuzzleArray = this.currentPuzzleArray;
var lettersInPuzzle = this.lettersInPuzzle;
var word = document.createElement('div');
displayArea.appendChild(word);
word.className = "word";
for (var i = 0; i < currentPuzzleArray.length; ++i) {
var span = document.createElement('div');
span.className = "wordLetter ";
if (currentPuzzleArray[i] != " ") {
span.className += "letter";
if (!(currentPuzzleArray[i] in lettersInPuzzle.toObject())) {
lettersInPuzzle.push(currentPuzzleArray[i]);
}
word.appendChild(span);
} else {
span.className += "space";
word = document.createElement('div');
displayArea.appendChild(word);
word.className = "word";
word.appendChild(span);
word = document.createElement('div');
displayArea.appendChild(word);
word.className = "word";
}
span.id = "letter" + i;
}
var clear = document.createElement('div');
displayArea.appendChild(clear);
clear.className = "clear";
};
Instead of JavaScript, this sounds more like a job for CSS, which solves this problem all the time when dealing with centered text.
Consider something like this:
CSS
#board {
text-align: center;
border: 1px solid blue;
font-size: 60pt;
}
.word {
display: inline-block;
white-space: nowrap; /* Don't break up words */
margin: 0 50px; /* The space between words */
}
.word span {
display: inline-block;
width: 100px;
border: 1px solid black
}
HTML
<div id="board">
<span class="word"><span>W</span><span>h</span><span>e</span><span>e</span><span>l</span></span>
<span class="word"><span>o</span><span>f</span></span>
<span class="word"><span>F</span><span>o</span><span>r</span><span>t</span><span>u</span><span>n</span><span>e</span></span>
</div>
Here's a fiddle (try resizing the output pane).
Here you go. Uses the element.offsetTop to determine if a .space element is on the same line as its parent.previousSibling.lastChild or parent.nextSibling.firstChild.
Relevant Code
Note: In the fiddle I change the background colors instead of changing display so you can see it work.
// hides and shows spaces if they are at the edge of a line or not.
function showHideSpaces() {
var space,
spaces = document.getElementsByClassName('space');
for (var i = 0, il = spaces.length ; i < il; i++) {
space = spaces[i];
// if still display:none, then offsetTop always 0.
space.style.display = 'inline-block';
if (getTop(nextLetter(space)) != space.offsetTop || getTop(prevLetter(space)) != space.offsetTop) {
space.style.display = 'none';
} else {
space.style.display = 'inline-block';
}
}
}
// navigate to previous letter
function nextLetter(fromLetter) {
if (fromLetter.nextSibling) return fromLetter.nextSibling;
if (fromLetter.parentElement.nextSibling)
return fromLetter.parentElement.nextSibling.firstChild;
return null;
}
// navigate to next letter
function prevLetter(fromLetter) {
if (fromLetter.previousSibling) return fromLetter.previousSibling;
if (fromLetter.parentElement.previousSibling)
return fromLetter.parentElement.previousSibling.lastChild;
return null;
}
// get offsetTop
function getTop(element) {
return (element) ? element.offsetTop : 0;
}
showHideSpaces();
if (window.addEventListener) window.addEventListener('resize', showHideSpaces);
else if (window.attachEvent) window.attachEvent('onresize', showHideSpaces);
jsFiddle

Categories

Resources