Search code examples
htmlperformancecanvaswindow-resizeframe-rate

HTML Canvas performance on resize


I managed to translate an old OO Processing sketch to HTML5 Canvas: https://gist.github.com/sindiploma/ce1bb3b8424c32fb8d9e094f8944a789

I added mr.Doob's Stats library to record the canvas performance.

It performs as expected on document load, but when trying to resize the window. framerate drops dramatically.

Any tips on how to boost the performance on resize event?

You can also check this live demo:

(function(f,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"===typeof define&&define.amd?define(e):f.Stats=e()})(this,function(){var f=function(){function e(a){c.appendChild(a.dom);return a}function u(a){for(var d=0;d<c.children.length;d++)c.children[d].style.display=d===a?"block":"none";l=a}var l=0,c=document.createElement("div");c.style.cssText="position:fixed;top:0;left:0;cursor:pointer;opacity:0.9;z-index:10000";c.addEventListener("click",function(a){a.preventDefault();
u(++l%c.children.length)},!1);var k=(performance||Date).now(),g=k,a=0,r=e(new f.Panel("FPS","#0ff","#002")),h=e(new f.Panel("MS","#0f0","#020"));if(self.performance&&self.performance.memory)var t=e(new f.Panel("MB","#f08","#201"));u(0);return{REVISION:16,dom:c,addPanel:e,showPanel:u,begin:function(){k=(performance||Date).now()},end:function(){a++;var c=(performance||Date).now();h.update(c-k,200);if(c>g+1E3&&(r.update(1E3*a/(c-g),100),g=c,a=0,t)){var d=performance.memory;t.update(d.usedJSHeapSize/
1048576,d.jsHeapSizeLimit/1048576)}return c},update:function(){k=this.end()},domElement:c,setMode:u}};f.Panel=function(e,f,l){var c=Infinity,k=0,g=Math.round,a=g(window.devicePixelRatio||1),r=80*a,h=48*a,t=3*a,v=2*a,d=3*a,m=15*a,n=74*a,p=30*a,q=document.createElement("canvas");q.width=r;q.height=h;q.style.cssText="width:80px;height:48px";var b=q.getContext("2d");b.font="bold "+9*a+"px Helvetica,Arial,sans-serif";b.textBaseline="top";b.fillStyle=l;b.fillRect(0,0,r,h);b.fillStyle=f;b.fillText(e,t,v);
b.fillRect(d,m,n,p);b.fillStyle=l;b.globalAlpha=.9;b.fillRect(d,m,n,p);return{dom:q,update:function(h,w){c=Math.min(c,h);k=Math.max(k,h);b.fillStyle=l;b.globalAlpha=1;b.fillRect(0,0,r,m);b.fillStyle=f;b.fillText(g(h)+" "+e+" ("+g(c)+"-"+g(k)+")",t,v);b.drawImage(q,d+a,m,n-a,p,d,m,n-a,p);b.fillRect(d+n-a,m,a,p);b.fillStyle=l;b.globalAlpha=.9;b.fillRect(d+n-a,m,a,g((1-h/w)*p))}}};return f});

var canvas = document.getElementById("inner_heading-canvas");

if (canvas.getContext) {
  var context = canvas.getContext("2d");
}

window.addEventListener("load", init, false);
window.addEventListener(
  "resize",
  function() {
    clearTimeout(init);
    setTimeout(init, 500);
  },
  false
);

// Init
function init() {
  var net = undefined;

  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;

  var nodesLength = Math.floor(canvas.width * canvas.height / 2000);

  // Nodes
  net = new Net();
  net.populate(nodesLength);

  window.requestAnimationFrame(render);

  function render() {
    net.update();
    net.draw();
    net.connect(50);
    window.requestAnimationFrame(render);
  }
}

// Net
class Net {
  constructor() {
    this.nodes = [];
    this.length = undefined;
  }

  populate(length) {
    this.length = length;

    for (var i = 0; i < length; i++) {
      var xPos = Math.floor(getRandom(0, canvas.width));
      var yPos = Math.floor(getRandom(0, canvas.height));
      this.nodes.push(new Node(xPos, yPos));
    }
  }

  update() {
    for (var i = 0; i < this.length; i++) {
      this.nodes[i].update();
    }
  }

  draw() {
    context.fillStyle = "#000000";
    context.fillRect(0, 0, canvas.width, canvas.height);

    for (var i = 0; i < this.length; i++) {
      this.nodes[i].draw();
    }
  }

