Search code examples
javascriptvue.jsvuejs3openseadragon

Why does this image make 2 network requests on button click?


I am using an open-source web-based viewer in a Vue3 app. The image is not shown until the user clicks a "Open Image" button. It works good.

However, does anyone know why the same image is making two network requests when the "Open Image" button is clicked?

enter image description here

Here's is my minimal reproduction:

sandbox: https://stackblitz.com/edit/vitejs-vite-xxxk9w?file=src/App.vue

App.vue:

<script setup>
import { ref } from 'vue';
import Viewer from './components/Viewer.vue';
const show = ref(false);
</script>

<template>
  <div>
    <button type="button" @click="show = true">Open Image</button>
    <Viewer v-if="show" />
  </div>
</template>

Viewer.vue:

<template>
  <div ref="osdContainer" style="width: 500px; height: 500px"></div>
</template>

<script setup>
import OpenSeadragon from 'openseadragon';
import { ref, onMounted } from 'vue';
const viewer = ref(null);
const osdContainer = ref(null);

const initViewer = () => {
  console.log('init Viewer');
  viewer.value = OpenSeadragon({
    element: osdContainer.value,
    tileSources: {
      type: 'image',
      url: 'https://ik.imagekit.io/pixstery/users%2F5cnu6iDlTsa5mujH2sKPsBJ8OKH2%2Fposts%2Fportrait-of-arabian-man_jjC2?alt=media&token=64fb0ae4-b0dc-4ead-b22e-292e55de1447&tr=f-auto,pr-true,q-80',
      buildPyramid: false,
    },
  });
};

onMounted(() => {
  console.log('mounting..');
  initViewer();
});
</script>

Solution

  • OpenSeadragon thinks in tiled image pyramids, where most of the time you access the image metadata (resolution, and the like) and the actual tiles (bitmap data) separately.

    Supporting actual images is the outlier in such world, and it's still handled as if image metadata and bitmap data would arrive from separate sources.

    The first request you see comes from getImageInfo() of ImageTileSource, the specialized class for supporting images:

            var image = this._image = new Image();
    
            [...]
    
            $.addEvent(image, 'load', function () {
                _this.width = image.naturalWidth;
                _this.height = image.naturalHeight;
                _this.aspectRatio = _this.width / _this.height;
                _this.dimensions = new $.Point(_this.width, _this.height);
                _this._tileWidth = _this.width;
                _this._tileHeight = _this.height;
                _this.tileOverlap = 0;
                _this.minLevel = 0;
                _this.levels = _this._buildLevels();
                _this.maxLevel = _this.levels.length - 1;
    
                _this.ready = true;
    
                // Note: this event is documented elsewhere, in TileSource
                _this.raiseEvent('ready', {tileSource: _this});
            });
    
            [...]
    
            image.src = url;             // <----------
    

    and the second request is when the bitmap data is requested in _loadTile():

          _loadTile: function(tile, time) {
            var _this = this;
            tile.loading = true;
            this._imageLoader.addJob({
              src: tile.getUrl(),           // <-------
    

    this part of the code is a generic one, TiledImage, which is common for everything. And this is a weakness of current(*) OpenSeadragon: the generic code asks for an URL, and not for tile data. So it doesn't matter that the ImageTileSource above stores the entire image in its _image field (and even more (*)), the drawing code never asks for it, it wants and gets an URL, for which it issues a request.

    getTileUrl() of TileImageSource indeed provides that URL without any magics:

            var url = null;
            if (level >= this.minLevel && level <= this.maxLevel) {
                url = this.levels[level].url;
            }
            return url;
    

    When mentioning "magic", I can think of usage of createObjectURL(). Then you would download the image with fetch(), ask for blob(), do the createObjectURL(), and use that URL for both the image.src = line and return it in getTileUrl().
    So if you have your own OpenSeadragon copy, it would become something like

        getImageInfo: function (url) {
            [...]
            // image.src = url;
            fetch(url)
                .then(response => response.blob())
                .then(blob => {
                    image.src = this.objurl = URL.createObjectURL(blob);
                });
        },
    

    and

        getTileUrl: function (level, x, y) {
            return this.objurl;
        },
    

    (*) And why this probably doesn't matter:

    • Browsers have cache. You likely see the 2 requests because you have "Disable cache (while DevTools is open)". If you unmark the option, there will be a single request. At least it happens for me in Chrome
    • They're working on it, see https://github.com/openseadragon/openseadragon/pull/2148 and the levels[] thing in the original getTileUrl() (and _buildLevels() at the end of the file). ImageTileSource already stores the image and even an entire pyramid created from it, just it's not in use, yet.