Performance Optimization

Framework:

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:

  1. Control Linkage Depth: Avoid more than 3-4 levels of cascading dependencies
  2. Reasonably Split Schema: Split complex forms into multiple independent Groups
  3. 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
  }
];

Complex Form Splitting

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} />;
}

Option 2: Split into Multiple Forms

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:

StrategyMethodUse Case
Global Validationvalidate()On form submission
Automatic ValidationTriggered on field value changeUser input (default behavior)

Best Practices Summary

Performance Optimization Checklist

ScenarioRecommendation
Complex DependenciesControl linkage depth, no more than 3-4 levels
Large FormsUse Group splitting, or split into multiple Forms
Async RequestsCache results, use debouncing
Frequent RendersUse useMemo/useCallback
Custom ComponentsWrap with React.memo
ValidationUse partial validation instead of global validation

Performance Benchmarks

The following data is based on typical scenario testing for reference:

ScenarioField CountFirst RenderLinkage TriggerRecommendation
Simple Form< 10< 50ms< 10msNo optimization needed
Medium Form10-5050-200ms10-50msRecommend using useMemo
Complex Form50-100200-500ms50-100msRecommend splitting Groups
Very Large Form> 100> 500ms> 100msRecommend splitting into multiple Forms

Note: The above data is for reference only. Actual performance depends on field complexity, linkage logic complexity, machine performance, and other factors.

Performance Monitoring

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>
  );
}

Common Performance Problem Diagnosis

SymptomPossible CauseSolution
Slow first renderSchema object too largeUse useMemo to cache
Input lagSlow onChange callback executionUse useCallback to cache
Too many linkage triggersDependency chain too longControl linkage depth, use debouncing
High memory usageComponents not properly unmountedCheck useEffect cleanup
Slow validationFrequent global validationUse partial validation

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} />;
}

Performance Testing Methods

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

Case 1: Lazy Loading for Large Forms

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>
    </>
  );
}