Ad

How To Prevent RequestAnimationFrame Choppiness Whilst Lazy Loading Images?

To paint the story:

I have some visual content which is divided into sets of images (tiles).

I'm using a simple loop which gets invoked by requestAnimationFrame to display these tiles on a canvas.

Actually I'm using PixiJS for this stuff; though it's probably an implementation detail not quite relevant to this question.

Because there might be a lot of tiles and because I would like the user to be able to 'somewhat immediately' scroll through these sets of tiles I'm preloading only a certain set of tiles, then when the user further navigates the tileset, another stream of tiles gets loaded, until there are no more tiles to load.

When the visual content is completely loaded the requestAnimationFrame looop functions well, without any choppiness.

But during the lazy loading there is some choppiness now and then.

I've tried using setInterval to separate the lazy loading (and the waste management or garbage collection too) from the requestAnimationFrame loop.

But I later realized this is because JavaScript only has one thread and thus the loading of images is blocking the rest of the code. So setInterval is not helpful; nor would be a second requestAnimatonFrame invocation for the lazy loading.

Then I read about Web Workers which does set up a possibility for concurrent execution of things. But as I read all data in a Web Worker thread is copied instead of passed by reference. So it would take up double the memory.

And also I'm doubting a Web Worker will be a suitable feature for loading tiles concurrently because the Mozilla Developer Network pages mostly appoint it as a help for parallel (heavy) number crunching solution.

There is a specific Web Worker type; a Service Worker which sits between the web page requests and the web server. Would a Service Worker help me at all loading images whilst executing the paint loop?

If not, does some one know of an alternate way to still lazy load but also animate the already loaded set of images? Is there something obvious I have overlooked?

Edit 3: Please look closely; the stutter is there while images load; sometimes more than other times; but it is still there; since I was instructed as per the comment section to remove my simulation it is much less obvious; but the stutter is still there. It's easier to see if one expands the snippet and then after opening up Developer Console, going to Network tab and checking "Disable cache", click the Run-button.

Edit 2: Ignore edit 1. Because the simulation has been replaced by real images loading now. The stutter is noticeable when loading the images the first time or when browser cache has been turned off.

Edit 1: As requested an example. The lazy load blockage is simulated by looping until a certain random delay has executed. The colors are supposed to represent the tile images but are actually drawn from a canvas using a random color.

var tileImageURLs = [
  "https://i.postimg.cc/FdFCY0RL/tile1.png",
  "https://i.postimg.cc/v1tS80Qy/tile2.png",
  "https://i.postimg.cc/3yTcHsQ9/tile3.png",
  "https://i.postimg.cc/Hr5hRpRV/tile4.png",
  "https://i.postimg.cc/fJ1P83HG/tile5.png",
  "https://i.postimg.cc/LnJ7kkVk/tile6.png",
];

var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');

var y = 0;
var NORMALSPEED = 5;
var currentSpeed = NORMALSPEED;
var TILEWIDTH = 768;
var TILEHEIGHT = 1024;
var TILECOUNT = 6;
var tileImages = [];
var lastTileLoad = -1;
var tileLoadStart = 0;
var PRELOADEDTILESCOUNT = 2;

// For offscreen generation of tiles.
//var tileCanvas = document.createElement('canvas');
//tileCanvas.width = TILEWIDTH;
//tileCanvas.height = TILEHEIGHT;
//var tileCtx = tileCanvas.getContext('2d');
//var tileColors = ['red', 'green', 'blue', 'orange', 'yellow'];

for (var tileIdx = 0; tileIdx < TILECOUNT; tileIdx++) {
  tileImages[tileIdx] = document.createElement('img');
  tileImages[tileIdx].width = TILEWIDTH;
  tileImages[tileIdx].height = TILEHEIGHT;
}


function loadTileImages(tileStart, tileEnd) {
  if (tileImages[tileStart]) {
    tileImages[tileStart].onload = function() {
      // Image loaded; go to next tile if applicable.
      if (tileStart + 1 <= tileEnd) {
        loadTileImages(++tileStart, tileEnd);
      }
    }
    tileImages[tileStart].src = tileImageURLs[tileStart];
  }
}

function loadTiles() {
  var tileLoadCount;
  if (lastTileLoad < Math.round(y / (canvas.height * 1))) {
    /**
     * Load checkpoint which lies past previous load checkpoint found;
     * so load some stuff.
     */
    tileLoadCount = Math.min(tileLoadStart + PRELOADEDTILESCOUNT - 1, TILECOUNT);
    loadTileImages(tileLoadStart, tileLoadCount);

    tileLoadStart += PRELOADEDTILESCOUNT;


    if (tileLoadStart > TILECOUNT - 1) {
      /**
       * Stop the loading; Infinity is always bigger than a non-infinite number.
       */
      console.log('Loading has finished.');
      lastTileLoad = Infinity;
    } else {
      /**
       * Store this 'load checkpoint'.
       */
      //this.needToDrawFrame = true;
      lastTileLoad = Math.round(y / (canvas.height * 1));
    }
  }
}

function tick() {
  var tileImgY;
  if (currentSpeed > 0 && (y >= (TILECOUNT * TILEHEIGHT) - canvas.height)) {
    currentSpeed = -NORMALSPEED;
  } else if (currentSpeed < 0 && (y <= 0)) {
    currentSpeed = NORMALSPEED;
  }
  y += currentSpeed;

  loadTiles();

  var tileStart = Math.max(Math.floor(y / TILEHEIGHT) - 1, 0);
  var tileCount = Math.min(tileStart + Math.ceil(canvas.height / TILEHEIGHT) + 2, TILECOUNT);
  //console.log(y, tileStart, tileCount);
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  for (var tileIndex = tileStart; tileIndex < tileCount; tileIndex++) {
    var tileImg = tileImages[tileIndex];
    tileImgY = (tileIndex * TILEHEIGHT) - y;
    ctx.drawImage(tileImg, 0, tileImgY, TILEWIDTH, TILEHEIGHT);
  }
  requestAnimationFrame(tick);
}

requestAnimationFrame(tick);
#canvas {
  width: 500px;
  height: 500px;
  border: solid 1px black;
}
<canvas id="canvas" width="768" height="1024"></canvas>

Ad

Answer

I have an answer to my own question; it was mostly stupidity on my end.

As it turns out it was making extra calls to the ticker function (invoked by requestAnimationFrame) during the loading phase (meant to make sure screen refreshed when new tile images arrived).

These calls caused spurts of extra movement and made it feel like there was some stutter (more heavy than I was able to reproduce in my example by the way).

But part of the stutter - the micro-stutter - went away after I used createImageBitmap in a Web Worker to load the images.

Ad
source: stackoverflow.com
Ad