Search code examples
javascriptcanvas

Rendering particles in Canvas, improving FireFox FPS


I'm trying to get the FPS above 30 in Firefox. I'm really not sure what else I may do to improve it. Any suggestions?

Chrome and Opera hover around 60 fps, while FF is stuck between 10-20, and because of that it's causing lag issues on the rest of my page.

http://jsfiddle.net/7vv2tur7/

window.requestAnimFrame = (function(){
    return window.requestAnimationFrame ||
    window.webkitRequestAnimationFrame ||
    window.mozRequestAnimationFrame ||
    function( callback ) {
        window.setTimeout(callback, 1000 / 60);
    };
})();


(function() {

    particleCanvas();

    con = particle.getContext('2d');
    pxs = [];
    for(var i = 0; i < 25; i++) {
        pxs[i] = new Circle();
        pxs[i].reset();
    }

    requestAnimFrame(paintParticles);

    window.onresize = function(event) {
        particleCanvas();
    };

})();

function Circle() {

    // settings
    this.s = {

        ttl:10000,

        // speeds
        xmax:4,
        ymax:4,

        // max size
        size:1,

        rt:4,

        xdrift:60,
        ydrift:60,

        opacity: 0.3,
    };


    this.reset = function() {

        // randomise positioning for each particle
        this.x = particle.width * Math.random();
        this.y = particle.height * Math.random();

        // size
        this.r = ((this.s.size-1)*Math.random()) + 1;


        this.dx = (Math.random()*this.s.xmax) * (Math.random() < 0.5 ? -1 : 1);
        this.dy = (Math.random()*this.s.ymax) * (Math.random() < 0.5 ? -1 : 1);
        this.hl = this.s.ttl / 4 * (this.r/this.s.size);
        this.rt = Math.random()*this.hl;
        this.s.rt = Math.random() +1;
        this.stop = Math.random();
        this.s.xdrift *= Math.random() * (Math.random() < 0.5 ? -1 : 1);
        this.s.ydrift *= Math.random() * (Math.random() < 0.5 ? -1 : 1);
    };

    this.fade = function() {
        this.rt += 5 + this.s.rt;
    };

    this.draw = function() {

        if(this.rt >= this.hl) this.reset();
        var newo = 1 - (this.rt/this.hl);



        con.globalAlpha = this.s.opacity;


        con.beginPath();
        con.arc(this.x,this.y,this.r,0,Math.PI*2,true);
        con.closePath();
        var cr = this.r*newo;

        g = con.createRadialGradient(this.x, this.y, 0, this.x, this.y, (cr <= 0 ? 1 : 5));
        g.addColorStop(0.0, 'rgba(255,255,255,' + newo + ')');
        g.addColorStop(this.stop, 'rgba(255,255,255,' + 0.5 * newo + ')');
        g.addColorStop(1.0, 'rgba(255,255,255, 0)');


        con.fillStyle = g;
        con.fill();
    };

    this.move = function() {
        this.x += (this.rt/this.hl)*this.dx;
        this.y += (this.rt/this.hl)*this.dy;
        if(this.x > particle.width || this.x < 0) this.dx *= -1;
        if(this.y > particle.height || this.y < 0) this.dy *= -1;
    };

    this.getX = function() { return this.x; };
    this.getY = function() { return this.y; };
}

function particleCanvas() {
    particle.width  = document.querySelector('.start').offsetWidth / 2;
    particle.height = document.querySelector('.start').offsetHeight / 2;
}

function paintParticles() {

    requestAnimFrame(paintParticles);

    con.clearRect ( 0 , 0 , particle.width, particle.height );

    for(var i = 0; i < pxs.length; i++) {
        pxs[i].fade();
        pxs[i].move();
        pxs[i].draw();
    }

    var thisFrameFPS = 1000 / ((now=new Date()) - lastUpdate);
        fps += (thisFrameFPS - fps) / fpsFilter;
        lastUpdate = now;

}

var fps = 0, now,
    lastUpdate = (new Date())*1 - 1, fpsFilter = 50;



setInterval(function(){
    document.querySelector('.fps').innerHTML = fps.toFixed(1) + ' fps';
}, 1000);

