3
\$\begingroup\$

This web algorithm is causing even newer computers to freeze with CPU spikes from 20% to 90%! How can I make it more efficient?

EDIT: An idea is to somehow get rid of the filter:blur(); property and use box-shadow instead but I don't know how to do that. Using something other than filter:blur(); would be awesome because IE doesn't support it and I think that would help with the high CPU usage. I'm not sure if the filter property can take advantage of the hardware acceleration which could be the problem.

/*settings variables*/
var fireworkDimensions = 10; //make each firework 5px in width and height
var fireworkTransitionTime = 1000; //in milliseconds
var resizeTimer = false; //resize delay timer (not set)
var rayWidth = 2; //width of firework ray
var rayLength = 100; //length of firework ray
var fireworkRayPositions = [
  [0, 3],
  [10, 5],
  [8, 6],
  [5, 8],
  [2, 8],
  [2, 6],
  [0, 4],
  [0, 2],
  [2, 2],
  [4, 0],
  [8, 2],
  [10, 2]
]; //firework array positions in px format: bottom,right
//210
var blurLights = true;

// IF THE BROWSER IS INTERNET EXPLORER 10 OR 11
var UAString = navigator.userAgent;
if ((navigator.appVersion.indexOf("MSIE 10") != -1) || (UAString.indexOf("Trident") !== -1 && UAString.indexOf("rv:11") !== -1)) {
  //ie 10 and 11 don't support filter blurring
  blurLights = false;
}

var fireworkCounter = 0;
var fireworkRayRotation = [0, -30, -60, -90, -120, -150, -180, -210, -240, 90, 60, 30];

window.addEventListener('load', function() {
  initialize();
});

function initialize() {
  fireworkTimers = [];
  createStars();
  createFireworks();

  function createStars() {
    var starContainer = document.getElementById('starContainer');
    starContainer.innerHTML = '';
    for (var i = 0; i != window.innerWidth; i++) {
      var star = document.createElement('div');
      star.className = 'star';
      star.style.opacity = Math.random() * 0.5;
      var randomDimensions = Math.floor(Math.random() * 4);
      star.style.width = randomDimensions + 'px';
      star.style.height = randomDimensions + 'px';
      star.style.top = Math.floor(Math.random() * starContainer.offsetHeight - randomDimensions) + 'px';
      star.style.left = Math.floor(Math.random() * starContainer.offsetWidth - randomDimensions) + 'px';
      starContainer.appendChild(star);
    }
  }

  function createFireworks() {
    var fireworkContainer = document.getElementById('fireworksContainer');
    fireworkContainer.innerHTML = '';
    var numFireworks = Math.floor(window.innerWidth / fireworkDimensions);
    var colors = ['#001EFF', '#DE0013', '#E2BC00', '#6600FF', '#78DD00', '#E06CBE'];
    //var colors = ['#FFC47A','#FF312D','#5CC1FF','#FF9137','#FFCE1E'];
    //#001EFF (blue), #DE0013 (red), #E2BC00 (yellow), #6600FF purple, #78DD00 (green), #E06CBE pink
    for (var i = 0; i != numFireworks; i++) {
      var firework = document.createElement('div');
      firework.className = 'fireworkContainer';
      firework.style.width = fireworkDimensions + 'px';
      firework.style.height = fireworkDimensions + 'px';
      var fireworkColor = colors[Math.floor(Math.random() * colors.length)];
      firework.style.backgroundColor = fireworkColor;
      firework.style.left = Math.floor(Math.random() * ((window.innerWidth - firework.offsetWidth) - (5 * firework.offsetWidth) + 1)) + (5 * firework.offsetWidth) + 'px';
      var numFireworkRay = Math.floor(Math.random() * 20);
      for (var j = 0; j != 12; j++) {
        var ray = document.createElement('div');
        ray.style.backgroundColor = fireworkColor;
        ray.style.bottom = fireworkRayPositions[j][0] + 'px';
        ray.style.right = fireworkRayPositions[j][1] + 'px';
        ray.className = 'fireworkRay';
        ray.style.transform = 'rotate(' + fireworkRayRotation[j] + 'deg)';
        firework.appendChild(ray);
        //fireworkRayPositions
        //fireworkRayRotation
      }
      if (blurLights == true) {
        var light = document.createElement('div');
        firework.appendChild(light);
        light.className = 'light';
      }
      fireworkContainer.appendChild(firework)
    }
    //console.log(fireworkContainer.children.length)
    fireworkTiming();
  }
}

