Оптимизация ре-рендеров

Интеграция Yup через yupResolver чаще всего используется совместно с библиотекой React Hook Form. Несмотря на высокую производительность этой связки, неправильная организация схем валидации и структуры формы может приводить к большому количеству повторных рендеров компонентов.

Основные причины:

  • пересоздание схемы Yup на каждом рендере;
  • динамическое создание resolver-функции;
  • подписка компонентов на весь formState;
  • использование watch() без ограничений;
  • валидация всей формы вместо отдельных полей;
  • изменение ссылочных типов (object, array, function);
  • работа с тяжёлыми вложенными схемами.

Как работает yupResolver внутри React Hook Form

yupResolver создаёт адаптер между схемой Yup и механизмом валидации React Hook Form.

Пример:

import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";

const schema = yup.object({
  email: yup.string().email().required(),
  password: yup.string().min(6).required()
});

const form = useForm({
  resolver: yupResolver(schema)
});

Во время изменения поля:

  1. React Hook Form вызывает resolver;
  2. resolver запускает schema.validate();
  3. формируется объект ошибок;
  4. обновляется formState;
  5. React выполняет ререндер подписанных компонентов.

Чем меньше изменений происходит в formState, тем меньше ре-рендеров.


Стабилизация схемы валидации

Проблема пересоздания схемы

Неправильный вариант:

function LoginForm() {
  const schema = yup.object({
    email: yup.string().required()
  });

  const form = useForm({
    resolver: yupResolver(schema)
  });

  return <form />;
}

На каждом рендере:

  • создаётся новый объект схемы;
  • создаётся новый resolver;
  • React Hook Form считает конфигурацию изменённой;
  • запускаются дополнительные вычисления.

Вынос схемы за пределы компонента

Оптимальный вариант:

const schema = yup.object({
  email: yup.string().required()
});

function LoginForm() {
  const form = useForm({
    resolver: yupResolver(schema)
  });

  return <form />;
}

Преимущества:

  • схема создаётся один раз;
  • resolver остаётся стабильным;
  • уменьшается нагрузка на garbage collector;
  • сокращается количество сравнений ссылок.

Использование useMemo для динамических схем

Иногда схема зависит от параметров.

Например:

function ProfileForm({ isAdmin }) {
  const schema = useMemo(() => {
    return yup.object({
      name: yup.string().required(),
      role: isAdmin
        ? yup.string().required()
        : yup.string()
    });
  }, [isAdmin]);

  const form = useForm({
    resolver: yupResolver(schema)
  });

  return <form />;
}

Без useMemo схема будет пересоздаваться постоянно.


Оптимизация resolver

Проблема инлайнового resolver

Плохой пример:

useForm({
  resolver: yupResolver(
    yup.object({
      name: yup.string().required()
    })
  )
});

Здесь одновременно создаются:

  • схема;
  • resolver;
  • внутренние функции валидации.

Кэширование resolver

const schema = yup.object({
  name: yup.string().required()
});

const resolver = yupResolver(schema);

function App() {
  const form = useForm({
    resolver
  });

  return <form />;
}

Это особенно важно в крупных формах с большим количеством полей.


Минимизация подписок на formState

Почему formState вызывает ре-рендеры

formState — реактивный объект.

При изменении:

  • errors;
  • dirtyFields;
  • touchedFields;
  • isValid;
  • isSubmitting;

происходит обновление подписанных компонентов.


Антипаттерн: деструктуризация всего formState

const {
  formState
} = useForm();

Или:

const {
  formState: { errors, isDirty, isValid }
} = useForm();

Компонент подписывается сразу на несколько состояний.


Локальная подписка через useFormState

Правильный подход:

const { control } = useForm();

const { errors } = useFormState({
  control,
  name: "email"
});

Теперь компонент реагирует только на изменения поля email.


Оптимизация watch()

Проблема глобального watch

const values = watch();

Такой вызов подписывает компонент на изменения всей формы.

Каждый ввод символа вызывает:

  • обновление объекта формы;
  • ререндер компонента.

Наблюдение за конкретным полем

const email = watch("email");

Или:

const [email, password] = watch([
  "email",
  "password"
]);

Количество обновлений становится значительно меньше.


Использование useWatch

useWatch изолирует ререндеры.

const email = useWatch({
  control,
  name: "email"
});

