feat: limit concurrent image loading
parent
154c379855
commit
7ee69a0388
|
@ -9,6 +9,7 @@
|
|||
import { createEventDispatcher } from "svelte";
|
||||
import { getTypes } from "../../util/mediatypes";
|
||||
import { formatDuration } from "../../util/fragments/time";
|
||||
import { concurrentImage } from "../imageQueue";
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let address: string;
|
||||
|
@ -94,8 +95,8 @@
|
|||
/>
|
||||
{:else if types.web}
|
||||
<img
|
||||
src={String($entity?.get("OG_IMAGE"))}
|
||||
alt="OpenGraph image for {$entityInfo?.t == 'Url' && $entityInfo?.c}"
|
||||
use:concurrentImage={String($entity?.get("OG_IMAGE"))}
|
||||
on:load={() => (loaded = address)}
|
||||
on:error={() => (handled = false)}
|
||||
/>
|
||||
|
@ -108,9 +109,8 @@
|
|||
{:else if types.audio}
|
||||
<div class="audiopreview image">
|
||||
<img
|
||||
src="{api.apiUrl}/thumb/{address}?mime=audio"
|
||||
alt="Thumbnail for {address}..."
|
||||
loading="lazy"
|
||||
use:concurrentImage={`${api.apiUrl}/thumb/${address}?mime=audio`}
|
||||
on:load={() => (loaded = address)}
|
||||
on:error={() => (handled = false)}
|
||||
/>
|
||||
|
@ -129,11 +129,10 @@
|
|||
{:else}
|
||||
<div class="image" class:loaded={loaded == address || !handled}>
|
||||
<img
|
||||
src="{api.apiUrl}/{types.mimeType?.includes('svg+xml')
|
||||
? 'raw'
|
||||
: 'thumb'}/{address}?size=512&quality=75"
|
||||
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:error={() => (handled = false)}
|
||||
/>
|
||||
|
|
|
@ -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 New Issue