perf: optimize drawings endpoint with caching and lazy loading

- Add 5s in-memory cache for /drawings responses with automatic cleanup
- Split Drawing/DrawingSummary types for efficient data fetching
- Implement lazy loading of drawing data in DrawingCard component
- Add configurable DRAWINGS_CACHE_TTL_MS and RATE_LIMIT_MAX_REQUESTS env vars
- Prevent memory leaks with periodic cleanup of cache and rate limit maps
- Add loading states and better UX for export operations
- Improve JSON parsing with error handling for malformed stored data

Benchmark results (100 drawings, cached):
- Avg latency: 6.94ms (p50: 4ms, p97.5: 8ms)
- Avg throughput: 668 req/s (peak: 1,023)
- 3k requests in 5s with 0 errors

Update .gitignore to exclude generated files, env files, and build artifacts
This commit is contained in:
Adrian Acala
2025-11-29 04:28:03 +00:00
parent 971046d568
commit 6f050aec7d
6 changed files with 348 additions and 57 deletions
+35 -10
View File
@@ -1,5 +1,5 @@
import axios from "axios";
import type { Drawing, Collection } from "../types";
import type { Drawing, Collection, DrawingSummary } from "../types";
export const API_URL = import.meta.env.VITE_API_URL || "/api";
@@ -14,23 +14,48 @@ const coerceTimestamp = (value: string | number | Date): number => {
return Number.isNaN(parsed) ? Date.now() : parsed;
};
const deserializeDrawing = (drawing: any): Drawing => ({
...drawing,
createdAt: coerceTimestamp(drawing.createdAt),
updatedAt: coerceTimestamp(drawing.updatedAt),
const deserializeTimestamps = <T extends { createdAt: any; updatedAt: any }>(
data: T
): T & { createdAt: number; updatedAt: number } => ({
...data,
createdAt: coerceTimestamp(data.createdAt),
updatedAt: coerceTimestamp(data.updatedAt),
});
export const getDrawings = async (
const deserializeDrawingSummary = (drawing: any): DrawingSummary =>
deserializeTimestamps(drawing);
const deserializeDrawing = (drawing: any): Drawing =>
deserializeTimestamps(drawing);
export function getDrawings(
search?: string,
collectionId?: string | null
) => {
): Promise<DrawingSummary[]>;
export function getDrawings(
search: string | undefined,
collectionId: string | null | undefined,
options: { includeData: true }
): Promise<Drawing[]>;
export async function getDrawings(
search?: string,
collectionId?: string | null,
options?: { includeData?: boolean }
) {
const params: any = {};
if (search) params.search = search;
if (collectionId !== undefined)
params.collectionId = collectionId === null ? "null" : collectionId;
const response = await api.get<Drawing[]>("/drawings", { params });
return response.data.map(deserializeDrawing);
};
if (options?.includeData) {
params.includeData = "true";
const response = await api.get<Drawing[]>("/drawings", { params });
return response.data.map(deserializeDrawing);
}
const response = await api.get<DrawingSummary[]>("/drawings", { params });
return response.data.map(deserializeDrawingSummary);
}
export const getDrawing = async (id: string) => {
const response = await api.get<Drawing>(`/drawings/${id}`);