feat: limit concurrent image loading
This commit is contained in:
parent
154c379855
commit
7ee69a0388
2 changed files with 129 additions and 7 deletions
|
@ -9,6 +9,7 @@
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
import { getTypes } from "../../util/mediatypes";
|
import { getTypes } from "../../util/mediatypes";
|
||||||
import { formatDuration } from "../../util/fragments/time";
|
import { formatDuration } from "../../util/fragments/time";
|
||||||
|
import { concurrentImage } from "../imageQueue";
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let address: string;
|
export let address: string;
|
||||||
|
@ -94,8 +95,8 @@
|
||||||
/>
|
/>
|
||||||
{:else if types.web}
|
{:else if types.web}
|
||||||
<img
|
<img
|
||||||
src={String($entity?.get("OG_IMAGE"))}
|
|
||||||
alt="OpenGraph image for {$entityInfo?.t == 'Url' && $entityInfo?.c}"
|
alt="OpenGraph image for {$entityInfo?.t == 'Url' && $entityInfo?.c}"
|
||||||
|
use:concurrentImage={String($entity?.get("OG_IMAGE"))}
|
||||||
on:load={() => (loaded = address)}
|
on:load={() => (loaded = address)}
|
||||||
on:error={() => (handled = false)}
|
on:error={() => (handled = false)}
|
||||||
/>
|
/>
|
||||||
|
@ -108,9 +109,8 @@
|
||||||
{:else if types.audio}
|
{:else if types.audio}
|
||||||
<div class="audiopreview image">
|
<div class="audiopreview image">
|
||||||
<img
|
<img
|
||||||
src="{api.apiUrl}/thumb/{address}?mime=audio"
|
|
||||||
alt="Thumbnail for {address}..."
|
alt="Thumbnail for {address}..."
|
||||||
loading="lazy"
|
use:concurrentImage={`${api.apiUrl}/thumb/${address}?mime=audio`}
|
||||||
on:load={() => (loaded = address)}
|
on:load={() => (loaded = address)}
|
||||||
on:error={() => (handled = false)}
|
on:error={() => (handled = false)}
|
||||||
/>
|
/>
|
||||||
|
@ -129,11 +129,10 @@
|
||||||
{:else}
|
{:else}
|
||||||
<div class="image" class:loaded={loaded == address || !handled}>
|
<div class="image" class:loaded={loaded == address || !handled}>
|
||||||
<img
|
<img
|
||||||
src="{api.apiUrl}/{types.mimeType?.includes('svg+xml')
|
|
||||||
? 'raw'
|
|
||||||
: 'thumb'}/{address}?size=512&quality=75"
|
|
||||||
alt="Thumbnail for {address}..."
|
alt="Thumbnail for {address}..."
|
||||||
loading="lazy"
|
use:concurrentImage={`${api.apiUrl}/${
|
||||||
|
types.mimeType?.includes("svg+xml") ? "raw" : "thumb"
|
||||||
|
}/${address}?size=512&quality=75`}
|
||||||
on:load={() => (loaded = address)}
|
on:load={() => (loaded = address)}
|
||||||
on:error={() => (handled = false)}
|
on:error={() => (handled = false)}
|
||||||
/>
|
/>
|
||||||
|
|
123
webui/src/components/imageQueue.ts
Normal file
123
webui/src/components/imageQueue.ts
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
import debug from "debug";
|
||||||
|
const dbg = debug("upend:imageQueue");
|
||||||
|
|
||||||
|
class ImageQueue {
|
||||||
|
concurrency: number;
|
||||||
|
queue: {
|
||||||
|
element: HTMLElement;
|
||||||
|
id: string;
|
||||||
|
callback: () => Promise<void>;
|
||||||
|
check?: () => boolean;
|
||||||
|
}[] = [];
|
||||||
|
active = 0;
|
||||||
|
|
||||||
|
constructor(concurrency: number) {
|
||||||
|
this.concurrency = concurrency;
|
||||||
|
}
|
||||||
|
|
||||||
|
public add(
|
||||||
|
element: HTMLImageElement,
|
||||||
|
id: string,
|
||||||
|
callback: () => Promise<void>,
|
||||||
|
order?: () => number,
|
||||||
|
check?: () => boolean
|
||||||
|
) {
|
||||||
|
this.queue = this.queue.filter((e) => e.element !== element);
|
||||||
|
this.queue.push({ element, id, callback, check });
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
private update() {
|
||||||
|
this.queue.sort((a, b) => {
|
||||||
|
const aBox = a.element.getBoundingClientRect();
|
||||||
|
const bBox = b.element.getBoundingClientRect();
|
||||||
|
const topDifference = aBox.top - bBox.top;
|
||||||
|
if (topDifference !== 0) {
|
||||||
|
return topDifference;
|
||||||
|
} else {
|
||||||
|
return aBox.left - bBox.left;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dbg(
|
||||||
|
"Active: %d, Queue: %O",
|
||||||
|
this.active,
|
||||||
|
this.queue.map((e) => [e.element, e.id])
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.active >= this.concurrency) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.queue.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nextIdx = this.queue.findIndex((e) => e.check()) || 0;
|
||||||
|
const next = this.queue.splice(nextIdx, 1)[0];
|
||||||
|
dbg(`Getting ${next.id}...`);
|
||||||
|
this.active += 1;
|
||||||
|
next
|
||||||
|
.callback()
|
||||||
|
.then(() => {
|
||||||
|
dbg(`Loaded ${next.id}`);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
dbg(`Failed to load ${next.id}...`);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.active -= 1;
|
||||||
|
this.update();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageQueue = new ImageQueue(1);
|
||||||
|
|
||||||
|
export function concurrentImage(element: HTMLImageElement, src: string) {
|
||||||
|
const bbox = element.getBoundingClientRect();
|
||||||
|
let visible =
|
||||||
|
bbox.top >= 0 &&
|
||||||
|
bbox.left >= 0 &&
|
||||||
|
bbox.bottom <= window.innerHeight &&
|
||||||
|
bbox.right <= window.innerWidth;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
visible = entries.some((e) => e.isIntersecting);
|
||||||
|
});
|
||||||
|
observer.observe(element);
|
||||||
|
|
||||||
|
function queueSelf() {
|
||||||
|
const loadSelf = () => {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
if (element.src === src) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
element.addEventListener("load", () => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
element.addEventListener("error", () => {
|
||||||
|
reject();
|
||||||
|
});
|
||||||
|
element.src = src;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
imageQueue.add(
|
||||||
|
element,
|
||||||
|
src,
|
||||||
|
loadSelf,
|
||||||
|
() => element.getBoundingClientRect().top,
|
||||||
|
() => visible
|
||||||
|
);
|
||||||
|
}
|
||||||
|
queueSelf();
|
||||||
|
|
||||||
|
return {
|
||||||
|
update(_src: string) {
|
||||||
|
queueSelf();
|
||||||
|
},
|
||||||
|
destroy() {
|
||||||
|
observer.disconnect();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in a new issue