Fabric JS: Performance of very large images (20mb+)

Viewed 1484

I'm using Fabric JS to manipulate very large images (20mb+). I have found that Fabric is considerably slower at handling large images in a canvas as compared to using the standard Canvas API.

The below code snippet has two input buttons, one for adding an image to canvas using standard Canvas API and the other using Fabric JS. Each method will also convert the canvas to a data url using toDataUrl(). Each method also logs three times: start time, time when img.onload function completes, and when toDataUrl() completes.

Here is a table comparing import+export times that I tested for varying image sizes: import times for 500kb to 50mb photos

Here is a graph displaying the performance of Fabric import+export times vs Canvas API: graph

Questions:

  • Why is Fabric performance so much slower than Canvas API at importing+exporting large images on a canvas?
  • Is there a way to increase Fabric performance when using large images?
  • Are my test cases accurately representing Fabric performance?

// Standard Import
function handleFiles(e) {
  var t0 = performance.now();
  console.log('Standard Import')
  console.log('Start Time: ', Math.round(t0/1000))  
  var promise = new Promise(function(resolve) {
    var URL = window.webkitURL || window.URL;
    var ctx = document.getElementById('canvas').getContext('2d');
    canvas = document.getElementById('canvas');
    var url = URL.createObjectURL(e.target.files[0]);
    var img = new Image();
    img.onload = function() {
        canvas.width = img.width;
        canvas.height = img.height;
        ctx.drawImage(img, 0, 0);
        resolve("done")
    };
    img.src = url;  
  });

  promise.then(function(result) {
    var t1 = performance.now()
    console.log('Done img.onload() Elapsed Time: ', Math.round(t1 - t0)/1000);
    var dataURL = canvas.toDataURL('image/png')
    return    
  }).then(function(result){
    t2 = performance.now()
    console.log('Done canvas.ToDataURL() Elapsed Time: ', Math.round(t2 - t0)/1000)
    return
  });
}

// Fabric Import
function handleFilesFabric(e) {
  var t0 = performance.now();
  console.log('Fabric Import')
  console.log('Start Time: ', Math.round(t0/1000))
  var promise = new Promise(function(resolve) {   
    var canvas = new fabric.Canvas('canvas');
    var reader = new FileReader();
    reader.onload = function (event){
      var imgObj = new Image();
      imgObj.src = event.target.result;
      imgObj.onload = function () {
        var image = new fabric.Image(imgObj);
        canvas.setHeight(imgObj.height);
        canvas.setWidth(imgObj.width);
        canvas.add(image);
        canvas.renderAll();
        canvas.forEachObject(function(object){ 
          object.selectable = false; 
        });
        resolve("done")
      }
    }
    reader.readAsDataURL(e.target.files[0]);
  });
  
  promise.then(function(result) {
    var t1 = performance.now()
    console.log('Done img.onload() Elapsed Time: ', Math.round(t1 - t0)/1000);
    var dataURL = canvas.toDataURL('image/png')
    return
  }).then(function(result){
    t2 = performance.now()
    console.log('Done canvas.ToDataURL() Elapsed Time: ', Math.round(t2 - t0)/1000)
    return
  });
}

window.onload = function() {
  // Standard Import 
  var input = document.getElementById('input');
  input.addEventListener('change', handleFiles, false);
  // Fabric Import
  var input2 = document.getElementById('input2');
  input2.addEventListener('change', handleFilesFabric, false);  
};
canvas {
  border: 2px solid;
}
<!DOCTYPE html>
<html lang="en" >
<head>
  <meta charset="UTF-8">
  <title>Upload & Display Image w/ Canvas</title>
  <link rel="stylesheet" href="css/style.css">
  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.3/fabric.min.js"></script>
</head>
<body>
    <h1> Upload & Display Image</h1>
    <canvas width="400" height="400" id="canvas"></canvas>
    <br>
    <b>Standard Add Image</b><br>
    <input type="file" id="input"/>
    <br>
    <b>Fabric Add Image</b><br>
    <input type="file" id="input2"/>
    <script src="js/index.js"></script>
