perf: optimize oss image delivery globally
This commit is contained in:
parent
a6083ac409
commit
a7b9533d72
@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
||||
import { Toaster } from "sonner";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { FaviconUpdater } from "@/components/favicon-updater";
|
||||
import { ImageDeliveryOptimizer } from "@/components/image-delivery-optimizer";
|
||||
|
||||
import "./globals.css";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
@ -171,6 +172,7 @@ export default async function RootLayout({
|
||||
</head>
|
||||
<body className="antialiased">
|
||||
<FaviconUpdater />
|
||||
<ImageDeliveryOptimizer />
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme={defaultTheme}
|
||||
|
||||
123
components/image-delivery-optimizer.tsx
Normal file
123
components/image-delivery-optimizer.tsx
Normal file
@ -0,0 +1,123 @@
|
||||
"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";
|
||||
|
||||
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 optimizeImageElement(image: HTMLImageElement): void {
|
||||
if (image.dataset.imageDelivery === "raw" || image.dataset.imageDelivery === "proxy") {
|
||||
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 optimizeAllImages(root: ParentNode = document): void {
|
||||
root.querySelectorAll("img").forEach((image) => {
|
||||
optimizeImageElement(image);
|
||||
});
|
||||
}
|
||||
|
||||
export function ImageDeliveryOptimizer() {
|
||||
useEffect(() => {
|
||||
let frameId = 0;
|
||||
const scheduleOptimize = (root: ParentNode = document) => {
|
||||
if (frameId) {
|
||||
cancelAnimationFrame(frameId);
|
||||
}
|
||||
frameId = requestAnimationFrame(() => {
|
||||
frameId = 0;
|
||||
optimizeAllImages(root);
|
||||
});
|
||||
};
|
||||
|
||||
scheduleOptimize();
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
if (mutation.type === "attributes") {
|
||||
const target = mutation.target;
|
||||
if (target instanceof HTMLImageElement) {
|
||||
optimizeImageElement(target);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const node of mutation.addedNodes) {
|
||||
if (node instanceof HTMLImageElement) {
|
||||
optimizeImageElement(node);
|
||||
} 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 () => {
|
||||
if (frameId) {
|
||||
cancelAnimationFrame(frameId);
|
||||
}
|
||||
observer.disconnect();
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
@ -99,6 +99,7 @@ export function buildImageProxyUrl(url: string, options: ImageProxyOptions = {})
|
||||
if (directDisplayUrl) {
|
||||
return directDisplayUrl;
|
||||
}
|
||||
return source;
|
||||
}
|
||||
|
||||
const width = normalizeWidth(options.width);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user