Solution

  • Your performance killer is creating a gradient for each particle during each animation frame.

    You might swap the expensive gradient for the relatively cheaper globalAlpha.

    Here's a refactoring that uses globalAlpha instead of gradient. I left it for you to tweak the globalAlpha to match your effect, but this refactoring is much faster on Firefox (59+ on my machine).

    There are other optimizations possible in your code, but using gradients is the performance killer...

    window.requestAnimFrame = (function(){
      return window.requestAnimationFrame ||
        window.webkitRequestAnimationFrame ||
        window.mozRequestAnimationFrame ||
        function( callback ) {
        window.setTimeout(callback, 1000 / 60);
      };
    })();
    
    
    (function() {
    
      particleCanvas();
    
      con = particle.getContext('2d');
      pxs = [];
      for(var i = 0; i < 25; i++) {
        pxs[i] = new Circle();
        pxs[i].reset();
      }
    
      requestAnimFrame(paintParticles);
    
      window.onresize = function(event) {
        particleCanvas();
      };
    
    })();
    
    function Circle() {
    
      // settings
      this.s = {
    
        ttl:10000,
    
        // speeds
        xmax:4,
        ymax:4,
    
        // max size
        size:1,
    
        rt:4,
    
        xdrift:60,
        ydrift:60,
    
        opacity: 0.7,
      };
    
    
      this.reset = function() {
    
        // randomise positioning for each particle
        this.x = particle.width * Math.random();
        this.y = particle.height * Math.random();
    
        // size
        this.r = ((this.s.size-1)*Math.random()) + 1;
    
    
        this.dx = (Math.random()*this.s.xmax) * (Math.random() < 0.5 ? -1 : 1);
        this.dy = (Math.random()*this.s.ymax) * (Math.random() < 0.5 ? -1 : 1);
        this.hl = this.s.ttl / 4 * (this.r/this.s.size);
        this.rt = Math.random()*this.hl;
        this.s.rt = Math.random() +1;
        this.stop = Math.random();
        this.s.xdrift *= Math.random() * (Math.random() < 0.5 ? -1 : 1);
        this.s.ydrift *= Math.random() * (Math.random() < 0.5 ? -1 : 1);
    
        this.s.opacity=0.70;
      };
    
      this.fade = function() {
        this.rt += 5 + this.s.rt;
        this.s.opacity-=.005;
      };
    
      this.draw = function() {
    
        if(this.rt >= this.hl) this.reset();
        var newo = 1 - (this.rt/this.hl);
    
        con.globalAlpha = this.s.opacity;
    
        con.beginPath();
        con.arc(this.x,this.y,this.r,0,Math.PI*2,true);
        con.closePath();
        var cr = this.r*newo;
        con.fill();
      };
    
      this.move = function() {
        this.x += (this.rt/this.hl)*this.dx;
        this.y += (this.rt/this.hl)*this.dy;
        if(this.x > particle.width || this.x < 0) this.dx *= -1;
        if(this.y > particle.height || this.y < 0) this.dy *= -1;
      };
    
      this.getX = function() { return this.x; };
      this.getY = function() { return this.y; };
    }
    
    function particleCanvas() {
      particle.width 	= document.querySelector('body').offsetWidth / 2;
      particle.height = document.querySelector('body').offsetHeight / 2;
    }
    
    function paintParticles() {
    
      requestAnimFrame(paintParticles);
    
      con.clearRect ( 0 , 0 , particle.width, particle.height );
    
      con.fillStyle = 'white';
      for(var i = 0; i < pxs.length; i++) {
        pxs[i].fade();
        pxs[i].move();
        pxs[i].draw();
      }
    
      var thisFrameFPS = 1000 / ((now=new Date()) - lastUpdate);
      fps += (thisFrameFPS - fps) / fpsFilter;
      lastUpdate = now;
    
    }
    
    var fps = 0, now,
        lastUpdate = (new Date())*1 - 1, fpsFilter = 50;
    
    
    
    setInterval(function(){
      document.querySelector('.fps').innerHTML = fps.toFixed(1) + ' fps';
    }, 1000);
    html,
    body {height:100%;width:100%;display:block;margin:0;padding:0;background:black}
    
    * {box-sizing:border-box}
    
    span {
      position:absolute;
      top:50px;
      left:50px;
      font-size:28px;
      color:white;
      font-family:mono;
    }
    canvas {width:100%;height:100%}    </style>
    <canvas id="particle"></canvas>
    <span class="fps"></span>