  connect(distanceMax) {
    for (var i = 0; i < this.length - 1; i++) {
      this.nodes[i].connections = [];

      for (var j = 0; j < this.length - 1; j++) {
        var a = this.nodes[j].x - this.nodes[i].x;
        var b = this.nodes[j].y - this.nodes[i].y;
        var c = Math.sqrt(a * a + b * b);

        if (c < distanceMax) {
          this.nodes[i].connections.push(j);
        }
      }

      for (var k = 0; k < this.nodes[i].connections.length; k++) {
        context.beginPath();
        context.moveTo(this.nodes[i].x, this.nodes[i].y);
        context.lineTo(
          this.nodes[this.nodes[i].connections[k]].x,
          this.nodes[this.nodes[i].connections[k]].y
        );
        context.strokeStyle = "rgba(255,255,255,.15)";
        context.stroke();
      }
    }
  }
}

// Node
class Node {
  constructor(_x, _y) {
    this.x = _x;
    this.y = _y;
    this.radius = 2;
    this.depth = Math.floor(getRandom(1, 10)) / 10;
  }

  update() {
    var velocity = (1 - this.depth) / 10;
    this.x = this.x + velocity;

    if (this.x > canvas.width || this.x < 0) {
      this.x = 0;
    }
  }

  draw() {
    var alpha = 1 - this.depth;
    context.beginPath();
    context.arc(this.x, this.y, this.radius, 0, 2 * Math.PI, false);
    context.fillStyle = "rgba(255,255,255," + alpha + ")";
    context.fill();
  }
}

// Helpers
function getRandom(min, max) {
  return Math.random() * (max - min) + min;
}

// Stats
var stats = new Stats();
stats.showPanel(0);
document.body.appendChild(stats.dom);

function animate() {
  stats.begin();
  stats.end();

  requestAnimationFrame(animate);
}

requestAnimationFrame(animate);
body {
  margin: 0;
}
<html>
    <head></head>
    <body>
        <canvas id="inner_heading-canvas" width="600" height="600"></canvas>
    </body>
</html>


