feat: limit concurrent image loading

feat/type-attributes
Tomáš Mládek 2023-06-05 12:56:52 +02:00
parent 154c379855
commit 7ee69a0388
2 changed files with 129 additions and 7 deletions

View File

@ -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)}
/>

View 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();
},
};
}