Преимущество:

  • обновляется только компонент с useWatch;
  • родительская форма не ререндерится.

Изоляция полей формы

Разделение формы на компоненты

Большой компонент:

function Form() {
  return (
    <>
      <input />
      <input />
      <input />
      <input />
    </>
  );
}

При изменении одного поля может обновиться весь компонент.


Компонентная сегментация

const EmailField = memo(() => {
  const { register } = useFormContext();

  return <input {...register("email")} />;
});

Теперь ререндер ограничивается конкретным полем.


React.memo и YupResolver

Мемоизация полей

const InputField = React.memo(function InputField({
  register,
  name
}) {
  return <input {...register(name)} />;
});

Если пропсы не изменились:

  • React пропускает ререндер;
  • поле остаётся неизменным.

Опасность новых ссылок

Антипаттерн:

<InputField rules={{ required: true }} />

На каждом рендере создаётся новый объект.

Правильнее:

const rules = useMemo(() => ({
  required: true
}), []);

<InputField rules={rules} />

Оптимизация mode в useForm

Влияние режима валидации

useForm({
  mode: "onChange"
});

onChange запускает валидацию при каждом вводе символа.

Это самый тяжёлый режим.


Более производительные режимы

onBlur

useForm({
  mode: "onBlur"
});

Валидация запускается после потери фокуса.


onSubmit

useForm({
  mode: "onSubmit"
});

Самый дешёвый режим:

  • минимальное число валидаций;
  • минимальное число ререндеров.

reValidateMode

useForm({
  mode: "onSubmit",
  reValidateMode: "onBlur"
});

Хороший баланс между UX и производительностью.


abortEarly и производительность

Поведение по умолчанию

Yup может собирать все ошибки формы:

schema.validate(data, {
  abortEarly: false
});

Это дороже по вычислениям.


Прерывание после первой ошибки

schema.validate(data, {
  abortEarly: true
});

Преимущества:

  • меньше проходов по схеме;
  • быстрее валидация;
  • меньше обновлений ошибок.

Особенно заметно в больших формах.


Lazy-схемы и условная валидация

Проблема тяжёлых условных схем

yup.object({
  type: yup.string(),
  data: yup.object().when("type", {
    is: "admin",
    then: heavyAdminSchema
  })
});

Сложные ветки могут вычисляться слишком часто.


Использование yup.lazy()

const schema = yup.object({
  type: yup.string(),
  data: yup.lazy((value) => {
    if (value?.type === "admin") {
      return adminSchema;
    }

    return userSchema;
  })
});

Преимущества:

  • схема выбирается динамически;
  • уменьшается число ненужных проверок;
  • снижается нагрузка на рендеринг.

Асинхронная валидация и ре-рендеры

Проблема async validation

yup.string().test(
  "email-check",
  "Email already exists",
  async (value) => {
    const res = await api.checkEmail(value);
    return res.ok;
  }
);

При каждом вводе:

  • создаётся запрос;
  • меняется isValidating;
  • происходят ререндеры.

Debounce для асинхронной проверки

const debouncedCheck = debounce(async (value) => {
  return api.checkEmail(value);
}, 500);

Преимущества:

  • меньше запросов;
  • меньше изменений состояния;
  • стабильнее интерфейс.

Оптимизация Field Arrays

Проблема useFieldArray

Массивы полей особенно чувствительны к ререндерам.

const { fields } = useFieldArray({
  control,
  name: "users"
});

При обновлении одного элемента может обновляться весь список.


Стабильные key

Правильно:

{
  fields.map((field) => (
    <UserRow
      key={field.id}
      field={field}
    />
  ));
}

Неправильно:

key={index}

Мемоизация строк массива

const UserRow = memo(function UserRow({
  field
}) {
  return <input defaultValue={field.name} />;
});

Это критически важно для больших таблиц.


Контролируемые и неконтролируемые компоненты

Преимущество uncontrolled inputs

React Hook Form изначально оптимизирован под неконтролируемые поля.

<input {...register("email")} />

Такой подход:

  • не хранит значение в React state;
  • уменьшает число ререндеров;
  • ускоряет ввод.

Проблема Controller

<Controller
  control={control}
  name="email"
  render={({ field }) => (
    <Input {...field} />
  )}
/>

Controller делает компонент контролируемым.

