redshift
redshift

Reputation: 5237

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>

Upvotes: 1

Views: 222

Answers (2)

tevemadar
tevemadar

Reputation: 13225

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.

Upvotes: 3

No One
No One

Reputation: 119

The reason why the same image is making two network requests when the "Open Image" button is clicked is that the initViewer() function is called every time when the viewer.vue component is mounted or re-rendered.

Look at what happens when the "Open Image" button is clicked,

  1. the Viewer component is rendered and mounted
  2. It triggers the onMounted hook and calls the initViewer() function.
  3. Then it sends the first network request to load the image. Then, when the show value is changed to false (for example, when the component is unmounted or re-rendered), the Viewer component is destroyed and then re-rendered when the show value is changed back to true, triggering the initViewer() function again and causing another network request to load the image

To avoid this, you can modify your code to call the initViewer function only once, by adding a isViewerInitialized ref variable to track if the viewer has been initialized or not, and checking its value before calling initViewer

UPDATE: I'm sorry, the above case is not the main cause of your trouble. OpenSeadragon is designed to load the image in a separate HTTP request when it is needed for "lazy loading"

When you click the "Open Image" button, the viewer component is loaded and the OpenSeadragon viewer initializes. At this point, the viewer does not yet have the image data and so it makes a separate HTTP request to fetch the image data from the server.

If you want to avoid this behavior and load the image data directly when the viewer is initialized, you can set the immediateRender option to true. This will cause OpenSeadragon to load the image data immediately when the viewer is initialized, instead of waiting until the image is needed.

Here's how you can modify your code to set the immediateRender option:

Source: https://openseadragon.github.io/docs/OpenSeadragon.html#.Options

<script setup>
import OpenSeadragon from 'openseadragon';
import { ref, onMounted } from 'vue';

const viewer = ref(null);
const osdContainer = ref(null);
const isViewerInitialized = ref(false);

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,
      immediateRender: true
    },
  });
  isViewerInitialized.value = true;
};

onMounted(() => {
  if (!isViewerInitialized.value) {
    initViewer();
  }
});
</script>

Personally, I wouldn't mind the two requests and keep the immediateRender: false. It's improving UX there for the lazy load.

Upvotes: 2

Related Questions