diff --git a/src/app/api/notes/route.ts b/src/app/api/notes/route.ts index 8627563..182e6a0 100644 --- a/src/app/api/notes/route.ts +++ b/src/app/api/notes/route.ts @@ -27,7 +27,7 @@ export async function GET(req: NextRequest) { const shortcut = sp.get('shortcut') === 'true'; const sortBy = sp.get('sortBy') || 'updatedAt'; const sortDir = (sp.get('sortDir') === 'asc' ? 'asc' : 'desc') as 'asc' | 'desc'; - const limit = Math.min(parseInt(sp.get('limit') || '500', 10) || 500, 2000); + const limit = Math.min(parseInt(sp.get('limit') || '50', 10) || 50, 150); const offset = parseInt(sp.get('offset') || '0', 10) || 0; const where = buildNoteWhereClause({ diff --git a/src/components/EvercloneWorkspace.tsx b/src/components/EvercloneWorkspace.tsx index 112c84e..fd10c15 100644 --- a/src/components/EvercloneWorkspace.tsx +++ b/src/components/EvercloneWorkspace.tsx @@ -65,6 +65,9 @@ import NoteHistoryModal from './NoteHistoryModal'; import KeyboardShortcutsModal from './KeyboardShortcutsModal'; import { PdfAutoThumbnail, InlinePdfViewerCard } from './PdfNoteComponents'; +// Maximum notes loaded per page/batch to ensure low memory usage and fast render times +const NOTES_PAGE_SIZE = 40; + export default function EvercloneWorkspace() { const [sidebar, setSidebar] = useState({ stacks: [], @@ -344,7 +347,7 @@ export default function EvercloneWorkspace() { }, []); const buildNoteQueryParams = useCallback( - (offset = 0, limit = 150) => { + (offset = 0, limit = NOTES_PAGE_SIZE) => { const sp = new URLSearchParams(); if (debouncedSearch.trim()) sp.set('q', debouncedSearch.trim()); if (activeView === 'notebook' && selectedNotebookId) { @@ -404,7 +407,7 @@ export default function EvercloneWorkspace() { const currentReqId = ++listRequestIdRef.current; setLoadingList(true); try { - const sp = buildNoteQueryParams(0, 150); + const sp = buildNoteQueryParams(0, NOTES_PAGE_SIZE); const res = await fetch(`/api/notes?${sp.toString()}`); if (!res.ok) return; const data = await res.json(); @@ -442,7 +445,7 @@ export default function EvercloneWorkspace() { const currentReqId = listRequestIdRef.current; setLoadingMore(true); try { - const sp = buildNoteQueryParams(notes.length, 150); + const sp = buildNoteQueryParams(notes.length, NOTES_PAGE_SIZE); const res = await fetch(`/api/notes?${sp.toString()}`); if (!res.ok) return; const data = await res.json(); @@ -2820,6 +2823,8 @@ export default function EvercloneWorkspace() { {firstPdf && ( @@ -3082,6 +3087,8 @@ export default function EvercloneWorkspace() { {firstPdf && ( diff --git a/src/components/PdfNoteComponents.tsx b/src/components/PdfNoteComponents.tsx index 6b986cc..56fa60c 100644 --- a/src/components/PdfNoteComponents.tsx +++ b/src/components/PdfNoteComponents.tsx @@ -69,6 +69,8 @@ export function PdfAutoThumbnail({ PDF Page 1 @@ -103,6 +105,8 @@ export function PdfAutoThumbnail({ PDF Page 1 diff --git a/src/lib/pdfClient.ts b/src/lib/pdfClient.ts index ab2d104..401a86f 100644 --- a/src/lib/pdfClient.ts +++ b/src/lib/pdfClient.ts @@ -52,24 +52,55 @@ export function loadPdfJs(): Promise { } const pageRenderCache = new Map(); +const MAX_CACHE_SIZE = 40; + +// Concurrency queue to throttle client-side PDF rendering (max 1 active worker render at a time) +let activeRenders = 0; +const renderQueue: Array<() => void> = []; + +async function acquireRenderSlot(): Promise<() => void> { + if (activeRenders < 1) { + activeRenders++; + return () => releaseRenderSlot(); + } + return new Promise((resolve) => { + renderQueue.push(() => { + activeRenders++; + resolve(() => releaseRenderSlot()); + }); + }); +} + +function releaseRenderSlot(): void { + activeRenders--; + if (renderQueue.length > 0) { + const next = renderQueue.shift(); + next?.(); + } +} /** * Renders a single page of a PDF (default Page 1) into a crisp JPEG data URL. + * Throttled to 1 concurrent render with automatic PDF.js memory destruction. */ export async function renderPdfPageToDataUrl( pdfUrl: string, pageNumber = 1, - targetSize = 480 + targetSize = 360 ): Promise { const cacheKey = `${pdfUrl}#page=${pageNumber}@${targetSize}`; if (pageRenderCache.has(cacheKey)) { return pageRenderCache.get(cacheKey)!; } + const release = await acquireRenderSlot(); + let loadingTask: any = null; + let pdf: any = null; + try { const pdfjsLib = await loadPdfJs(); - const loadingTask = pdfjsLib.getDocument(pdfUrl); - const pdf = await loadingTask.promise; + loadingTask = pdfjsLib.getDocument(pdfUrl); + pdf = await loadingTask.promise; if (pageNumber < 1 || pageNumber > pdf.numPages) { return null; @@ -94,12 +125,30 @@ export async function renderPdfPageToDataUrl( await page.render({ canvasContext: ctx, viewport }).promise; - const dataUrl = canvas.toDataURL('image/jpeg', 0.86); + const dataUrl = canvas.toDataURL('image/jpeg', 0.78); + + // Evict oldest if cache exceeds limit + if (pageRenderCache.size >= MAX_CACHE_SIZE) { + const oldestKey = pageRenderCache.keys().next().value; + if (oldestKey) pageRenderCache.delete(oldestKey); + } pageRenderCache.set(cacheKey, dataUrl); + + // Explicitly release canvas memory + canvas.width = 0; + canvas.height = 0; + return dataUrl; } catch (err) { console.warn('PDF thumbnail render error:', err); return null; + } finally { + try { + if (pdf?.cleanup) pdf.cleanup(); + if (pdf?.destroy) await pdf.destroy(); + if (loadingTask?.destroy) await loadingTask.destroy(); + } catch {} + release(); } }