Performance comparison between readAsDataUrl and createObjectURL

If you work with applications that handle uploading images as blobs then you’ve most likely wondered if it’s faster to convert the image using FileReader.readAsDataUrl() or URL.createObjectURL(). For our implementations in the geographic mapping industry we often typically request dozens, hundreds or sometimes thousands of relatively tiny map tile images such as .png and .jpeg in a single user session. There’s always a question of loading and rendering performance.

I was working on a related customer question and I was curious which one is faster in our use cases, so I did some simple testing. A common example of an online web map contains tiles that are 256×256 and vary in size from around 2Kb to 15kb. I assumed for the file types and sizes we use the results would be different because I’d read that createObjectUrl() is typically faster.

TLDR;

The results were surprising to me. For our use cases with relatively small .png images I saw the following results:

  • Chrome: readAsDataUrl() was consistently faster uncached
  • Firefox: createObjectUrl() was consistently faster uncached
  • Safari: was inconsistent between the two.

Test appHere is the link to the test app.

Note that Safari had unpredictable performance in that sometimes readAsDataUrl() was faster than createObjectUrl(). I saw the same behavior for cached and uncached tests and didn’t have time to investigate further.

YMMV!

Just a caveat that since we use lots of small images, your mileage may vary if you use larger images. I hope someone reads this and devises a test for larger images and then shares the results.

The Tests

I tested basic performance of pulling a map tile image from a CDN then using performance.now() to determine the time to create the image and then append it to an HTML list element. I did try to build the code in a way that each loop of the test used a different image to avoid any unintentional optimizations, such as sharing an image in-memory, or in-browser caching. I also ran the comparative tests recursively to try and normalize for readAsDataUrl() being asynchronous. I didn’t have time to investigate memory usage between the two patterns.

Note that testing with the browser console closed will be significantly faster than testing with the console open.  I used a 2018 Macbook Pro, 16GB with DDR4 RAM. I cleared the browser cache before each loop and I used this test app. In the code, each pattern goes through 25 loops and is averaged.

Chrome 80
Test results averaged
Test 1
(ms)
Test 2
(ms)
Test 3
(ms)
Test 4
(ms)
readAsDataUrl0.7890.8270.8130.839
createObjectURL1.6841.6381.6411.544
Firefox 73Test 1
(ms)
Test 2
(ms)
Test 3
(ms)
Test 4
(ms)
readAsDataUrl1.481.281.281.2
createObjectURL1.120.841.041.0
Safari 13Test 1
(ms)
Test 2
(ms)
Test 3
(ms)
Test 4
(ms)
readAsDataUrl0.360.280.720.68
createObjectURL1.960.60.561.6

Conclusions

If you are only uploading a few smaller images, then wondering which approach is faster probably isn’t a good use of your time – either one is good. If you handle hundreds or thousands of smaller images per user session then it might be worth some testing. Based on these quick tests, and more testing is needed to be truly definitive, it really depends on which browsers your users prefer. For example, if you are building hybrid apps then you have control over which browser. In a pure web application you don’t typical have control over what users use in the wild.

I didn’t test larger sized images or images of a different type, such as .jpeg. I’m curious what type of test results those might produce.

Using async tokens with JavaScript FileReader

The JavaScript FileReader is a very powerful, efficient and asynchronous way to read the binary content of files or Blobs. Because it’s asynchronous, if you are doing high-volume, in-memory processing there is no guarantee as to the order in which reading events are completed. This can be a challenge if you have a requirement to associate some additional unique information with each file or Blob and persist it all the way thru to the end of the process. The good news is there is an easy way to do this using async tokens.

Using an asynchronous token means you can assign a unique Object, Number or String to each FileReader operation. Once you do that, the order in which the results are returned no longer matters. When each read operation completes you can simply retrieve the token uniquely associated with the original file or Blob.  There really isn’t any magic. Here is a snippet of the coding pattern. You can test out a complete example on github.


function parse(blob,token,callback){

    // Always create a new instance of FileReader every time.
    var reader = new FileReader();

    // Attach the token as a property to the FileReader Object.
    reader.token = token;

    reader.onerror = function (event) {
        console.error(new Error(event.target.error.code).stack);
    }

    reader.onloadend = function (evt) {
        if(this.token != undefined){

            // The reader operation is complete.
            // Now we can retrieve the unique token associated
            // with this instance of FileReader.
            callback(this.result,this.token);
        }
    };
    reader.readAsBinaryString(blob);
}

Note, it is a very bad practice to simply associate the FileReader result object with the token being passed into the parse() function’s closure. Because the results from the onloadend events can be returned in any order, each parsed result could end up being assigned the wrong token. This is an easy mistake to make and it can seriously corrupt your data.