function fireworkTiming() {
  var numCompletedFireworks = 0;
  var fireworks = document.getElementsByClassName('fireworkContainer');
  for (var i = 0; i != fireworks.length; i++) {
    createTimer(i, fireworks[i]);
  }

  function createTimer(i, firework) {
    fireworkTimers.push(window.setTimeout(function() {
      firework.style.bottom = Math.floor(Math.random() * ((window.innerHeight * .9) - (window.innerHeight * .7) + 1)) + (window.innerHeight * .7) + 'px';
      numCompletedFireworks++;
      explodeTimer(firework);
      if (numCompletedFireworks == fireworks.length - 1) {
        repositionFireworks();
        //console.log('fireworks cycled, reset them')
      }
      //Math.floor(Math.random() * (maximum - minimum + 1)) + minimum;
      //i*Math.floor(Math.random()*6000))
      fireworkCounter++;
      //console.log(fireworkCounter);
    }, i * Math.floor(Math.random() * (4000 - 2000 + 1)) + 2000));
  }

  function explodeTimer(firework, i) {
    //var firework = firework;
    //console.log(firework.innerHTML)
    window.setTimeout(function() {
      var fireworkRays = firework.getElementsByClassName('fireworkRay');
      var light = firework.getElementsByClassName('light')[0];
      for (var i = 0; i != fireworkRays.length; i++) {
        fireworkRays[i].style.height = rayLength + 'px';
        fireworkRays[i].style.width = rayWidth + 'px';
        fireworkRays[i].style.boxShadow = "10px 10px 10px #fff";
      }
      if (blurLights == true) {
        light.style.width = 2 * rayLength + 'px';
        light.style.height = 2 * rayLength + 'px';
        light.style.backgroundColor = firework.style.backgroundColor;
      }
      //alert(light.length)
      window.setTimeout(function() {
        firework.style.opacity = '0';
      }, 800);
    }, fireworkTransitionTime - 200);
  }
}

function repositionFireworks() {
  var fireworks = document.getElementsByClassName('fireworkContainer');
  //var colors = ['#FFC47A','#FF312D','#5CC1FF','#FF9137','#FFCE1E'];
  var colors = ['#001EFF', '#DE0013', '#E2BC00', '#6600FF', '#78DD00', '#E06CBE'];
  for (var i = 0; i != fireworks.length; i++) {
    var fireworkColor = colors[Math.floor(Math.random() * colors.length)];
    fireworks[i].style.opacity = '1';
    fireworks[i].lastChild.removeAttribute('style');
    var fireworkRays = fireworks[i].getElementsByClassName('fireworkRay');
    for (var j = 0; j != fireworkRays.length; j++) {
      fireworkRays[j].style.width = '0px';
      fireworkRays[j].style.height = '0px';
      fireworkRays[j].style.backgroundColor = fireworkColor;
    }
    fireworks[i].style.backgroundColor = fireworkColor;
    fireworks[i].style.left = Math.floor(Math.random() * ((window.innerWidth - fireworks[i].offsetWidth) - fireworks[i].offsetWidth + 1)) + fireworks[i].offsetWidth + 'px';
    fireworks[i].style.bottom = '0';
  }
  for (var i = 0; i != fireworkTimers.length; i++) {
    window.clearTimeout(fireworkTimers[i]);
  }
  fireworkTiming();
}

window.addEventListener('resize', function() {
  window.clearTimeout(resizeTimer);
  resizeTimer = window.setTimeout(function() {
    initialize();
  }, 100);
});
#projectContainer {
  background-color: #333;
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
}
html,
body {
  margin: 0;
  padding: 0;
  height: 100%;
  width: 100%;
  overflow: hidden;
}
#starSuperContainer {
  width: 100vw;
  height: 70vh;
  position: relative;
}
#starContainer {
  width: 100%;
  height: 100%;
}
#starFade {
  width: 100%;
  height: 100%;
  background: -webkit-linear-gradient(transparent, #333);
  /* For Safari 5.1 to 6.0 */
  background: -o-linear-gradient(transparent, #333);
  /* For Opera 11.1 to 12.0 */
  background: -moz-linear-gradient(transparent, #333);
  /* For Firefox 3.6 to 15 */
  background: linear-gradient(transparent, #333);
  /* Standard syntax */
  position: absolute;
  top: 0;
}
.star {
  background-color: #fff;
  border-radius: 50%;
  position: fixed;
}
.fireworkContainer {
  /*width:20px;
                height:20px;*/
  margin-top: -4px;
  margin-bottom: -4px;
  position: absolute;
  display: inline-block;
  -o-transition: all 1s linear, opacity 2s linear;
  -moz-transition: all 1s linear, opacity 2s linear;
  -webkit-transition: all 1s linear, opacity 2s linear;
  -ms-transition: all 1s linear, opacity 2s linear;
  transition: all 1s linear, opacity 2s linear;
  bottom: 0;
  border-radius: 50%;
  -webkit-filter: blur(1px);
  -moz-filter: blur(1px);
  -ms-filter: blur(1px);
  -o-filter: blur(1px);
  filter: blur(1px);
  /*-webkit-box-shadow: 0 8px 6px -6px red;
                -moz-box-shadow: 0 8px 6px -6px red;
                box-shadow: 0 8px 6px -6px red;*/
}
.fireworkContainer * {
  -o-transition: all .8s linear, opacity .1s linear;
  -moz-transition: all .8s linear, opacity .1s linear;
  -webkit-transition: all .8s linear, opacity .1s linear;
  -ms-transition: all .8s linear, opacity .1s linear;
  transition: all .8s linear, opaciyt .1s linear;
  position: absolute;
  width: 0;
  height: 0;
  -o-transform-origin: bottom center;
  -moz-transform-origin: bottom center;
  -webkit-transform-origin: bottom center;
  -ms-transform-origin: bottom center;
  transform-origin: bottom center;
}
#fireworksContainer {
  position: fixed;
  bottom: -100px;
  display: table;
  left: 10px;
}
* {
  -webkit-user-select: none;
  /* Chrome all / Safari all */
  -moz-user-select: none;
  /* Firefox all */
  -ms-user-select: none;
  /* IE 10+ */
  user-select: none;
}
.light {
  border-radius: 50%;
  opacity: .4;
  -webkit-filter: blur(40px);
  -o-filter: blur(40px);
  -moz-filter: blur(40px);
  -ms-filter: blur(40px);
  filter: blur(add=0, direction=300, strength=10);
  -webkit-transform: translate(-50%, -50%);
  -moz-transform: translate(-50%, -50%);
  -ms-transform: translate(-50%, -50%);
  -o-transform: translate(-50%, -50%);
  transform: translate(-50%, -50%);
}
<div id="projectContainer">
  <div id="starSuperContainer">
    <div id="starContainer"></div>
    <div id="starFade"></div>
  </div>
  <div id="fireworksContainer"></div>
</div>

\$\endgroup\$
2
\$\begingroup\$

TLDR: Use a canvas.

The problem with your code above is that it uses the DOM, and doing DOM manipulation is slow. The browser has to reposition elements, recalculate the layout, style the elements with CSS, render. In a canvas, you have only one DOM element which just sits there and does nothing. All you tell JS is to draw pixels. The browser doesn't do all those repositioning, recalculating, CSS styling, minding layout and stuff.

So I see little benefit doing this in DOM code. I really suggest you learn how to draw using the canvas element.


Now if I did review the code...

First lets start with filter:blur(). That's a costly operation (I've been battling it out myself the past few weeks). One hack is to add transform3D(0,0,0) along with filter:blur(). Visually, it does nothing (it moves the element 0 units on all axes) but it tricks the browser to use hardware acceleration for the transition. But your mileage may vary. I've seen it so laggy on first blur, but further blurs are smooth.

Now I notice that you have a lot of timers running, and in them, you update the DOM. That's... really hard to read. It also affects performance as you update the DOM in a random manner. Like I mentioned earlier, when the browser manipulates the DOM, it takes into account its surrounding layout. This will take up unnecessary processing.

What I would suggest is you separate rendering from the logic. That way, you can simply treat logic as a bunch of functions that calculate data and mutate state, regardless of what renders the data. And you can also render, regardless of your logic, as long as you get the properly formatted data.

function render(){
  requestAnimationFrame(render);

  // If it's not time to render, return
  // If it's time to render
  // - Update state. This is where you calculate position etc.
  // - Render. You hand off your state to your renderer.
}

Also note the use of requestAnimationFrame. That's a special timer that aims to render the scene at 60fps. When your operations slow down, it compensates by dropping frames gracefully. Browser vendors also optimize this timer so that it can render optimally, as well as save battery life (it will slow down/pause when the tab is not in focus).

You can optimize your fireworks logic by thinking of your fireworks as an array of objects that hold the state. Think of it as an array of launchers and their explosive. Each iteration of your loop, you run through just this array, and depending on the time everything started, and the current time, calculate the state of the firework (how high it is now, is it exploding, how long has it been exploding, should it be resting) and update the values in the array.

var fireworks = [
  {                        == config ==
    trailColor: '#F00',   <-- red trail
    explodeColor: '#00E', <-- blue explosion
    riseTime: '10000',    <-- 10s until explode
    explodeTime: '2000',  <-- 2s explosion flare
                           == mutable state ==
    riseState: '1000',    <-- still rising
    explodeState: '0'     <-- still rising, not exploded yet
  },
  ... and so on
];

Once you've done all the mutations in the data. You simply hand off this array to your renderer. Depending on the state, the renderer will just run through the data in the array and render it. You can even swap renderers, as long as they consume the same format of data.

Now with this approach, you can optimize. For DOM rendering, you can create everything in advance, but hidden. Rendering data is simply just repositioning the elements that comprise the firework. If you did canvas, you could even layer canvas elements one on top of the other, with background transparent. That way, if a firework needs to be updated or cleared, it can clear the entire canvas without affecting the other fireworks. These rendering optimmizations can happen regardless of your logic.

In summary:

var element = ...
var fireworks = [...]

function update(){
  fireWorks.forEach(firework = {
    // update firework state
  });
}

function render(){
  fireWorks.forEach(firework = {
    // update the rendering
  });
}

function loop(){
  requestAnimationFrame(loop);
  if(notTimeToRender) return;

  update();
  render();
}

loop();

And yes, it's just one timer running for everything, the requestAnimationFrame. And oh, I forgot. Since requestAnimationFrame aims to hit 60fps, each iteration should be done within 16ms. This means update and render shouldn't take too long with whatever they're doing.

\$\endgroup\$
  • \$\begingroup\$ Thank you so much for your answer. It brought up many points that I haven't thought about. Where do you suggest I start for learning canvas? What do you think of SVG? Do you know of some good documentation for array.forEach functions that you used in your example? Thank you; a very informative answer indeed :) \$\endgroup\$ – www139 Nov 29 '15 at 22:35
  • \$\begingroup\$ @www139 I haven't played much with SVG, but I heard it's about the same as the DOM more or less (I heard it was harder to manipulate without some library). forEach is an array method. You can find more in MDN. If you have performance issues with it, you can easily replace it with your regular loops (for, while etc.). \$\endgroup\$ – Joseph Nov 29 '15 at 22:38
  • \$\begingroup\$ Thank you. I found it on MDN's website (developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…). Where did you learn canvas? \$\endgroup\$ – www139 Nov 29 '15 at 22:42

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.