From b62b44eab9139ece8c4c32bf9b8871b97e901695 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Thu, 20 Feb 2025 22:33:39 +0100 Subject: [PATCH] feat: perf optim search --- src/app/layout.tsx | 2 +- src/app/settings/page.tsx | 4 -- src/components/home/HeroSection.tsx | 12 ++-- src/components/home/HomeContent.tsx | 53 +++++++++------ src/components/home/MediaRow.tsx | 34 ++++++++-- src/components/series/SeriesHeader.tsx | 2 - src/components/ui/cover-client.tsx | 58 ++++++++++++++++ src/components/ui/cover.tsx | 94 +++++--------------------- src/lib/services/base-api.service.ts | 20 +++++- src/lib/services/book.service.ts | 21 +++--- src/lib/services/image.service.ts | 7 +- src/lib/services/series.service.ts | 10 ++- 12 files changed, 175 insertions(+), 142 deletions(-) create mode 100644 src/components/ui/cover-client.tsx diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 6a10d13..40ef9be 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -15,7 +15,7 @@ export const metadata: Metadata = { manifest: "/manifest.json", keywords: ["comics", "manga", "bd", "reader", "komga", "stripstream"], authors: [{ name: "Julien Froidefond" }], - colorScheme: "dark light", + // colorScheme: "dark light", formatDetection: { telephone: false, }, diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx index 47625f0..e7be094 100644 --- a/src/app/settings/page.tsx +++ b/src/app/settings/page.tsx @@ -7,10 +7,6 @@ export const metadata: Metadata = { description: "Configurez vos préférences StripStream", }; -export const viewport = { - colorScheme: "dark light", -}; - export default async function SettingsPage() { let config = null; let ttlConfig = null; diff --git a/src/components/home/HeroSection.tsx b/src/components/home/HeroSection.tsx index c537b13..2446c4f 100644 --- a/src/components/home/HeroSection.tsx +++ b/src/components/home/HeroSection.tsx @@ -1,10 +1,14 @@ -"use client"; - -import { KomgaSeries } from "@/types/komga"; import { Cover } from "@/components/ui/cover"; +interface OptimizedHeroSeries { + id: string; + metadata: { + title: string; + }; +} + interface HeroSectionProps { - series: KomgaSeries[]; + series: OptimizedHeroSeries[]; } export function HeroSection({ series }: HeroSectionProps) { diff --git a/src/components/home/HomeContent.tsx b/src/components/home/HomeContent.tsx index a6ea7e6..ea41b02 100644 --- a/src/components/home/HomeContent.tsx +++ b/src/components/home/HomeContent.tsx @@ -1,9 +1,6 @@ -"use client"; - import { HeroSection } from "./HeroSection"; import { MediaRow } from "./MediaRow"; import { KomgaBook, KomgaSeries } from "@/types/komga"; -import { useRouter } from "next/navigation"; interface HomeData { ongoing: KomgaSeries[]; @@ -16,13 +13,6 @@ interface HomeContentProps { } export function HomeContent({ data }: HomeContentProps) { - const router = useRouter(); - - const handleItemClick = async (item: KomgaSeries | KomgaBook) => { - const path = "booksCount" in item ? `/series/${item.id}` : `/books/${item.id}`; - await router.push(path); - }; - // Vérification des données pour le debug // console.log("HomeContent - Données reçues:", { // ongoingCount: data.ongoing?.length || 0, @@ -30,31 +20,50 @@ export function HomeContent({ data }: HomeContentProps) { // onDeckCount: data.onDeck?.length || 0, // }); + const optimizeSeriesData = (series: KomgaSeries[]) => { + return series.map(({ id, metadata, booksCount }) => ({ + id, + metadata: { title: metadata.title }, + booksCount, + })); + }; + + const optimizeHeroSeriesData = (series: KomgaSeries[]) => { + return series.map(({ id, metadata }) => ({ + id, + metadata: { title: metadata.title }, + })); + }; + + const optimizeBookData = (books: KomgaBook[]) => { + return books.map(({ id, metadata }) => ({ + id, + metadata: { + title: metadata.title, + number: metadata.number, + }, + })); + }; + return (
{/* Hero Section - Afficher uniquement si nous avons des séries en cours */} - {data.ongoing && data.ongoing.length > 0 && } + {data.ongoing && data.ongoing.length > 0 && ( + + )} {/* Sections de contenu */}
{data.ongoing && data.ongoing.length > 0 && ( - + )} {data.onDeck && data.onDeck.length > 0 && ( - + )} {data.recentlyRead && data.recentlyRead.length > 0 && ( - + )}
diff --git a/src/components/home/MediaRow.tsx b/src/components/home/MediaRow.tsx index 5cdfadb..26c4708 100644 --- a/src/components/home/MediaRow.tsx +++ b/src/components/home/MediaRow.tsx @@ -1,20 +1,43 @@ "use client"; -import { KomgaBook, KomgaSeries } from "@/types/komga"; import { ChevronLeft, ChevronRight } from "lucide-react"; import { useRef, useState } from "react"; import { Cover } from "@/components/ui/cover"; +import { useRouter } from "next/navigation"; + +interface BaseItem { + id: string; + metadata: { + title: string; + }; +} + +interface OptimizedSeries extends BaseItem { + booksCount: number; +} + +interface OptimizedBook extends BaseItem { + metadata: { + title: string; + number?: string; + }; +} interface MediaRowProps { title: string; - items: (KomgaSeries | KomgaBook)[]; - onItemClick?: (item: KomgaSeries | KomgaBook) => void; + items: (OptimizedSeries | OptimizedBook)[]; } -export function MediaRow({ title, items, onItemClick }: MediaRowProps) { +export function MediaRow({ title, items }: MediaRowProps) { const scrollContainerRef = useRef(null); const [showLeftArrow, setShowLeftArrow] = useState(false); const [showRightArrow, setShowRightArrow] = useState(true); + const router = useRouter(); + + const onItemClick = (item: OptimizedSeries | OptimizedBook) => { + const path = "booksCount" in item ? `/series/${item.id}` : `/books/${item.id}`; + router.push(path); + }; const handleScroll = () => { if (!scrollContainerRef.current) return; @@ -75,12 +98,11 @@ export function MediaRow({ title, items, onItemClick }: MediaRowProps) { } interface MediaCardProps { - item: KomgaSeries | KomgaBook; + item: OptimizedSeries | OptimizedBook; onClick?: () => void; } function MediaCard({ item, onClick }: MediaCardProps) { - // Déterminer si c'est une série ou un livre const isSeries = "booksCount" in item; const title = isSeries ? item.metadata.title diff --git a/src/components/series/SeriesHeader.tsx b/src/components/series/SeriesHeader.tsx index b15b45f..a6b441d 100644 --- a/src/components/series/SeriesHeader.tsx +++ b/src/components/series/SeriesHeader.tsx @@ -102,7 +102,6 @@ export const SeriesHeader = ({ series }: SeriesHeaderProps) => { alt={`Couverture de ${series.metadata.title}`} className="blur-sm scale-105 brightness-50" quality={60} - priority /> @@ -116,7 +115,6 @@ export const SeriesHeader = ({ series }: SeriesHeaderProps) => { id={series.id} alt={`Couverture de ${series.metadata.title}`} quality={90} - priority /> diff --git a/src/components/ui/cover-client.tsx b/src/components/ui/cover-client.tsx new file mode 100644 index 0000000..f33126d --- /dev/null +++ b/src/components/ui/cover-client.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { ImageOff } from "lucide-react"; +import Image from "next/image"; +import { useState } from "react"; +import { cn } from "@/lib/utils"; +import { ImageLoader } from "@/components/ui/image-loader"; + +interface CoverClientProps { + imageUrl: string; + alt: string; + className?: string; + quality?: number; + sizes?: string; + isCompleted?: boolean; +} + +export const CoverClient = ({ + imageUrl, + alt, + className, + quality = 80, + sizes = "100vw", + isCompleted = false, +}: CoverClientProps) => { + const [imageError, setImageError] = useState(false); + const [isLoading, setIsLoading] = useState(true); + + if (imageError) { + return ( +
+ +
+ ); + } + + return ( +
+ + {alt} setImageError(true)} + onLoad={() => setIsLoading(false)} + loading={"lazy"} + quality={quality} + /> +
+ ); +}; diff --git a/src/components/ui/cover.tsx b/src/components/ui/cover.tsx index b798855..39e0e5d 100644 --- a/src/components/ui/cover.tsx +++ b/src/components/ui/cover.tsx @@ -1,101 +1,41 @@ -"use client"; - -import { ImageOff } from "lucide-react"; -import Image from "next/image"; -import { useState, useEffect, useRef, useCallback } from "react"; -import { cn } from "@/lib/utils"; -import { ImageLoader } from "@/components/ui/image-loader"; +import { CoverClient } from "./cover-client"; interface CoverProps { type: "series" | "book"; id: string; alt?: string; className?: string; - priority?: boolean; quality?: number; sizes?: string; isCompleted?: boolean; } +function getImageUrl(type: "series" | "book", id: string) { + if (type === "series") { + return `/api/komga/images/series/${id}/thumbnail`; + } + return `/api/komga/images/books/${id}/thumbnail`; +} + export function Cover({ type, id, alt = "Image de couverture", className, - priority = false, quality = 80, sizes = "100vw", isCompleted = false, }: CoverProps) { - const [imageError, setImageError] = useState(false); - const [isLoading, setIsLoading] = useState(true); - const [imageUrl, setImageUrl] = useState(null); - const coverRef = useRef(null); - - const getImageUrl = useCallback(() => { - if (type === "series") { - return `/api/komga/images/series/${id}/thumbnail`; - } - return `/api/komga/images/books/${id}/thumbnail`; - }, [type, id]); - - // Observer pour détecter quand la cover est dans le viewport - useEffect(() => { - const observer = new IntersectionObserver( - (entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting && !imageUrl) { - setImageUrl(getImageUrl()); - } - }); - }, - { - rootMargin: "50px", // Préchargement avant que l'élément soit visible - } - ); - - const element = coverRef.current; - if (element) { - observer.observe(element); - } - - return () => { - if (element) { - observer.unobserve(element); - } - }; - }, [id, imageUrl, getImageUrl]); - - if (imageError) { - return ( -
- -
- ); - } + const imageUrl = getImageUrl(type, id); return ( -
- - {imageUrl && ( - {alt} setImageError(true)} - onLoad={() => setIsLoading(false)} - loading={priority ? "eager" : "lazy"} - quality={quality} - priority={priority} - /> - )} -
+ ); } diff --git a/src/lib/services/base-api.service.ts b/src/lib/services/base-api.service.ts index ae73ff7..b9dae68 100644 --- a/src/lib/services/base-api.service.ts +++ b/src/lib/services/base-api.service.ts @@ -73,13 +73,29 @@ export abstract class BaseApiService { return url.toString(); } - protected static async fetchFromApi(url: string, headers: Headers): Promise { + protected static async fetchFromApi( + url: string, + headers: Headers, + isImage: boolean = false + ): Promise { + // const startTime = Date.now(); // Capture le temps de début + const response = await fetch(url, { headers }); + // const endTime = Date.now(); // Capture le temps de fin + // const responseTime = endTime - startTime; // Calcule le temps de réponse + + // // Log le temps de réponse en ms ou en s + // if (responseTime >= 1000) { + // console.log(`Temps de réponse pour ${url}: ${(responseTime / 1000).toFixed(2)}s`); + // } else { + // console.log(`Temps de réponse pour ${url}: ${responseTime}ms`); + // } + if (!response.ok) { throw new Error(`Erreur HTTP: ${response.status} ${response.statusText}`); } - return response.json(); + return isImage ? response : response.json(); } } diff --git a/src/lib/services/book.service.ts b/src/lib/services/book.service.ts index 12f7659..05c6532 100644 --- a/src/lib/services/book.service.ts +++ b/src/lib/services/book.service.ts @@ -5,6 +5,7 @@ import { PreferencesService } from "./preferences.service"; export class BookService extends BaseApiService { static async getBook(bookId: string): Promise<{ book: KomgaBook; pages: number[] }> { + console.log("dzadaz"); try { const config = await this.getKomgaConfig(); const headers = this.getAuthHeaders(config); @@ -13,20 +14,16 @@ export class BookService extends BaseApiService { `book-${bookId}`, async () => { // Récupération des détails du tome - const bookResponse = await fetch(this.buildUrl(config, `books/${bookId}`), { headers }); - if (!bookResponse.ok) { - throw new Error("Erreur lors de la récupération des détails du tome"); - } - const book = await bookResponse.json(); + const book = await this.fetchFromApi( + this.buildUrl(config, `books/${bookId}`), + headers + ); // Récupération des pages du tome - const pagesResponse = await fetch(this.buildUrl(config, `books/${bookId}/pages`), { - headers, - }); - if (!pagesResponse.ok) { - throw new Error("Erreur lors de la récupération des pages du tome"); - } - const pages = await pagesResponse.json(); + const pages = await this.fetchFromApi<{ number: number }[]>( + this.buildUrl(config, `books/${bookId}/pages`), + headers + ); return { book, diff --git a/src/lib/services/image.service.ts b/src/lib/services/image.service.ts index 7e4b467..40830c5 100644 --- a/src/lib/services/image.service.ts +++ b/src/lib/services/image.service.ts @@ -16,12 +16,7 @@ export class ImageService extends BaseApiService { return this.fetchWithCache( `image-${path}`, async () => { - const response = await fetch(url, { headers }); - - if (!response.ok) { - throw new Error(`Erreur HTTP: ${response.status} ${response.statusText}`); - } - + const response = await this.fetchFromApi(url, headers, true); const contentType = response.headers.get("content-type"); const arrayBuffer = await response.arrayBuffer(); const buffer = Buffer.from(arrayBuffer); diff --git a/src/lib/services/series.service.ts b/src/lib/services/series.service.ts index 1c604fc..19c47a3 100644 --- a/src/lib/services/series.service.ts +++ b/src/lib/services/series.service.ts @@ -57,12 +57,10 @@ export class SeriesService extends BaseApiService { return this.fetchWithCache( `series-first-book-${seriesId}`, async () => { - const response = await fetch(`${url}?page=0&size=1`, { headers }); - if (!response.ok) { - throw new Error(`Erreur HTTP: ${response.status}`); - } - - const data = await response.json(); + const data = await this.fetchFromApi>( + `series/${seriesId}/books?page=0&size=1`, + headers + ); if (!data.content || data.content.length === 0) { throw new Error("Aucun livre trouvé dans la série"); }