Generating Thumbnails Dynamically on the Client

Client Computing

I’ll start this with a disclaimer: it’s nearly always best to generate thumbnails server-side. It’s fast and doesn’t duplicate computation over multiple clients. In most cases, I would probably create a cloud function to generate thumbnails of various dimensions and set it up to be triggered by a storage event (e.g. saving an image to AWS S3).

However, I make a case for client-side thumbnail generation when:

  1. You don’t control the backend of your application
  2. Your user’s experience would be significantly improved by the use of thumbnails

Take for example the hybrid mobile app my partners and I are building to help dermatologists take and manage patient photos. The backend is provided by the clinic’s EHR, so the app can only interact with it through an outdated network API and therefore has no internal control. It’s a photo management app, so thumbnails are prevalent and play a significant role in the user experience. However, rendering full-sized images as thumbnails is computationally intensive (especially in a mobile context) and we noticed that it creates a lot of UI lag.

So, we decided to try out dynamic client-side generation and we were pleased with the result. The dynamic bit means that we generate thumbnails on the fly as the photos are loaded. I’ve included a simplified gist below that illustrates how we did this in a browser context (webview in the case of our hybrid app):

function ThumbnailGenerator() {
  this.resizeCanvas = document.createElement('canvas');

  this.generate = function (imgSrc, thumbDims, compression) {
    [this.resizeCanvas.width, this.resizeCanvas.height] = [thumbDims.x, thumbDims.y];
    const ctx = this.resizeCanvas.getContext("2d");
  
    const tmp = new Image();
  
    const ret = new Promise(resolve => {
      tmp.onload = () => {
        ctx.drawImage(tmp, 0, 0, thumbDims.x, thumbDims.y);
        resolve(this.resizeCanvas.toDataURL('image/jpeg', 
            compression || 0.5));
      };
    });
    tmp.src = imgSrc;
    return ret;
  };

  this.generateBatch = function (imgSrcs, thumbDims, compression) {
    return Promise.all(imgSrcs.map(img => this.generate(img, thumbDims, compression)));
  }

  return this;
}

Running some performance tests on a photo of my hand yielded the following results:

Benchmark results
My hand (original)

So, dynamic thumbnail generation turned out to be pretty fast on average, certainly faster than I expected. Some of the thumbnails took 2–3 milliseconds, but on average they took about 1.07 ms. That said, I would encourage you to test with context and the actual images you would be using. That hand image is pretty small, so it’s going to be much faster than something HD.

Dynamic thumbnail generation turned out to be pretty fast… certainly faster than I expected.

Generating thumbnails dynamically wasn’t the only option we considered. We could have stored the thumbnails with the original photos in the EHR whenever a photo was saved. However, this was problematic because there was no way to mark them as hidden, so they would have cluttered the doctors’ desktop interface. In addition, there was nothing to stop inadvertent deletions (our situation is akin to having your S3 bucket viewed and edited directly on a daily basis by your users).

Alternatively, we could have generated the thumbnails on either the client or server-side and stored them in an external system we did control. However, this was ruled out because of the security and liability implications of taking responsibility for storing medical photos (not to mention the extra trouble of syncing the new external system with the EHR).

[Client-side thumbnail generation] is worth considering in cases where you have no control over the backend and thumbnails would improve your UX.

In conclusion, dynamic thumbnail generation (or client-side generation in general) is usually not the best way to go. But, it is worth considering in cases where you have no control over the backend and thumbnails would improve your UX.

(Originally published on Medium.)

Leave a Reply

Your email address will not be published. Required fields are marked *