Это увеличивает:

  • число обновлений;
  • нагрузку на reconciliation;
  • стоимость рендеринга.

Когда Controller оправдан

Использование допустимо для:

  • сложных UI-библиотек;
  • masked inputs;
  • date pickers;
  • rich text editors.

Во всех остальных случаях register() производительнее.


Профилирование ререндеров

React DevTools Profiler

Основной инструмент анализа:

  • количество ререндеров;
  • длительность рендера;
  • источник обновления;
  • дерево обновляемых компонентов.

why-did-you-render

Библиотека помогает выявлять:

  • изменения ссылок;
  • бесполезные обновления;
  • повторные рендеры memo-компонентов.

Пример:

InputField.whyDidYouRender = true;

Архитектурные стратегии оптимизации

Разделение больших форм

Плохо:

<MassiveForm />

Лучше:

<UserSection />
<AddressSection />
<SettingsSection />

Ленивые секции

const AdminSection = lazy(() => import("./AdminSection"));

Преимущества:

  • уменьшение initial render;
  • снижение нагрузки на hydration;
  • более быстрая загрузка.

Многошаговые формы

step === 1 && <StepOne />
step === 2 && <StepTwo />

Это уменьшает:

  • количество активных полей;
  • число подписок;
  • объём валидации Yup.

Оптимизация сложных Yup-схем

Повторное использование частей схемы

const emailSchema = yup.string()
  .email()
  .required();

const schema = yup.object({
  email: emailSchema
});

Преимущества:

  • меньше дублирования;
  • меньше пересозданий;
  • проще memoization.

Избегание deeply nested схем

Плохо:

yup.object({
  a: yup.object({
    b: yup.object({
      c: yup.object({
        d: yup.string()
      })
    })
  })
});

Глубокие структуры:

  • тяжелее валидируются;
  • сложнее сравниваются;
  • увеличивают стоимость обновлений.

Практический пример оптимизированной формы

const schema = yup.object({
  email: yup.string()
    .email()
    .required(),

  password: yup.string()
    .min(6)
    .required()
});

const resolver = yupResolver(schema);

const EmailField = memo(() => {
  const { register } = useFormContext();

  return (
    <input
      {...register("email")}
    />
  );
});

const PasswordField = memo(() => {
  const { register } = useFormContext();

  return (
    <input
      type="password"
      {...register("password")}
    />
  );
});

function LoginForm() {
  const methods = useForm({
    resolver,
    mode: "onBlur"
  });

  return (
    <FormProvider {...methods}>
      <form>
        <EmailField />
        <PasswordField />
      </form>
    </FormProvider>
  );
}

Особенности оптимизации:

  • схема вынесена наружу;
  • resolver стабилен;
  • поля мемоизированы;
  • используется onBlur;
  • минимизированы подписки;
  • отсутствуют лишние inline-объекты.

Ключевые правила минимизации ре-рендеров

Основные принципы

  • не создавать schema внутри компонента;
  • не пересоздавать resolver;
  • использовать useMemo;
  • избегать глобального watch();
  • подписываться только на нужные поля;
  • сегментировать форму;
  • использовать React.memo;
  • минимизировать Controller;
  • избегать onChange при тяжёлой валидации;
  • применять lazy-схемы;
  • использовать debounce для async validation;
  • профилировать интерфейс через DevTools.

Наиболее затратные операции

Самые дорогие источники ререндеров

Источник Стоимость
watch() без аргументов Очень высокая
mode: "onChange" Очень высокая
Async validation Высокая
Controller-компоненты Высокая
Глубокие Yup-схемы Высокая
Пересоздание schema Средняя
Глобальный formState Средняя
Большие FieldArray Средняя

Производительность в больших enterprise-формах

В крупных приложениях формы могут содержать:

  • сотни полей;
  • динамические секции;
  • сложные зависимости;
  • серверную валидацию;
  • вложенные массивы.

В таких сценариях оптимизация YupResolver становится обязательной частью архитектуры.

Типичный стек оптимизации:

  • React.memo;
  • useWatch;
  • FormProvider;
  • useFormContext;
  • ленивые секции;
  • раздельные схемы;
  • debounce;
  • виртуализация больших списков;
  • асинхронная подгрузка частей формы;
  • кэширование resolver и schema.

Даже небольшое сокращение числа ререндеров в таких формах может уменьшить время отклика интерфейса в несколько раз.