feat: perf optimisation
Some checks failed
Deploy with Docker Compose / deploy (push) Failing after 2s

This commit is contained in:
2026-02-27 16:23:05 +01:00
parent bcfd602353
commit 0c3a54c62c
20 changed files with 883 additions and 489 deletions

View File

@@ -7,6 +7,8 @@ import type { KomgaBookWithPages } from "@/types/komga";
import type { NextRequest } from "next/server";
import logger from "@/lib/logger";
// Cache handled in service via fetchFromApi options
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ bookId: string }> }

View File

@@ -5,6 +5,8 @@ import { AppError } from "@/utils/errors";
import { getErrorMessage } from "@/utils/errors";
import logger from "@/lib/logger";
// Cache handled in service via fetchFromApi options
export async function GET() {
try {
const data = await HomeService.getHomeData();

View File

@@ -6,6 +6,8 @@ import { getErrorMessage } from "@/utils/errors";
import type { NextRequest } from "next/server";
import logger from "@/lib/logger";
// Cache handled in service via fetchFromApi options
const DEFAULT_PAGE_SIZE = 20;
export async function GET(

View File

@@ -5,7 +5,8 @@ import { AppError } from "@/utils/errors";
import type { KomgaLibrary } from "@/types/komga";
import { getErrorMessage } from "@/utils/errors";
import logger from "@/lib/logger";
export const dynamic = "force-dynamic";
// Cache handled in service via fetchFromApi options
export async function GET() {
try {

View File

@@ -6,6 +6,8 @@ import { getErrorMessage } from "@/utils/errors";
import type { NextRequest } from "next/server";
import logger from "@/lib/logger";
// Cache handled in service via fetchFromApi options
const DEFAULT_PAGE_SIZE = 20;
export async function GET(

View File

@@ -93,7 +93,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
}
if (librariesData.status === "fulfilled") {
libraries = librariesData.value;
libraries = librariesData.value || [];
}
if (favoritesData.status === "fulfilled") {

View File

@@ -8,6 +8,8 @@ import logger from "@/lib/logger";
interface KomgaRequestInit extends RequestInit {
isImage?: boolean;
noJson?: boolean;
/** Next.js cache duration in seconds. Use false to disable cache, number for TTL */
revalidate?: number | false;
}
interface KomgaUrlBuilder {
@@ -90,7 +92,8 @@ export abstract class BaseApiService {
}
const isDebug = process.env.KOMGA_DEBUG === "true";
const startTime = isDebug ? Date.now() : 0;
const isCacheDebug = process.env.CACHE_DEBUG === "true";
const startTime = isDebug || isCacheDebug ? Date.now() : 0;
if (isDebug) {
logger.info(
@@ -100,11 +103,23 @@ export abstract class BaseApiService {
params,
isImage: options.isImage,
noJson: options.noJson,
revalidate: options.revalidate,
},
"🔵 Komga Request"
);
}
if (isCacheDebug && options.revalidate) {
logger.info(
{
url,
cache: "enabled",
ttl: options.revalidate,
},
"💾 Cache enabled"
);
}
// Timeout de 15 secondes pour éviter les blocages longs
const timeoutMs = 15000;
const controller = new AbortController();
@@ -122,6 +137,10 @@ export abstract class BaseApiService {
connectTimeout: timeoutMs,
bodyTimeout: timeoutMs,
headersTimeout: timeoutMs,
// Next.js cache
next: options.revalidate !== undefined
? { revalidate: options.revalidate }
: undefined,
});
} catch (fetchError: any) {
// Gestion spécifique des erreurs DNS
@@ -139,6 +158,10 @@ export abstract class BaseApiService {
// Force IPv4 si IPv6 pose problème
// @ts-ignore
family: 4,
// Next.js cache
next: options.revalidate !== undefined
? { revalidate: options.revalidate }
: undefined,
});
} else if (fetchError?.cause?.code === "UND_ERR_CONNECT_TIMEOUT") {
// Retry automatique sur timeout de connexion (cold start)
@@ -152,6 +175,10 @@ export abstract class BaseApiService {
connectTimeout: timeoutMs,
bodyTimeout: timeoutMs,
headersTimeout: timeoutMs,
// Next.js cache
next: options.revalidate !== undefined
? { revalidate: options.revalidate }
: undefined,
});
} else {
throw fetchError;
@@ -160,8 +187,9 @@ export abstract class BaseApiService {
clearTimeout(timeoutId);
const duration = Date.now() - startTime;
if (isDebug) {
const duration = Date.now() - startTime;
logger.info(
{
url,
@@ -173,6 +201,16 @@ export abstract class BaseApiService {
);
}
// Log potential cache hit/miss based on response time
if (isCacheDebug && options.revalidate) {
// Fast response (< 50ms) is likely a cache hit
if (duration < 50) {
logger.info({ url, duration: `${duration}ms` }, "⚡ Cache HIT (fast response)");
} else {
logger.info({ url, duration: `${duration}ms` }, "🔄 Cache MISS (slow response)");
}
}
if (!response.ok) {
if (isDebug) {
logger.error(

View File

@@ -6,12 +6,22 @@ import { ERROR_CODES } from "../../constants/errorCodes";
import { AppError } from "../../utils/errors";
export class BookService extends BaseApiService {
private static readonly CACHE_TTL = 60; // 1 minute
static async getBook(bookId: string): Promise<KomgaBookWithPages> {
try {
// Récupération parallèle des détails du tome et des pages
const [book, pages] = await Promise.all([
this.fetchFromApi<KomgaBook>({ path: `books/${bookId}` }),
this.fetchFromApi<{ number: number }[]>({ path: `books/${bookId}/pages` }),
this.fetchFromApi<KomgaBook>(
{ path: `books/${bookId}` },
{},
{ revalidate: this.CACHE_TTL }
),
this.fetchFromApi<{ number: number }[]>(
{ path: `books/${bookId}/pages` },
{},
{ revalidate: this.CACHE_TTL }
),
]);
return {
@@ -44,7 +54,11 @@ export class BookService extends BaseApiService {
static async getBookSeriesId(bookId: string): Promise<string> {
try {
const book = await this.fetchFromApi<KomgaBook>({ path: `books/${bookId}` });
const book = await this.fetchFromApi<KomgaBook>(
{ path: `books/${bookId}` },
{},
{ revalidate: this.CACHE_TTL }
);
return book.seriesId;
} catch (error) {
throw new AppError(ERROR_CODES.BOOK.NOT_FOUND, {}, error);

View File

@@ -8,53 +8,75 @@ import { AppError } from "../../utils/errors";
export type { HomeData };
export class HomeService extends BaseApiService {
private static readonly CACHE_TTL = 120; // 2 minutes
static async getHomeData(): Promise<HomeData> {
try {
const [ongoing, ongoingBooks, recentlyRead, onDeck, latestSeries] = await Promise.all([
this.fetchFromApi<LibraryResponse<KomgaSeries>>({
path: "series",
params: {
read_status: "IN_PROGRESS",
sort: "readDate,desc",
page: "0",
size: "10",
media_status: "READY",
this.fetchFromApi<LibraryResponse<KomgaSeries>>(
{
path: "series",
params: {
read_status: "IN_PROGRESS",
sort: "readDate,desc",
page: "0",
size: "10",
media_status: "READY",
},
},
}),
this.fetchFromApi<LibraryResponse<KomgaBook>>({
path: "books",
params: {
read_status: "IN_PROGRESS",
sort: "readProgress.readDate,desc",
page: "0",
size: "10",
media_status: "READY",
{},
{ revalidate: this.CACHE_TTL }
),
this.fetchFromApi<LibraryResponse<KomgaBook>>(
{
path: "books",
params: {
read_status: "IN_PROGRESS",
sort: "readProgress.readDate,desc",
page: "0",
size: "10",
media_status: "READY",
},
},
}),
this.fetchFromApi<LibraryResponse<KomgaBook>>({
path: "books/latest",
params: {
page: "0",
size: "10",
media_status: "READY",
{},
{ revalidate: this.CACHE_TTL }
),
this.fetchFromApi<LibraryResponse<KomgaBook>>(
{
path: "books/latest",
params: {
page: "0",
size: "10",
media_status: "READY",
},
},
}),
this.fetchFromApi<LibraryResponse<KomgaBook>>({
path: "books/ondeck",
params: {
page: "0",
size: "10",
media_status: "READY",
{},
{ revalidate: this.CACHE_TTL }
),
this.fetchFromApi<LibraryResponse<KomgaBook>>(
{
path: "books/ondeck",
params: {
page: "0",
size: "10",
media_status: "READY",
},
},
}),
this.fetchFromApi<LibraryResponse<KomgaSeries>>({
path: "series/latest",
params: {
page: "0",
size: "10",
media_status: "READY",
{},
{ revalidate: this.CACHE_TTL }
),
this.fetchFromApi<LibraryResponse<KomgaSeries>>(
{
path: "series/latest",
params: {
page: "0",
size: "10",
media_status: "READY",
},
},
}),
{},
{ revalidate: this.CACHE_TTL }
),
]);
return {

View File

@@ -14,18 +14,28 @@ interface KomgaLibraryRaw {
}
export class LibraryService extends BaseApiService {
private static readonly CACHE_TTL = 300; // 5 minutes
static async getLibraries(): Promise<KomgaLibrary[]> {
try {
const libraries = await this.fetchFromApi<KomgaLibraryRaw[]>({ path: "libraries" });
const libraries = await this.fetchFromApi<KomgaLibraryRaw[]>(
{ path: "libraries" },
{},
{ revalidate: this.CACHE_TTL }
);
// Enrich each library with book counts
// Enrich each library with book counts (parallel requests)
const enrichedLibraries = await Promise.all(
libraries.map(async (library) => {
try {
const booksResponse = await this.fetchFromApi<{ totalElements: number }>({
path: "books",
params: { library_id: library.id, size: "0" },
});
const booksResponse = await this.fetchFromApi<{ totalElements: number }>(
{
path: "books",
params: { library_id: library.id, size: "0" },
},
{},
{ revalidate: this.CACHE_TTL }
);
return {
...library,
importLastModified: "",
@@ -76,40 +86,19 @@ export class LibraryService extends BaseApiService {
let condition: any;
if (unreadOnly) {
// Utiliser allOf pour combiner libraryId avec anyOf pour UNREAD ou IN_PROGRESS
condition = {
allOf: [
{
libraryId: {
operator: "is",
value: libraryId,
},
},
{ libraryId: { operator: "is", value: libraryId } },
{
anyOf: [
{
readStatus: {
operator: "is",
value: "UNREAD",
},
},
{
readStatus: {
operator: "is",
value: "IN_PROGRESS",
},
},
{ readStatus: { operator: "is", value: "UNREAD" } },
{ readStatus: { operator: "is", value: "IN_PROGRESS" } },
],
},
],
};
} else {
condition = {
libraryId: {
operator: "is",
value: libraryId,
},
};
condition = { libraryId: { operator: "is", value: libraryId } };
}
const searchBody: { condition: any; fullTextSearch?: string } = { condition };
@@ -127,13 +116,10 @@ export class LibraryService extends BaseApiService {
const response = await this.fetchFromApi<LibraryResponse<Series>>(
{ path: "series/list", params },
headers,
{
method: "POST",
body: JSON.stringify(searchBody),
}
{ method: "POST", body: JSON.stringify(searchBody), revalidate: this.CACHE_TTL }
);
// Filtrer uniquement les séries supprimées côté client (léger)
// Filtrer uniquement les séries supprimées
const filteredContent = response.content.filter((series) => !series.deleted);
return {
@@ -149,12 +135,9 @@ export class LibraryService extends BaseApiService {
static async scanLibrary(libraryId: string, deep: boolean = false): Promise<void> {
try {
await this.fetchFromApi(
{
path: `libraries/${libraryId}/scan`,
params: { deep: String(deep) },
},
{ path: `libraries/${libraryId}/scan`, params: { deep: String(deep) } },
{},
{ method: "POST", noJson: true }
{ method: "POST", noJson: true, revalidate: 0 } // bypass cache on mutations
);
} catch (error) {
throw new AppError(ERROR_CODES.LIBRARY.SCAN_ERROR, { libraryId }, error);

View File

@@ -10,9 +10,15 @@ import type { UserPreferences } from "@/types/preferences";
import logger from "@/lib/logger";
export class SeriesService extends BaseApiService {
private static readonly CACHE_TTL = 120; // 2 minutes
static async getSeries(seriesId: string): Promise<KomgaSeries> {
try {
return this.fetchFromApi<KomgaSeries>({ path: `series/${seriesId}` });
return this.fetchFromApi<KomgaSeries>(
{ path: `series/${seriesId}` },
{},
{ revalidate: this.CACHE_TTL }
);
} catch (error) {
throw new AppError(ERROR_CODES.SERIES.FETCH_ERROR, {}, error);
}
@@ -81,6 +87,7 @@ export class SeriesService extends BaseApiService {
{
method: "POST",
body: JSON.stringify(searchBody),
revalidate: this.CACHE_TTL,
}
);