</body>
</html>

1 Answers

I refactored the code a bit to be more fair: Same way of loading the image and both event run after the other on 2 different canvases.

Also remove additional fabricJS functionality that for this test do not make sense. I think that for bigger image the differnce should be lower now, can you try in the snippet with a big image?

By the way you cannot compare a URL.createObjectUrl from a file with a file reader on a dataUrl. Is just unfair. createObjectUrl create a reference in memory to the file you uploaded.

ReadAsDataUrl read the file, encode in base64, create a string object, then the browser has to read that string again, decode from base64.

The difference could also be in the fact fabricJS paint the image with drawImage and 9 args, while you used the 3 args version.

// Standard Import
fabric.Object.prototype.objectCaching = false;
function handleFiles(e) {
  var t0 = performance.now();

  var promise = new Promise(function(resolve) {
    var URL = window.webkitURL || window.URL;
    var ctx = document.getElementById('canvas').getContext('2d');
    canvas = document.getElementById('canvas');
    var url = URL.createObjectURL(e.target.files[0]);
    var img = new Image();
    img.onload = function() {
      console.log('Standard Import')
  console.log('Start Time: ', Math.round(t0/1000))  
        canvas.width = img.width;
        canvas.height = img.height;
        ctx.drawImage(img, 0, 0);
        resolve("done")
    };
    img.src = url;  
  });

  promise.then(function(result) {
    var t1 = performance.now()
    console.log('Done img.onload() Elapsed Time: ', Math.round(t1 - t0)/1000);
    var dataURL = canvas.toDataURL('image/png')
    return    
  }).then(function(result){
    t2 = performance.now()
    console.log('Done canvas.ToDataURL() Elapsed Time: ', Math.round(t2 - t0)/1000)
    return
  });
}

// Fabric Import
function handleFilesFabric(e) {
  var t0 = performance.now();
  var canvas = new fabric.StaticCanvas('canvas2', {enableRetinaScaling: false, renderOnAddRemove: false });
  var promise = new Promise(function(resolve) {   
    
    var reader = new FileReader();
    var URL = window.webkitURL || window.URL;
    var url = URL.createObjectURL(e.target.files[0]);
      var imgObj = new Image();
      
      imgObj.onload = function () {
        console.log('Fabric Import')
  console.log('Start Time: ', Math.round(t0/1000))

        var image = new fabric.Image(imgObj);
        canvas.setDimensions({ width: imgObj.width, height: imgObj.height});
        canvas.add(image);
        resolve("done")
      }
      imgObj.src = url;
  });
  
  promise.then(function(result) {
    var t1 = performance.now()
    console.log('Done img.onload() Elapsed Time: ', Math.round(t1 - t0)/1000);
    canvas.renderAll();
    var dataURL = canvas.lowerCanvasEl.toDataURL('image/png')
    return
  }).then(function(result){
    t2 = performance.now()
    console.log('Done canvas.ToDataURL() Elapsed Time: ', Math.round(t2 - t0)/1000)
    return
  });
}

window.onload = function() {
  // Standard Import 
  var input = document.getElementById('input');
  input.addEventListener('change', handleFiles, false);
  input.addEventListener('change', handleFilesFabric, false);
};
canvas {
  border: 2px solid;
}
<!DOCTYPE html>
<html lang="en" >
<head>
  <meta charset="UTF-8">
  <title>Upload & Display Image w/ Canvas</title>
  <link rel="stylesheet" href="css/style.css">
  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.3/fabric.min.js"></script>
</head>
<body>
    <h1> Upload & Display Image</h1>
    <canvas width="400" height="400" id="canvas"></canvas>
    <canvas width="400" height="400" id="canvas2"></canvas>
    <br>
    <b>Standard Add Image</b><br>
    <input type="file" id="input"/>
    <br>
</body>
</html>

Related