Optimize note list memory: reduce batch to 40, add lazy loading and throttle PDF rendering
This commit is contained in:
1 parent
3bd9eacbe3
commit
ad640397e6
4 files changed
+68
-8
No files matched your search
@@ -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({
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user