Solution

  • Most important boost you get from clearing the requestAnimationFrame:

    var rafID = null; //scoping requestAnimationFrame request-ID
    //...
    //inside resize-callback
    window.cancelAnimationFrame(rafID);
    ...
    //inside init() + inside render()
    rafID = window.requestAnimationFrame(render); 
    

    In this way, we make sure that the browser is not still trying to update the old instances. I added it to the code snippet bellow, where I also made sure that the timeout works (for debouncing).

    (function(f,e){"object"===typeof exports&&"undefined"!==typeof module?module.exports=e():"function"===typeof define&&define.amd?define(e):f.Stats=e()})(this,function(){var f=function(){function e(a){c.appendChild(a.dom);return a}function u(a){for(var d=0;d<c.children.length;d++)c.children[d].style.display=d===a?"block":"none";l=a}var l=0,c=document.createElement("div");c.style.cssText="position:fixed;top:0;left:0;cursor:pointer;opacity:0.9;z-index:10000";c.addEventListener("click",function(a){a.preventDefault();
    u(++l%c.children.length)},!1);var k=(performance||Date).now(),g=k,a=0,r=e(new f.Panel("FPS","#0ff","#002")),h=e(new f.Panel("MS","#0f0","#020"));if(self.performance&&self.performance.memory)var t=e(new f.Panel("MB","#f08","#201"));u(0);return{REVISION:16,dom:c,addPanel:e,showPanel:u,begin:function(){k=(performance||Date).now()},end:function(){a++;var c=(performance||Date).now();h.update(c-k,200);if(c>g+1E3&&(r.update(1E3*a/(c-g),100),g=c,a=0,t)){var d=performance.memory;t.update(d.usedJSHeapSize/
    1048576,d.jsHeapSizeLimit/1048576)}return c},update:function(){k=this.end()},domElement:c,setMode:u}};f.Panel=function(e,f,l){var c=Infinity,k=0,g=Math.round,a=g(window.devicePixelRatio||1),r=80*a,h=48*a,t=3*a,v=2*a,d=3*a,m=15*a,n=74*a,p=30*a,q=document.createElement("canvas");q.width=r;q.height=h;q.style.cssText="width:80px;height:48px";var b=q.getContext("2d");b.font="bold "+9*a+"px Helvetica,Arial,sans-serif";b.textBaseline="top";b.fillStyle=l;b.fillRect(0,0,r,h);b.fillStyle=f;b.fillText(e,t,v);
    b.fillRect(d,m,n,p);b.fillStyle=l;b.globalAlpha=.9;b.fillRect(d,m,n,p);return{dom:q,update:function(h,w){c=Math.min(c,h);k=Math.max(k,h);b.fillStyle=l;b.globalAlpha=1;b.fillRect(0,0,r,m);b.fillStyle=f;b.fillText(g(h)+" "+e+" ("+g(c)+"-"+g(k)+")",t,v);b.drawImage(q,d+a,m,n-a,p,d,m,n-a,p);b.fillRect(d+n-a,m,a,p);b.fillStyle=l;b.globalAlpha=.9;b.fillRect(d+n-a,m,a,g((1-h/w)*p))}}};return f});
    
    var canvas = document.getElementById("inner_heading-canvas");
    
    var timeoutID = null; 
    var rafID = null;
    
    if (canvas.getContext) {
      var context = canvas.getContext("2d");
    }
    
    window.addEventListener("load", init, false);
    window.addEventListener(
      "resize",
      function() {
        clearTimeout(timeoutID);
        window.cancelAnimationFrame(rafID);
        timeoutID = setTimeout(init, 500);
      },
      false
    );
    
    // Init
    function init() {
      var net = undefined;
    
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
    
      var nodesLength = Math.floor(canvas.width * canvas.height / 2000);
    
      // Nodes
      net = new Net();
      net.populate(nodesLength);
    
      rafID = window.requestAnimationFrame(render);
    
      function render() {
        net.update();
        net.draw();
        net.connect(50);
        rafID = window.requestAnimationFrame(render);
      }
    }
    
    // Net
    class Net {
      constructor() {
        this.nodes = [];
        this.length = undefined;
      }
    
      populate(length) {
        this.length = length;
    
        for (var i = 0; i < length; i++) {
          var xPos = Math.floor(getRandom(0, canvas.width));
          var yPos = Math.floor(getRandom(0, canvas.height));
          this.nodes.push(new Node(xPos, yPos));
        }
      }
    
      update() {
        for (var i = 0; i < this.length; i++) {
          this.nodes[i].update();
        }
      }
    
      draw() {
        context.fillStyle = "#000000";
        context.fillRect(0, 0, canvas.width, canvas.height);
    
        for (var i = 0; i < this.length; i++) {
          this.nodes[i].draw();
        }
      }
    
      connect(distanceMax) {
        for (var i = 0; i < this.length - 1; i++) {
          this.nodes[i].connections = [];
    
          for (var j = 0; j < this.length - 1; j++) {
            var a = this.nodes[j].x - this.nodes[i].x;
            var b = this.nodes[j].y - this.nodes[i].y;
            var c = Math.sqrt(a * a + b * b);
    
            if (c < distanceMax) {
              this.nodes[i].connections.push(j);
            }
          }
    
          for (var k = 0; k < this.nodes[i].connections.length; k++) {
            context.beginPath();
            context.moveTo(this.nodes[i].x, this.nodes[i].y);
            context.lineTo(
              this.nodes[this.nodes[i].connections[k]].x,
              this.nodes[this.nodes[i].connections[k]].y
            );
            context.strokeStyle = "rgba(255,255,255,.15)";
            context.stroke();
          }
        }
      }
    }
    
    // Node
    class Node {
      constructor(_x, _y) {
        this.x = _x;
        this.y = _y;
        this.radius = 2;
        this.depth = Math.floor(getRandom(1, 10)) / 10;
      }
    
      update() {
        var velocity = (1 - this.depth) / 10;
        this.x = this.x + velocity;
    
        if (this.x > canvas.width || this.x < 0) {
          this.x = 0;
        }
      }
    
      draw() {
        var alpha = 1 - this.depth;
        context.beginPath();
        context.arc(this.x, this.y, this.radius, 0, 2 * Math.PI, false);
        context.fillStyle = "rgba(255,255,255," + alpha + ")";
        context.fill();
      }
    }
    
    // Helpers
    function getRandom(min, max) {
      return Math.random() * (max - min) + min;
    }
    
    // Stats
    var stats = new Stats();
    stats.showPanel(0);
    document.body.appendChild(stats.dom);
    
    function animate() {
      stats.begin();
      stats.end();
    
      requestAnimationFrame(animate);
    }
    
    requestAnimationFrame(animate);
    body {
      margin: 0;
    }
    <html>
        <head></head>
        <body>
            <canvas id="inner_heading-canvas" width="600" height="600"></canvas>
        </body>
    </html>

    Apart from this quick-fix there are many other general ways to make the resize work better. By searching on stackoverflow one can learn a few techniques: