Framework: React Vue Svelte Solid
Overview
Schema FormX is designed with performance in mind, but performance bottlenecks may still occur in complex scenarios. This chapter introduces common optimization strategies and best practices.
Interactive Example
Dependency Linkage Optimization
Problem: Too Many Linkage Triggers
When there are complex dependencies between form fields, it may trigger excessive linkage computations.
Optimization Strategies :
Control Linkage Depth : Avoid more than 3-4 levels of cascading dependencies
Reasonably Split Schema : Split complex forms into multiple independent Groups
Use Debouncing : Use debouncing for frequently triggered linkages
Optimization Example
import { useMemo , useCallback } from 'react' ;
// ❌ Wrong: Creating new function on each render
const schema = [
{
type : 'string' ,
name : 'field1' ,
deps : [ 'field0' ] ,
onDepsChange : async ({ deps }) => {
// New function each time, may trigger unnecessary re-renders
}
}
];
// ✅ Correct: Use useCallback to stabilize function reference
const handleDepsChange = useCallback ( async ({ deps , schema }) => {
const data = await fetchData (deps[ 0 ]);
return { schema : { ... schema , props : { options : data } } };
} , []);
const schema = [
{
type : 'string' ,
name : 'field1' ,
deps : [ 'field0' ] ,
onDepsChange : handleDepsChange
}
];
Option 1: Using Groups
Split large forms into multiple Groups, each managing independently:
import { Form } from '@schema-formx/react' ;
import type { GroupSchema } from '@schema-formx/react' ;
const schema : GroupSchema [] = [
{
key : 'basic' ,
props : { title : 'Basic Info' } ,
fields : [
{ type : 'string' , name : 'name' , label : 'Name' } ,
{ type : 'string' , name : 'email' , label : 'Email' }
]
} ,
{
key : 'address' ,
props : { title : 'Address' } ,
fields : [
{ type : 'string' , name : 'province' , label : 'Province' } ,
{ type : 'string' , name : 'city' , label : 'City' }
]
}
];
function MyForm () {
return < Form schema = {schema} />;
}
For completely independent form sections, use multiple Form instances:
import { Form } from '@schema-formx/react' ;
import { useRef } from 'react' ;
function MultiFormPage () {
const formRef1 = useRef ( null );
const formRef2 = useRef ( null );
const handleSubmit = async () => {
// Get data from both forms separately
const result1 = await formRef1 . current .validate ();
const result2 = await formRef2 . current .validate ();
if ( ! result1 .errors && ! result2 .errors) {
console .log ( 'Form 1 data:' , result1 .data);
console .log ( 'Form 2 data:' , result2 .data);
}
};
return (
< div >
< h3 >Section 1</ h3 >
< Form
ref = {formRef1}
schema = {schema1}
components = {components}
layout = {layout}
onChange = {({ data }) => console .log ( 'Form 1 changed:' , data)}
/>
< h3 >Section 2</ h3 >
< Form
ref = {formRef2}
schema = {schema2}
components = {components}
layout = {layout}
onChange = {({ data }) => console .log ( 'Form 2 changed:' , data)}
/>
< button onClick = {handleSubmit}>Submit</ button >
</ div >
);
}
Async Data Loading Optimization
Use Caching to Avoid Duplicate Requests
const dataCache = new Map ();
const fetchData = async (key : string ) => {
if ( dataCache .has (key)) {
return dataCache .get (key);
}
const data = await api .getData (key);
dataCache .set (key , data);
return data;
};
const schema = [
{
type : 'string' ,
name : 'industry' ,
label : 'Industry' ,
props : {
options : [ 'IT' , 'Finance' , 'Education' ]
}
} ,
{
type : 'string' ,
name : 'company' ,
label : 'Company' ,
deps : [ 'industry' ] ,
onDepsChange : async ({ deps , schema }) => {
const companies = await fetchData (deps[ 0 ]);
return {
schema : { ... schema , props : { options : companies } }
};
}
}
];
Request Debouncing
For input linkage, use debouncing to avoid frequent triggers:
import { debounce } from 'lodash-es' ;
const fetchSuggestions = debounce ( async (keyword : string ) => {
const results = await api .search (keyword);
return results;
} , 300 );
const schema = [
{
type : 'string' ,
name : 'username' ,
label : 'Username' ,
deps : [] ,
onDepsChange : async () => {
// Note: Simplified demo, actual implementation needs more complex debouncing logic
}
}
];
Render Optimization
Avoid Unnecessary Re-renders
import { useMemo } from 'react' ;
function MyForm () {
// ✅ Use useMemo to cache schema, avoid creating new objects on each render
const schema = useMemo (() => [
{ type : 'string' , name : 'name' , label : 'Name' } ,
{ type : 'string' , name : 'email' , label : 'Email' }
] , []); // Empty dependency array, only created once
// ✅ Use useMemo to cache layout
const layout = useMemo (() => ({
formClassName : 'my-form' ,
formStyle : { maxWidth : '600px' }
}) , []);
return < Form schema = {schema} layout = {layout} />;
}
Use React.memo to Wrap Components
import { memo } from 'react' ;
// Use React.memo to avoid unnecessary re-renders
const CustomField = memo (({ value , onChange } : any ) => {
return (
< input
value = {value || '' }
onChange = {(e) => onChange ( e . target .value)}
style = {{ padding : '8px' , border : '1px solid #ccc' }}
/>
);
});
// Register custom component
const components = {
DefaultControl : CustomField
};
Validation Optimization
Partial vs Global Validation
Choose the appropriate validation strategy for your use case:
import { useRef } from 'react' ;
function MyForm () {
const formRef = useRef ( null );
// ✅ Validate all fields on submit
const handleSubmit = async () => {
const { data , errors } = await formRef . current .validate ();
if (errors) {
console .log ( 'Validation failed:' , errors);
return ;
}
console .log ( 'Validation passed:' , data);
};
return < Form ref = {formRef} schema = {schema} />;
}
Avoid Unnecessary Global Validation
When the form has many fields, avoid triggering global validation on every value change. Instead, validate only on submission:
import { useRef , useCallback } from 'react' ;
function MyForm () {
const formRef = useRef ( null );
// ✅ Only trigger global validation on submit, not on every value change
const handleSubmit = useCallback ( async () => {
const { data , errors } = await formRef . current .validate ();
if (errors) {
console .log ( 'Validation failed:' , errors);
return ;
}
console .log ( 'Validation passed:' , data);
} , []);
return (
<>
< Form ref = {formRef} schema = {schema} />
< button onClick = {handleSubmit}>Submit</ button >
</>
);
}
Validation Strategy Comparison :
Best Practices Summary
The following data is based on typical scenario testing for reference:
Note : The above data is for reference only. Actual performance depends on field complexity, linkage logic complexity, machine performance, and other factors.
Use React DevTools Profiler to monitor form performance:
import { Profiler } from 'react' ;
function onRenderCallback (
id , // The "id" of the Profiler tree that committed
phase , // "mount" (first mount) or "update" (re-render)
actualDuration , // Time spent rendering the committed update in milliseconds
baseDuration , // Estimated time to render the entire subtree without memoization
startTime , // When React began rendering this update
commitTime // When React committed this update
) {
// If render time is too long, output a warning
if (actualDuration > 100 ) {
console .warn ( `Form render took ${ actualDuration } ms` , {
phase ,
fieldsCount : schema . length ,
hasComplexDeps : schema .some (f => f . deps ?. length > 0 )
});
}
}
function MyForm () {
return (
< Profiler id = "MyForm" onRender = {onRenderCallback}>
< Form schema = {schema} components = {components} layout = {layout} />
</ Profiler >
);
}
Before and After Optimization Example
Before Optimization :
function MyForm () {
// ❌ Creating new objects on every render
const schema = [
{ type : 'string' , name : 'name' , label : 'Name' } ,
{ type : 'string' , name : 'email' , label : 'Email' }
];
const layout = {
formClassName : 'my-form' ,
formStyle : { maxWidth : '600px' }
};
// ❌ Creating new function on every render
const handleChange = ({ data }) => {
console .log (data);
};
return < Form schema = {schema} layout = {layout} onChange = {handleChange} />;
}
After Optimization :
function MyForm () {
// ✅ Use useMemo to cache schema
const schema = useMemo (() => [
{ type : 'string' , name : 'name' , label : 'Name' } ,
{ type : 'string' , name : 'email' , label : 'Email' }
] , []);
// ✅ Use useMemo to cache layout
const layout = useMemo (() => ({
formClassName : 'my-form' ,
formStyle : { maxWidth : '600px' }
}) , []);
// ✅ Use useCallback to cache callback
const handleChange = useCallback (({ data }) => {
console .log (data);
} , []);
return < Form schema = {schema} layout = {layout} onChange = {handleChange} />;
}
Benchmark Code
Use performance.now() to measure key operation duration:
function benchmarkFormRender (schema , iterations = 100 ) {
const times = [];
for ( let i = 0 ; i < iterations; i ++ ) {
const start = performance .now ();
// Render form
const end = performance .now ();
times .push (end - start);
}
const avg = times .reduce ((a , b) => a + b) / times . length ;
const max = Math .max ( ... times);
const min = Math .min ( ... times);
console .log ( `Benchmark Results ( ${ iterations } iterations):` );
console .log ( ` Average: ${ avg .toFixed ( 2 ) } ms` );
console .log ( ` Max: ${ max .toFixed ( 2 ) } ms` );
console .log ( ` Min: ${ min .toFixed ( 2 ) } ms` );
return { avg , max , min };
}
Using React Profiler
import { Profiler } from 'react' ;
function onRenderCallback (id , phase , actualDuration) {
console .log ( ` ${ id } ${ phase } : ${ actualDuration .toFixed ( 2 ) } ms` );
}
// Wrap form component
< Profiler id = "MyForm" onRender = {onRenderCallback}>
< Form schema = {schema} components = {components} />
</ Profiler >
Memory Usage Monitoring
// Measure memory usage (only in supported browsers)
function measureMemory () {
if ( performance .memory) {
return {
usedJSHeapSize : ( performance . memory .usedJSHeapSize / 1048576 ) .toFixed ( 2 ) + ' MB' ,
totalJSHeapSize : ( performance . memory .totalJSHeapSize / 1048576 ) .toFixed ( 2 ) + ' MB'
};
}
return null ;
}
More Optimization Cases
When form contains 100+ fields, use lazy loading strategy:
function LazyForm () {
const [ visibleRange , setVisibleRange ] = useState ({ start : 0 , end : 20 });
const visibleSchema = useMemo (() =>
schema .slice ( visibleRange .start , visibleRange .end) ,
[visibleRange]
);
const handleScroll = useCallback ((e) => {
const { scrollTop , clientHeight } = e .target;
const itemHeight = 60 ; // Height per field
const start = Math .floor (scrollTop / itemHeight);
const end = Math .ceil ((scrollTop + clientHeight) / itemHeight);
setVisibleRange ({ start : Math .max ( 0 , start - 5 ) , end : end + 5 });
} , []);
return (
< div onScroll = {handleScroll} style = {{ height : '600px' , overflow : 'auto' }}>
< Form schema = {visibleSchema} components = {components} />
</ div >
);
}
Case 2: Async Validation
const schema = [
{
type : 'string' ,
name : 'username' ,
label : 'Username' ,
validator : async ({ value }) => {
// Simulate async validation (e.g., check if username exists)
const response = await checkUsernameExists (value);
if ( response .exists) {
return 'Username already exists' ;
}
return undefined ;
}
}
];
Case 3: Conditional Rendering Optimization
function ConditionalForm () {
const [ showAdvanced , setShowAdvanced ] = useState ( false );
// ✅ Base schema always exists
const baseSchema = useMemo (() => [
{ type : 'string' , name : 'name' , label : 'Name' } ,
{ type : 'string' , name : 'email' , label : 'Email' }
] , []);
// ✅ Advanced config only added when needed
const advancedSchema = useMemo (() =>
showAdvanced ? [
{ type : 'string' , name : 'phone' , label : 'Phone' } ,
{ type : 'string' , name : 'address' , label : 'Address' }
] : [] ,
[showAdvanced]
);
const schema = useMemo (() => [ ... baseSchema , ... advancedSchema] , [baseSchema , advancedSchema]);
return (
<>
< Form schema = {schema} />
< button onClick = {() => setShowAdvanced ( ! showAdvanced)}>
{showAdvanced ? 'Hide' : 'Show' } Advanced Options
</ button >
</>
);
}