Reputation: 5237
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?
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
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:
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
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,
onMounted
hook and calls the initViewer()
function.initViewer()
function again and causing another network request to load the imageTo 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