Optimize note list memory: reduce batch to 40, add lazy loading and throttle PDF rendering

This commit is contained in:
chrisf committed 2026-10-24 22:25:04 -06:00
1 parent 3bd9eacbe3
commit ad640397e6
4 files changed
+68 -8

No files matched your search

+1 -1
View File
@@ -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({
+10 -3
View File
@@ -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<SidebarData>({
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() {
<img
src={effectiveThumbUrl}
alt=""
loading="lazy"
decoding="async"
className="w-full h-full object-cover group-hover/card:scale-105 transition duration-300"
/>
{firstPdf && (
@@ -3082,6 +3087,8 @@ export default function EvercloneWorkspace() {
<img
src={effectiveThumbUrl}
alt=""
loading="lazy"
decoding="async"
className="w-14 h-14 object-cover rounded-lg border border-gray-200 bg-white"
/>
{firstPdf && (
+4
View File
@@ -69,6 +69,8 @@ export function PdfAutoThumbnail({
<img
src={dataUrl}
alt="PDF Page 1"
loading="lazy"
decoding="async"
className="w-full h-full object-cover object-top group-hover/card:scale-105 transition duration-300"
/>
<span className="absolute bottom-1.5 left-2 px-1.5 py-0.5 rounded bg-red-600/90 text-white text-[9px] font-bold uppercase tracking-wider shadow">
@@ -103,6 +105,8 @@ export function PdfAutoThumbnail({
<img
src={dataUrl}
alt="PDF Page 1"
loading="lazy"
decoding="async"
className="w-full h-full object-cover object-top bg-white"
/>
<span className="absolute bottom-0.5 right-0.5 px-1 py-0.1 rounded bg-red-600/90 text-white text-[8px] font-bold uppercase">
+53 -4
View File
@@ -52,24 +52,55 @@ export function loadPdfJs(): Promise<any> {
}
const pageRenderCache = new Map<string, string>();
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<string | null> {
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();
}
}