Интерфейсы и frontend
March 18

Вычисляемые свойства во Vue сломаны?

На reddit наткнулся на пост в комьюнити Vue.JS с описанием проблемы с производительностью у вычисляемых свойств (computed). Заключается проблема в том, что судя по тестам использованием watch + ref на 50% более эффективно, чем использованием computed.

Добавим немного контекста. Тест проводился на дропдауне с 1 000 000 элементов. Задача теста состояла в том, чтобы написать функцию, которая будет конвертировать объекты (или строки) с типом selectOption в объект selectOptionsObject.

Типы выглядят так:

export type selectOption = selectOptionObject | string;  
export type selectOptionObject = {
  id: string | number;
  render: string;
  raw?: any;
};

Код функции normaliseOptions для конвертации:

const normaliseOptions = (
  options?: selectOption[]
): normalisedOptionObject[] => {
  if (!options) return [];

  // We will use a straight for loop for performance
  const normalisedOptions = [];
  for (let i = 0; i < options.length; i++) {
    const option = options[i];
    if (typeof option === "string") {
      normalisedOptions.push({
        id: option,
        render: option,
      });
      continue;
    }
    normalisedOptions.push({
      id: option.id.toString(),
      render: option.render,
      disabled: option.disabled || false,
      raw: option.raw,
    });
  }
  return normalisedOptions;
};

Код, который использовался в тесте:

С использованием computed:

const normalisedOptions = computed(() => {
 return normaliseOptions(props.options);
});

С использованием watch + ref:

// Using the pattern below rather than a computed value gives us a 2x performance improvement
const normalisedOptions = ref(normaliseOptions(props.options));
const recomputeOptions = () => {
 normalisedOptions.value = normaliseOptions(props.options);
};

watch(
 () => props.options,
 () => {
   recomputeOptions();
 }
);

Самые умные и опытные уже догадались, в чём может заключаться проблема конкретно в данном случае (а я не успел самостоятельно подумать и наткнулся на ответ в комментариях). Проблема заключается в том, что computed отслеживает каждую вложенную зависимость. Поэтому он будет работать медленнее, если props.options не является плоской структурой, состоящей из примитивов, потому что требуется провести кратно больше вычислений. Таким образом, корректный код для данного случая выглядит так:

const recomputedOptions = computed(() => normaliseOptions(toRaw(props.options)));

Метод toRaw извлекает необработанный объект из proxy-объекта, созданного Vue, давая нам возможность поработать с оригинальным объектом. Подробнее про него можно почитать в документации.