ai-chatbot/components/image-delivery-optimizer.tsx
Your Name 68eab912d8
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
perf: optimize agent style rendering
2026-05-20 18:10:43 +08:00

202 lines
5.4 KiB
TypeScript

"use client";
import { useEffect } from "react";
import { buildImageProxyUrl } from "@/lib/image-delivery";
const MIN_DISPLAY_WIDTH = 160;
const DEFAULT_DISPLAY_WIDTH = 960;
const MAX_DISPLAY_WIDTH = 2200;
const OPTIMIZED_MARK = "ossDisplayOptimized";
const MANUAL_SCOPE_ATTR = "data-image-delivery-scope";
const BATCH_SIZE = 48;
type IdleDeadlineLike = {
timeRemaining: () => number;
};
type IdleWindow = Window & {
requestIdleCallback?: (
callback: (deadline: IdleDeadlineLike) => void,
options?: { timeout?: number }
) => number;
cancelIdleCallback?: (id: number) => void;
};
function isSkippableImageUrl(url: string): boolean {
const normalized = url.trim().toLowerCase();
return (
!normalized ||
normalized.startsWith("data:") ||
normalized.startsWith("blob:") ||
normalized.startsWith("/api/image-proxy") ||
normalized.includes("x-oss-process=") ||
/\.(svg|gif|ico)(?:[?#]|$)/i.test(normalized)
);
}
function getDisplayWidth(image: HTMLImageElement): number {
const rectWidth = image.getBoundingClientRect().width;
const width =
rectWidth ||
image.clientWidth ||
Number.parseInt(image.getAttribute("width") || "", 10) ||
DEFAULT_DISPLAY_WIDTH;
const dpr = Math.min(2, Math.max(1, window.devicePixelRatio || 1));
return Math.round(Math.max(MIN_DISPLAY_WIDTH, Math.min(MAX_DISPLAY_WIDTH, width * dpr)));
}
function isInManualDeliveryScope(image: HTMLImageElement): boolean {
if (
image.dataset.imageDelivery === "raw" ||
image.dataset.imageDelivery === "proxy" ||
image.dataset.imageDelivery === "manual"
) {
return true;
}
return Boolean(image.closest(`[${MANUAL_SCOPE_ATTR}="manual"]`));
}
function optimizeImageElement(image: HTMLImageElement): void {
if (isInManualDeliveryScope(image)) {
return;
}
const currentSrc = image.currentSrc || image.src || image.getAttribute("src") || "";
if (isSkippableImageUrl(currentSrc)) {
return;
}
const optimizedUrl = buildImageProxyUrl(currentSrc, {
width: getDisplayWidth(image),
quality: 84,
format: "webp",
});
if (
!optimizedUrl ||
optimizedUrl === currentSrc ||
optimizedUrl.startsWith("/api/image-proxy")
) {
return;
}
image.dataset[OPTIMIZED_MARK] = "1";
image.src = optimizedUrl;
}
function collectImages(root: ParentNode = document): HTMLImageElement[] {
if (root instanceof HTMLImageElement) {
return [root];
}
if (root instanceof Element && root.closest(`[${MANUAL_SCOPE_ATTR}="manual"]`)) {
return [];
}
return Array.from(root.querySelectorAll("img"));
}
export function ImageDeliveryOptimizer() {
useEffect(() => {
const pendingImages = new Set<HTMLImageElement>();
const idleWindow = window as IdleWindow;
let scheduledId = 0;
let scheduledWithIdle = false;
const cancelScheduledFlush = () => {
if (!scheduledId) return;
if (scheduledWithIdle && typeof idleWindow.cancelIdleCallback === "function") {
idleWindow.cancelIdleCallback(scheduledId);
} else {
cancelAnimationFrame(scheduledId);
}
scheduledId = 0;
scheduledWithIdle = false;
};
const scheduleFlush = () => {
if (scheduledId) return;
const flush = (deadline?: IdleDeadlineLike) => {
scheduledId = 0;
scheduledWithIdle = false;
let processed = 0;
for (const image of Array.from(pendingImages)) {
pendingImages.delete(image);
if (image.isConnected) {
optimizeImageElement(image);
}
processed += 1;
if (
processed >= BATCH_SIZE ||
(deadline && deadline.timeRemaining() < 4)
) {
break;
}
}
if (pendingImages.size > 0) {
scheduleFlush();
}
};
if (typeof idleWindow.requestIdleCallback === "function") {
scheduledWithIdle = true;
scheduledId = idleWindow.requestIdleCallback(flush, { timeout: 500 });
return;
}
scheduledId = requestAnimationFrame(() => {
flush({ timeRemaining: () => 8 });
});
};
const scheduleOptimize = (root: ParentNode = document) => {
for (const image of collectImages(root)) {
pendingImages.add(image);
}
if (pendingImages.size > 0) {
scheduleFlush();
}
};
scheduleOptimize();
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === "attributes") {
const target = mutation.target;
if (target instanceof HTMLImageElement) {
pendingImages.add(target);
scheduleFlush();
}
continue;
}
for (const node of mutation.addedNodes) {
if (node instanceof HTMLImageElement) {
pendingImages.add(node);
scheduleFlush();
} else if (node instanceof Element) {
scheduleOptimize(node);
}
}
}
});
observer.observe(document.body, {
attributes: true,
attributeFilter: ["src"],
childList: true,
subtree: true,
});
const handleResize = () => scheduleOptimize();
window.addEventListener("resize", handleResize, { passive: true });
return () => {
cancelScheduledFlush();
observer.disconnect();
window.removeEventListener("resize", handleResize);
};
}, []);
return null;
}