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 shortcut = sp.get('shortcut') === 'true';
|
||||||
const sortBy = sp.get('sortBy') || 'updatedAt';
|
const sortBy = sp.get('sortBy') || 'updatedAt';
|
||||||
const sortDir = (sp.get('sortDir') === 'asc' ? 'asc' : 'desc') as 'asc' | 'desc';
|
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 offset = parseInt(sp.get('offset') || '0', 10) || 0;
|
||||||
|
|
||||||
const where = buildNoteWhereClause({
|
const where = buildNoteWhereClause({
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ import NoteHistoryModal from './NoteHistoryModal';
|
|||||||
import KeyboardShortcutsModal from './KeyboardShortcutsModal';
|
import KeyboardShortcutsModal from './KeyboardShortcutsModal';
|
||||||
import { PdfAutoThumbnail, InlinePdfViewerCard } from './PdfNoteComponents';
|
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() {
|
export default function EvercloneWorkspace() {
|
||||||
const [sidebar, setSidebar] = useState<SidebarData>({
|
const [sidebar, setSidebar] = useState<SidebarData>({
|
||||||
stacks: [],
|
stacks: [],
|
||||||
@@ -344,7 +347,7 @@ export default function EvercloneWorkspace() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const buildNoteQueryParams = useCallback(
|
const buildNoteQueryParams = useCallback(
|
||||||
(offset = 0, limit = 150) => {
|
(offset = 0, limit = NOTES_PAGE_SIZE) => {
|
||||||
const sp = new URLSearchParams();
|
const sp = new URLSearchParams();
|
||||||
if (debouncedSearch.trim()) sp.set('q', debouncedSearch.trim());
|
if (debouncedSearch.trim()) sp.set('q', debouncedSearch.trim());
|
||||||
if (activeView === 'notebook' && selectedNotebookId) {
|
if (activeView === 'notebook' && selectedNotebookId) {
|
||||||
@@ -404,7 +407,7 @@ export default function EvercloneWorkspace() {
|
|||||||
const currentReqId = ++listRequestIdRef.current;
|
const currentReqId = ++listRequestIdRef.current;
|
||||||
setLoadingList(true);
|
setLoadingList(true);
|
||||||
try {
|
try {
|
||||||
const sp = buildNoteQueryParams(0, 150);
|
const sp = buildNoteQueryParams(0, NOTES_PAGE_SIZE);
|
||||||
const res = await fetch(`/api/notes?${sp.toString()}`);
|
const res = await fetch(`/api/notes?${sp.toString()}`);
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -442,7 +445,7 @@ export default function EvercloneWorkspace() {
|
|||||||
const currentReqId = listRequestIdRef.current;
|
const currentReqId = listRequestIdRef.current;
|
||||||
setLoadingMore(true);
|
setLoadingMore(true);
|
||||||
try {
|
try {
|
||||||
const sp = buildNoteQueryParams(notes.length, 150);
|
const sp = buildNoteQueryParams(notes.length, NOTES_PAGE_SIZE);
|
||||||
const res = await fetch(`/api/notes?${sp.toString()}`);
|
const res = await fetch(`/api/notes?${sp.toString()}`);
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -2820,6 +2823,8 @@ export default function EvercloneWorkspace() {
|
|||||||
<img
|
<img
|
||||||
src={effectiveThumbUrl}
|
src={effectiveThumbUrl}
|
||||||
alt=""
|
alt=""
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
className="w-full h-full object-cover group-hover/card:scale-105 transition duration-300"
|
className="w-full h-full object-cover group-hover/card:scale-105 transition duration-300"
|
||||||
/>
|
/>
|
||||||
{firstPdf && (
|
{firstPdf && (
|
||||||
@@ -3082,6 +3087,8 @@ export default function EvercloneWorkspace() {
|
|||||||
<img
|
<img
|
||||||
src={effectiveThumbUrl}
|
src={effectiveThumbUrl}
|
||||||
alt=""
|
alt=""
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
className="w-14 h-14 object-cover rounded-lg border border-gray-200 bg-white"
|
className="w-14 h-14 object-cover rounded-lg border border-gray-200 bg-white"
|
||||||
/>
|
/>
|
||||||
{firstPdf && (
|
{firstPdf && (
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ export function PdfAutoThumbnail({
|
|||||||
<img
|
<img
|
||||||
src={dataUrl}
|
src={dataUrl}
|
||||||
alt="PDF Page 1"
|
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"
|
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">
|
<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
|
<img
|
||||||
src={dataUrl}
|
src={dataUrl}
|
||||||
alt="PDF Page 1"
|
alt="PDF Page 1"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
className="w-full h-full object-cover object-top bg-white"
|
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">
|
<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 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.
|
* 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(
|
export async function renderPdfPageToDataUrl(
|
||||||
pdfUrl: string,
|
pdfUrl: string,
|
||||||
pageNumber = 1,
|
pageNumber = 1,
|
||||||
targetSize = 480
|
targetSize = 360
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
const cacheKey = `${pdfUrl}#page=${pageNumber}@${targetSize}`;
|
const cacheKey = `${pdfUrl}#page=${pageNumber}@${targetSize}`;
|
||||||
if (pageRenderCache.has(cacheKey)) {
|
if (pageRenderCache.has(cacheKey)) {
|
||||||
return pageRenderCache.get(cacheKey)!;
|
return pageRenderCache.get(cacheKey)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const release = await acquireRenderSlot();
|
||||||
|
let loadingTask: any = null;
|
||||||
|
let pdf: any = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pdfjsLib = await loadPdfJs();
|
const pdfjsLib = await loadPdfJs();
|
||||||
const loadingTask = pdfjsLib.getDocument(pdfUrl);
|
loadingTask = pdfjsLib.getDocument(pdfUrl);
|
||||||
const pdf = await loadingTask.promise;
|
pdf = await loadingTask.promise;
|
||||||
|
|
||||||
if (pageNumber < 1 || pageNumber > pdf.numPages) {
|
if (pageNumber < 1 || pageNumber > pdf.numPages) {
|
||||||
return null;
|
return null;
|
||||||
@@ -94,12 +125,30 @@ export async function renderPdfPageToDataUrl(
|
|||||||
|
|
||||||
await page.render({ canvasContext: ctx, viewport }).promise;
|
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);
|
pageRenderCache.set(cacheKey, dataUrl);
|
||||||
|
|
||||||
|
// Explicitly release canvas memory
|
||||||
|
canvas.width = 0;
|
||||||
|
canvas.height = 0;
|
||||||
|
|
||||||
return dataUrl;
|
return dataUrl;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn('PDF thumbnail render error:', err);
|
console.warn('PDF thumbnail render error:', err);
|
||||||
return null;
|
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