Edit in JSFiddle

// A simple utility for executing multiple tasks within one requestAnimationCallback.
// Priority tasks are executed before idle tasks. The utility makes it easier to
// split long-running rendering tasks into batches whose duration does not exceed
// the deadline provided in the parameter.
var RequestAnimationFrameManager = function(deadlineMs) {
  var priority = [];
  var idle = [];
  var animationFrameRequested = false;

  this.schedulePriorityTask = function (task) {
    priority.push(task);
    schedule();
  };

  this.scheduleIdleTask = function (task) {
    idle.push(task);
    schedule();
  };

  function schedule() {
    if (!animationFrameRequested) {
      requestAnimationFrame(processQueues);
      animationFrameRequested = true;
    }
  }

  function processQueues(now) {
    animationFrameRequested = false;
    var i, task;

    var started = window.performance.now();
    var deadline = started + deadlineMs;

    var priorityTasksToRun = priority;
    var idleTasksToRun = idle;
    priority = [];
    idle = [];

    execute(priorityTasksToRun, priority);
    execute(idleTasksToRun, idle);
    if (priority.length > 0 || idle.length > 0) {
      schedule();
    }

    function execute(tasks, nextTasks) {
      for (i = 0; i < tasks.length; i++) {
        task = tasks[i];

        var taskStart = window.performance.now();
        if (taskStart < deadline) {
          // Let the task know what the current deadline is.
          // It is the responsibility of the task to return
          // before the deadline passes.
          var completed = task(now, taskStart, deadline);
          if (completed) {
            continue;
          }
        }
        nextTasks.push(task);
      }
    }
  }
};

// Allow 12ms for each frame.
var rafManager = new RequestAnimationFrameManager(12);

// Set up canvas
var screenCanvas = document.getElementById("screen");
var width = screenCanvas.width, height = screenCanvas.height;

// Initialize drawing context
var screenCtx = screenCanvas.getContext("2d");
screenCtx.lineWidth = 0.5;
screenCtx.textAlign = "center";
screenCtx.font = "13px sans-serif";

// Draw initial content
var label = 0;
draw(screenCtx, label);

// Animate things. On every frame we modify the transform which is a low-cost operation.
// Additionally, once in a while we redraw the contents of the canvas. Since the drawing
// code respects the 12ms deadline for drawing, we shouldn't see any jank.
var animationStart = window.performance.now();
var frame = 0;
rafManager.schedulePriorityTask(function (now) {
  // Update transform
  var v = now - animationStart;
  var tx = 100 + 100 * Math.sin(4 * v / 2000);
  var ty = 100 + 100 * Math.cos(3 * v / 2000);
  var scale = 0.8 + 0.4 * (1 + Math.sin(v / 2751));
  screenCanvas.style.transform = "translateX(" + tx.toFixed(2) + "px) translateY(" + ty.toFixed(2) + "px) scale(" + scale + ")";

  frame++;
  if (frame % 100 == 0) {
    // Redraw the content
    draw(screenCtx, ++label);
  }

  // Loop the animation.
  return false;
});

// Draws content on the canvas.
function draw(ctx, it) {
  var i = 0;

  // We'll split rendering into batches, so that we don't miss frames.
  rafManager.scheduleIdleTask(function (now, start, deadline) {
    var size = 15;
    var totalX = Math.floor(width / size);
    var total = totalX * Math.floor(height / size);

    // Make drawing a little costly, so that it needs to be split into multiple frames.
    for (; i < total; i++) {
      var x = (i % totalX) * size;
      var y = Math.floor(i / totalX) * size;

      if (window.performance.now() >= deadline) {
        // Deadline exceeded, finish for now, we'll get called again in the next frame.
        return false;
      }

      // Draw our stuff
      roundRectPath(ctx, x, y, size - 2, size - 2, 1);
      var h = (Math.atan2(x - width / 2, y - width / 2) * 180 / Math.PI) + 180 + 5 * it;
      ctx.fillStyle = "hsla(" + h.toFixed(2) + ", 50%, 80%, 1.0)";
      ctx.strokeStyle = "hsla(" + h.toFixed(2) + ", 50%, 50%, 1.0)";
      ctx.fill();
      ctx.stroke();
      ctx.fillStyle = "black";
      ctx.fillText((it % 10), x + (size - 2) / 2, y + size - 3);
    }

    return true; // done now

    function roundRectPath(ctx, x, y, width, height, radius) {
      ctx.beginPath();
      ctx.moveTo(x + radius, y);
      ctx.lineTo(x + width - radius, y);
      ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
      ctx.lineTo(x + width, y + height - radius);
      ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
      ctx.lineTo(x + radius, y + height);
      ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
      ctx.lineTo(x, y + radius);
      ctx.quadraticCurveTo(x, y, x + radius, y);
      ctx.closePath();
    }
  });
}
<body>
  <canvas id="screen" width="800" height="600"></canvas>
</body>
    
  canvas {
    width: 800px;
    height: 600px;
  }