HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavascriptMinor

Fireworks animation causing high CPU load

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
fireworksanimationhighcausingloadcpu

Problem

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 =

Solution

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.

Code Snippets

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.
}
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
];
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();

Context

StackExchange Code Review Q#112245, answer score: 2

Revisions (0)

No revisions yet.