Improve gallery loading speed with image optimization and lazy loading
Implement a thumbnail proxy endpoint and lazy loading for gallery images to significantly reduce page load times and improve user experience. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 517dfa7b-26ac-463d-a6e1-a58c6df97188 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 1488855f-6772-48e1-9244-282d1cc91352 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/f209e72a-0939-48fa-84fc-57854de71967/517dfa7b-26ac-463d-a6e1-a58c6df97188/VgutZ7W Replit-Helium-Checkpoint-Created: true
This commit is contained in:
parent
97d09ba27e
commit
39d43cd876
@ -1,5 +1,5 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
import { ChevronLeft, ChevronRight, X, Images, Maximize2 } from "lucide-react";
|
import { ChevronLeft, ChevronRight, X, Images, Maximize2 } from "lucide-react";
|
||||||
|
|
||||||
interface GalleryImage {
|
interface GalleryImage {
|
||||||
@ -9,6 +9,45 @@ interface GalleryImage {
|
|||||||
large: string;
|
large: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function thumbUrl(src: string) {
|
||||||
|
return `/api/gallery/thumb?src=${encodeURIComponent(src)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LazyImage({ src, alt, className, onClick }: { src: string; alt: string; className?: string; onClick?: () => void }) {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
const [loaded, setLoaded] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = ref.current;
|
||||||
|
if (!el) return;
|
||||||
|
const obs = new IntersectionObserver(
|
||||||
|
([entry]) => { if (entry.isIntersecting) { setVisible(true); obs.disconnect(); } },
|
||||||
|
{ rootMargin: "200px" }
|
||||||
|
);
|
||||||
|
obs.observe(el);
|
||||||
|
return () => obs.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} className={className} onClick={onClick}>
|
||||||
|
{visible ? (
|
||||||
|
<>
|
||||||
|
{!loaded && <div className="absolute inset-0 bg-muted animate-pulse" />}
|
||||||
|
<img
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
className={`w-full h-full object-cover transition-opacity duration-300 ${loaded ? "opacity-100" : "opacity-0"}`}
|
||||||
|
onLoad={() => setLoaded(true)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="w-full h-full bg-muted" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function Lightbox({
|
function Lightbox({
|
||||||
images,
|
images,
|
||||||
startIndex,
|
startIndex,
|
||||||
@ -242,11 +281,10 @@ export default function GalleryPage() {
|
|||||||
className="group relative aspect-square rounded-lg overflow-hidden bg-card border border-card-border cursor-pointer"
|
className="group relative aspect-square rounded-lg overflow-hidden bg-card border border-card-border cursor-pointer"
|
||||||
data-testid={`button-gallery-image-${i}`}
|
data-testid={`button-gallery-image-${i}`}
|
||||||
>
|
>
|
||||||
<img
|
<LazyImage
|
||||||
src={img.thumb}
|
src={thumbUrl(img.thumb)}
|
||||||
alt={img.fileName}
|
alt={img.fileName}
|
||||||
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
|
className="absolute inset-0 w-full h-full transition-transform duration-500 group-hover:scale-110"
|
||||||
loading="lazy"
|
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors" />
|
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors" />
|
||||||
<div className="absolute bottom-0 left-0 right-0 p-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
<div className="absolute bottom-0 left-0 right-0 p-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
|
|||||||
@ -114,7 +114,7 @@ async function refreshAccessToken(refreshToken: string): Promise<DropboxTokens>
|
|||||||
return tokens;
|
return tokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getValidAccessToken(): Promise<string | null> {
|
export async function getValidAccessToken(): Promise<string | null> {
|
||||||
let tokens = loadTokens();
|
let tokens = loadTokens();
|
||||||
if (!tokens) return null;
|
if (!tokens) return null;
|
||||||
|
|
||||||
@ -200,6 +200,7 @@ async function getTemporaryLinks(accessToken: string, paths: string[]): Promise<
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const galleryCache: { data: GalleryImage[]; timestamp: number } = { data: [], timestamp: 0 };
|
const galleryCache: { data: GalleryImage[]; timestamp: number } = { data: [], timestamp: 0 };
|
||||||
const CACHE_DURATION = 30 * 60 * 1000;
|
const CACHE_DURATION = 30 * 60 * 1000;
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -6,7 +6,7 @@ import { seedDatabase } from "./seed";
|
|||||||
import { generateDailyHoroscopes, getHoroscopesForToday, getOrGenerateHoroscope } from "./horoscope-generator";
|
import { generateDailyHoroscopes, getHoroscopesForToday, getOrGenerateHoroscope } from "./horoscope-generator";
|
||||||
import { analyzeAllArticleImages, getCachedFocalPoints } from "./focal-point";
|
import { analyzeAllArticleImages, getCachedFocalPoints } from "./focal-point";
|
||||||
import { optimizeImage } from "./image-optimizer";
|
import { optimizeImage } from "./image-optimizer";
|
||||||
import { getAuthUrl, exchangeCodeForTokens, isConnected, fetchGalleryFromDropbox } from "./dropbox";
|
import { getAuthUrl, exchangeCodeForTokens, isConnected, fetchGalleryFromDropbox, getValidAccessToken } from "./dropbox";
|
||||||
import multer from "multer";
|
import multer from "multer";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
@ -233,6 +233,48 @@ export async function registerRoutes(
|
|||||||
res.json({ connected: isConnected() });
|
res.json({ connected: isConnected() });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const thumbCache = new Map<string, { data: Buffer; timestamp: number }>();
|
||||||
|
const THUMB_CACHE_TTL = 30 * 60 * 1000;
|
||||||
|
|
||||||
|
app.get("/api/gallery/thumb", async (req, res) => {
|
||||||
|
const src = req.query.src as string;
|
||||||
|
if (!src) return res.status(400).send("Missing src");
|
||||||
|
|
||||||
|
const cached = thumbCache.get(src);
|
||||||
|
if (cached && Date.now() - cached.timestamp < THUMB_CACHE_TTL) {
|
||||||
|
res.set("Content-Type", "image/jpeg");
|
||||||
|
res.set("Cache-Control", "public, max-age=1800");
|
||||||
|
return res.send(cached.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch(src);
|
||||||
|
if (!resp.ok) return res.status(502).send("Upstream error");
|
||||||
|
|
||||||
|
const arrayBuf = await resp.arrayBuffer();
|
||||||
|
const fullBuf = Buffer.from(arrayBuf);
|
||||||
|
|
||||||
|
const sharp = (await import("sharp")).default;
|
||||||
|
const thumbBuf = await sharp(fullBuf)
|
||||||
|
.resize(400, 400, { fit: "cover", position: "attention" })
|
||||||
|
.jpeg({ quality: 70 })
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
thumbCache.set(src, { data: thumbBuf, timestamp: Date.now() });
|
||||||
|
|
||||||
|
if (thumbCache.size > 300) {
|
||||||
|
const oldest = [...thumbCache.entries()].sort((a, b) => a[1].timestamp - b[1].timestamp);
|
||||||
|
for (let i = 0; i < 50; i++) thumbCache.delete(oldest[i][0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.set("Content-Type", "image/jpeg");
|
||||||
|
res.set("Cache-Control", "public, max-age=1800");
|
||||||
|
res.send(thumbBuf);
|
||||||
|
} catch (err: any) {
|
||||||
|
res.status(500).send(err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.post("/api/dropbox/refresh-gallery", async (_req, res) => {
|
app.post("/api/dropbox/refresh-gallery", async (_req, res) => {
|
||||||
try {
|
try {
|
||||||
const images = await fetchGalleryFromDropbox();
|
const images = await fetchGalleryFromDropbox();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user