Performance Optimization
Learn how to optimize graneet-form for maximum performance in large and complex applications
Performance Optimization
Learn how to optimize graneet-form for maximum performance in large and complex applications.
Understanding Graneet-Form's Performance Model
Subscription-Based Architecture
Graneet-form uses a subscription system where components only re-render when their watched fields change:
// ✅ Only re-renders when 'name' or 'email' changes
const { name, email } = useFieldsWatch(form, ['name', 'email']);
// ❌ Re-renders when ANY field changes
const allValues = useFieldsWatch(form, undefined);Field Registration Overhead
Each field has a small registration cost, but the benefits outweigh the overhead:
- Registration: One-time cost when field mounts
- Value Updates: Optimized O(1) lookups for subscribers
- Validation: Runs only on changed fields
- Cleanup: Automatic when field unmounts
Optimization Strategies
1. Selective Field Watching
Watch Only Required Fields
// ❌ Inefficient - watches all fields
function FormSummary() {
const form = useFormContext<LargeFormData>();
const allValues = useFieldsWatch(form, undefined);
return <div>Name: {allValues.name}</div>;
}
// ✅ Efficient - watches only needed fields
function FormSummary() {
const form = useFormContext<LargeFormData>();
const { name } = useFieldsWatch(form, ['name']);
return <div>Name: {name}</div>;
}Use Appropriate Watch Modes
// For real-time UI updates (search, live preview)
const { searchQuery } = useFieldsWatch(form, ['searchQuery']);
// For less critical updates (summaries, auto-save)
const { title, description } = useFieldsWatch(form, ['title', 'description'], { mode: 'onBlur' });2. Lazy Field Registration
Conditional Field Rendering
interface ConditionalFormData {
showAdvanced: boolean;
// ... many other fields
}
function ConditionalForm() {
const form = useFormContext<ConditionalFormData>();
const { showAdvanced } = useFieldsWatch(form, ['showAdvanced']);
return (
<Form form={form}>
{/* Always render basic fields */}
<BasicFields />
{/* Only register expensive fields when needed */}
{showAdvanced && <AdvancedFields />}
</Form>
);
}
function AdvancedFields() {
return (
<>
{/* These 50+ fields only register when showAdvanced is true */}
<Field name="advancedField1" render={/* ... */} />
<Field name="advancedField2" render={/* ... */} />
{/* ... more fields */}
</>
);
}Virtualized Forms
For forms with hundreds of fields:
function VirtualizedForm() {
const [visibleRange, setVisibleRange] = useState({ start: 0, end: 20 });
const form = useFormContext<MassiveFormData>();
return (
<div className="virtualized-container">
{/* Only render fields in visible range */}
{Array.from({ length: visibleRange.end - visibleRange.start })
.map((_, index) => {
const fieldIndex = visibleRange.start + index;
return (
<Field
key={fieldIndex}
name={`field_${fieldIndex}`}
render={/* ... */}
/>
);
})}
</div>
);
}3. Optimized Validation
Validation Order Optimization
Place cheap validations first, expensive ones last:
<Field name="email" render={/* ... */}>
{/* Fast validations first */}
<Rule validationFn={isRequired} message="Email is required" />
<Rule validationFn={isValidEmailFormat} message="Invalid email format" />
<Rule validationFn={isReasonableLength} message="Email too long" />
{/* Expensive async validation last */}
<Rule
validationFn={checkEmailAvailability}
message="Email already exists"
isDebounced={true}
/>
</Field>Debounced Validation
// Custom debounced validation hook
function useDebouncedValidation(validationFn: Function, delay: number = 500) {
const timeoutRef = useRef<NodeJS.Timeout>();
return useCallback((value: any) => {
return new Promise((resolve) => {
clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => {
resolve(validationFn(value));
}, delay);
});
}, [validationFn, delay]);
}
// Usage
function AsyncValidatedField() {
const debouncedValidation = useDebouncedValidation(expensiveApiCall, 1000);
return (
<Field name="username" render={/* ... */}>
<Rule
validationFn={debouncedValidation}
message="Username validation failed"
isDebounced={true}
/>
</Field>
);
}4. Component Memoization
Memoize Field Components
// Memoize stable field components
const MemoizedTextField = React.memo(<T extends FieldValues, K extends keyof T>({
name,
label,
placeholder
}: {
name: K;
label: string;
placeholder: string;
}) => (
<Field<T, K>
name={name}
render={(fieldProps, fieldState) => (
<div>
<label>{label}</label>
<input
placeholder={placeholder}
value={fieldProps.value || ''}
onChange={(e) => fieldProps.onChange(e.target.value)}
onBlur={fieldProps.onBlur}
onFocus={fieldProps.onFocus}
/>
{!fieldState.isPristine && fieldState.validationStatus.status === 'invalid' && (
<span>{fieldState.validationStatus.message}</span>
)}
</div>
)}
/>
));
// Usage
<MemoizedTextField
name="email"
label="Email Address"
placeholder="Enter email"
/>Memoize Form Sections
const MemoizedFormSection = React.memo(({
title,
children
}: {
title: string;
children: React.ReactNode;
}) => (
<div className="form-section">
<h3>{title}</h3>
{children}
</div>
));
function OptimizedForm() {
return (
<Form form={form}>
<MemoizedFormSection title="Basic Information">
<BasicFields />
</MemoizedFormSection>
<MemoizedFormSection title="Contact Details">
<ContactFields />
</MemoizedFormSection>
</Form>
);
}5. Efficient State Updates
Batch Form Updates
// ❌ Multiple individual updates
function updateUserProfile(userData: UserData) {
form.setFormValues({ name: userData.name });
form.setFormValues({ email: userData.email });
form.setFormValues({ phone: userData.phone });
// Each call triggers re-renders
}
// ✅ Single batched update
function updateUserProfile(userData: UserData) {
form.setFormValues({
name: userData.name,
email: userData.email,
phone: userData.phone
});
// Single re-render for all subscribers
}Minimize Form Method Calls in Renders
// ❌ Calling getFormValues in render
function FormDisplay() {
const form = useFormContext();
const values = form.getFormValues(); // Called on every render
return <div>{JSON.stringify(values)}</div>;
}
// ✅ Use optimized watching hooks
function FormDisplay() {
const form = useFormContext();
const values = useFieldsWatch(form, undefined); // Optimized subscription
return <div>{JSON.stringify(values)}</div>;
}Custom Performance Hooks
Optimized Multi-field Watcher
function useOptimizedFieldWatcher<T extends FieldValues>(
form: FormContextApi<T>,
criticalFields: (keyof T)[],
normalFields: (keyof T)[]
) {
// Critical fields update immediately (onChange)
const criticalValues = useFieldsWatch(form, criticalFields);
// Normal fields update on blur only
const normalValues = useFieldsWatch(form, normalFields, { mode: 'onBlur' });
return useMemo(() => ({
...criticalValues,
...normalValues
}), [criticalValues, normalValues]);
}
// Usage
function SmartFormWatcher() {
const form = useFormContext<FormData>();
const values = useOptimizedFieldWatcher(
form,
['searchQuery', 'liveFilter'], // Need immediate updates
['description', 'notes', 'tags'] // Blur updates are sufficient
);
return <FormPreview values={values} />;
}Debounced Form Watcher
function useDebouncedFormValues<T extends FieldValues>(
form: FormContextApi<T>,
fields: (keyof T)[],
delay: number = 300
) {
const values = useFieldsWatch(form, fields);
const [debouncedValues, setDebouncedValues] = useState(values);
useEffect(() => {
const timeout = setTimeout(() => {
setDebouncedValues(values);
}, delay);
return () => clearTimeout(timeout);
}, [values, delay]);
return debouncedValues;
}
// Usage for expensive operations
function ExpensiveFormProcessor() {
const form = useFormContext<FormData>();
// Only process after 500ms of no changes
const debouncedValues = useDebouncedFormValues(form, ['data'], 500);
const processedData = useMemo(() => {
return performExpensiveCalculation(debouncedValues.data);
}, [debouncedValues.data]);
return <div>{processedData}</div>;
}Memory Management
Cleanup Strategies
function FormWithCleanup() {
const form = useForm<FormData>();
useEffect(() => {
// Cleanup function
return () => {
// Form automatically cleans up field subscriptions
// But you might want to clean up additional resources
clearTimeout(autoSaveTimeout.current);
cancelPendingRequests();
};
}, []);
return <Form form={form}>{/* ... */}</Form>;
}Prevent Memory Leaks
function SafeAsyncField() {
const [loading, setLoading] = useState(false);
const mountedRef = useRef(true);
useEffect(() => {
return () => {
mountedRef.current = false;
};
}, []);
const handleAsyncValidation = useCallback(async (value: string) => {
setLoading(true);
try {
const result = await validateAsync(value);
// Only update state if component is still mounted
if (mountedRef.current) {
setLoading(false);
}
return result;
} catch (error) {
if (mountedRef.current) {
setLoading(false);
}
return false;
}
}, []);
return (
<Field name="asyncField" render={/* ... */}>
<Rule
validationFn={handleAsyncValidation}
message="Validation failed"
isDebounced={true}
/>
</Field>
);
}Performance Monitoring
Custom Performance Hook
function useFormPerformance<T extends FieldValues>(form: FormContextApi<T>) {
const [metrics, setMetrics] = useState({
renderCount: 0,
lastRenderTime: Date.now(),
averageRenderTime: 0
});
useEffect(() => {
const startTime = Date.now();
setMetrics(prev => {
const renderTime = startTime - prev.lastRenderTime;
const newRenderCount = prev.renderCount + 1;
const newAverageRenderTime =
(prev.averageRenderTime * (newRenderCount - 1) + renderTime) / newRenderCount;
return {
renderCount: newRenderCount,
lastRenderTime: startTime,
averageRenderTime: newAverageRenderTime
};
});
});
return metrics;
}
// Usage (development only)
function PerformanceMonitor() {
const form = useFormContext();
const metrics = useFormPerformance(form);
if (process.env.NODE_ENV !== 'development') {
return null;
}
return (
<div className="performance-monitor">
<div>Renders: {metrics.renderCount}</div>
<div>Avg Render Time: {metrics.averageRenderTime.toFixed(2)}ms</div>
</div>
);
}Bundle Size Optimization
Tree Shaking
Import only what you need:
// ❌ Imports entire library
import * as GraneetForm from 'graneet-form';
// ✅ Import only needed components
import { useForm, Form, Field, Rule } from 'graneet-form';Code Splitting
// Lazy load complex form sections
const AdvancedFormSection = React.lazy(() => import('./AdvancedFormSection'));
const PaymentSection = React.lazy(() => import('./PaymentSection'));
function OptimizedForm() {
const [activeSection, setActiveSection] = useState('basic');
return (
<Form form={form}>
<BasicSection />
<Suspense fallback={<div>Loading...</div>}>
{activeSection === 'advanced' && <AdvancedFormSection />}
{activeSection === 'payment' && <PaymentSection />}
</Suspense>
</Form>
);
}Performance Best Practices Summary
✅ Do's
- Watch specific fields instead of all fields
- Use appropriate watch modes (onChange vs onBlur)
- Lazy register fields in conditional sections
- Debounce expensive validations
- Memoize stable components
- Batch form updates
- Clean up resources properly
❌ Don'ts
- Don't watch all fields unless absolutely necessary
- Don't call form methods in render functions
- Don't create new objects in render functions
- Don't ignore cleanup in async operations
- Don't validate on every keystroke for expensive operations
- Don't render all fields if many are conditional
Performance Checklist
- Fields watch only required data
- Expensive validations are debounced
- Conditional sections use lazy registration
- Components are memoized where appropriate
- Form updates are batched
- Memory leaks are prevented
- Bundle size is optimized
Measuring Performance
Use React DevTools Profiler to measure:
- Render frequency of form components
- Time spent rendering form sections
- Memory usage over time
- JavaScript heap size growth
Monitor these metrics in your applications to identify performance bottlenecks and optimize accordingly.