Performance Best Practices
Optimize rendering, reduce bundle size, and deliver a smooth document viewing experience.
Zustand Selector Pattern
The most important performance optimization is using selectors with useDocumentStore. Without selectors, components re-render on every state change.
Bad vs Goodtsx
// BAD: Re-renders on ANY store change
function BadComponent() {
const store = useDocumentStore();
return <span>{store.currentPage}</span>;
}
// GOOD: Only re-renders when currentPage changes
function GoodComponent() {
const currentPage = useDocumentStore((s) => s.currentPage);
return <span>{currentPage}</span>;
}
// BETTER: Multiple values with shallow equality
import { shallow } from 'zustand/shallow';
function BetterComponent() {
const { currentPage, totalPages } = useDocumentStore(
(s) => ({ currentPage: s.currentPage, totalPages: s.totalPages }),
shallow
);
return <span>{currentPage} / {totalPages}</span>;
}Lazy Loading
Load the SDK only when needed to reduce initial bundle size:
Lazy Loadingtsx
import dynamic from 'next/dynamic';
// Lazy load the entire document viewer
const DocumentViewer = dynamic(
() => import('@/components/DocumentViewer'),
{
loading: () => <DocumentSkeleton />,
ssr: false // PDF rendering requires browser APIs
}
);
function DocumentPage({ documentId }: { documentId: string }) {
const [showViewer, setShowViewer] = useState(false);
return (
<div>
{!showViewer ? (
<button onClick={() => setShowViewer(true)}>
Open Document
</button>
) : (
<DocumentViewer documentId={documentId} />
)}
</div>
);
}Memoization
Memoize expensive computations and callbacks:
Memoizationtsx
import { useMemo, useCallback, memo } from 'react';
// Memoize derived data
function ProgressIndicator() {
const fields = useDocumentStore((s) => s.fields);
const progress = useMemo(() => {
const allFields = Array.from(fields.values());
const required = allFields.filter(f => f.definition.required);
const completed = required.filter(f => f.value !== null);
return required.length > 0
? (completed.length / required.length) * 100
: 100;
}, [fields]);
return <ProgressBar value={progress} />;
}
// Memoize callbacks
function FieldEditor({ fieldId }: { fieldId: string }) {
const setFieldValue = useDocumentStore((s) => s.setFieldValue);
const handleChange = useCallback((value: string) => {
setFieldValue(fieldId, { type: 'text', content: value });
}, [fieldId, setFieldValue]);
return <input onChange={(e) => handleChange(e.target.value)} />;
}
// Memoize components
const FieldItem = memo(function FieldItem({ field }: { field: FieldState }) {
return (
<div>
<span>{field.definition.label}</span>
<span>{field.definition.type}</span>
</div>
);
});Virtual Scrolling
For documents with many pages, implement virtual scrolling to render only visible pages:
Virtual Scrollingtsx
import { useVirtualizer } from '@tanstack/react-virtual';
function VirtualizedPageList() {
const pages = useDocumentStore((s) => s.pages);
const containerRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: pages.length,
getScrollElement: () => containerRef.current,
estimateSize: () => 800, // Estimated page height
overscan: 1, // Render 1 page above/below viewport
});
return (
<div ref={containerRef} className="h-[600px] overflow-auto">
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
<PDFPage pageNumber={virtualItem.index + 1} />
</div>
))}
</div>
</div>
);
}Image Optimization
Optimize signature and uploaded images before submission:
Image Optimizationtsx
async function optimizeSignatureImage(
imageData: string,
maxWidth = 400,
quality = 0.8
): Promise<string> {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
// Calculate new dimensions
let width = img.width;
let height = img.height;
if (width > maxWidth) {
height = (height * maxWidth) / width;
width = maxWidth;
}
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d')!;
ctx.drawImage(img, 0, 0, width, height);
// Convert to optimized PNG
resolve(canvas.toDataURL('image/png', quality));
};
img.src = imageData;
});
}
// Use before setting field value
const handleSignatureSave = async (rawImageData: string) => {
const optimized = await optimizeSignatureImage(rawImageData);
setFieldValue(fieldId, { type: 'signature', imageData: optimized });
};Debouncing Input
Debounce text input to reduce state updates:
Debounced Inputtsx
import { useDebouncedCallback } from 'use-debounce';
function TextFieldInput({ fieldId }: { fieldId: string }) {
const setFieldValue = useDocumentStore((s) => s.setFieldValue);
const field = useDocumentStore((s) => s.fields.get(fieldId));
// Local state for immediate UI feedback
const [localValue, setLocalValue] = useState(
(field?.value as TextFieldValue)?.content || ''
);
// Debounced store update
const debouncedUpdate = useDebouncedCallback((value: string) => {
setFieldValue(fieldId, { type: 'text', content: value });
}, 300);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setLocalValue(value); // Immediate UI update
debouncedUpdate(value); // Debounced store update
};
return <input value={localValue} onChange={handleChange} />;
}Caching
Cache document data to reduce API calls:
Document Cachingtsx
import { useQuery } from '@tanstack/react-query';
function useDocumentData(documentId: string) {
const { apiRequest, isInitialized } = useSDK();
return useQuery({
queryKey: ['document', documentId],
queryFn: () => apiRequest<DocumentWithFields>(
`/sdk/documents/${documentId}`
),
enabled: isInitialized && !!documentId,
staleTime: 5 * 60 * 1000, // Consider fresh for 5 minutes
cacheTime: 30 * 60 * 1000, // Keep in cache for 30 minutes
});
}
// Usage
function DocumentViewer({ documentId }: { documentId: string }) {
const { data, isLoading, error } = useDocumentData(documentId);
if (isLoading) return <Skeleton />;
if (error) return <Error error={error} />;
return <PDFViewer documentUrl={data.documentUrl} />;
}Bundle Analysis
Use
@next/bundle-analyzer to identify large dependencies and optimize your bundle size.Metrics to Monitor
- Time to First Page - How quickly the first PDF page renders
- Field Interaction Latency - Response time when filling fields
- Memory Usage - Especially important with large PDFs
- Re-render Count - Use React DevTools Profiler
- Submission Time - End-to-end submission duration