Говоря о направлении оптимизации производительности React

JavaScript React.js
Говоря о направлении оптимизации производительности React

Эта статья является результатом молниеносного обмена информацией внутри компании и опубликована с небольшими исправлениями. В основном обсуждается основное направление и некоторые советы по оптимизации производительности React.Если вы думаете, что можете, пожалуйста, ставьте больше лайков и поощряйте меня писать больше интересных статей.🙏.


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

  • уменьшить количество вычислений. -> Соответствует ReactУменьшите визуализированные узлы или упростите визуализацию компонентов.
  • Использовать кеш. -> Соответствует ReactКак избежать повторного рендеринга и использования функциональной программирования MEMO, чтобы избежать повторного рендеринга компонентов
  • Точно пересчитанный диапазон. Соответствует ReactСвязывайте компоненты и отношения между состояниями, точно определяйте «время» и «объем» обновлений. Повторно визуализируйте только «грязные» компоненты или уменьшите область визуализации.

содержание



Сокращение узлов рендеринга/уменьшение вычислений рендеринга (сложность)

Во-первых, усердно работайте над объемом вычислений, уменьшая количество рендеринга узлов или уменьшая объем вычислений рендеринга, что может значительно улучшить производительность рендеринга компонентов.


0️⃣ Не производить лишних вычислений в функциях рендеринга

Например, не делайте в функции рендеринга (render) сортировку массивов, преобразование данных, подписку на события, создание обработчиков событий и т.д.В функции рендеринга не должно быть слишком много побочных эффектов.


1️⃣ Уменьшить ненужную вложенность

Наша команда тяжелаяstyled-componentsПользователь,На самом деле, большую часть времени он нам не нужен., такие как чисто статические правила стиля и сценарии, требующие серьезной оптимизации производительности. Помимо проблем с производительностью, еще одна проблема, которая нас беспокоит, — это ад вложенности узлов, который он приносит (как показано выше).

Поэтому нам нужно рационально выбирать некоторые инструменты, такие как использование нативного CSS, чтобы уменьшить нагрузку на среду выполнения React.

Как правило, ненужное вложение узлов вызвано злоупотреблением компонентами более высокого порядка/RenderProps. Это то же самое, что и поговорка «используйте xxx только при необходимости».. Есть много способов заменить компоненты более высокого порядка / RenderProps, например предпочтительное использование реквизита, React Hooks.


2️⃣ Виртуальный список

Виртуальный список является распространенным методом оптимизации «длинного списка» и «сложного дерева компонентов», суть его оптимизации заключается в уменьшении количества отображаемых узлов.

Виртуальный список отображает только элементы, видимые в текущем окне просмотра:

Сравнение производительности рендеринга виртуального списка:

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

  • Бесконечная прокрутка списков, сеток, таблиц, раскрывающихся списков, электронных таблиц
  • Неограниченное переключение календаря или карусели
  • Большие объемы данных или бесконечно вложенные деревья
  • Окно чата, поток данных (канал), временная шкала
  • так далее

Решения связанных компонентов:

Расширение:



3️⃣ Ленивый рендеринг

Разум по сути инертен рендерингу и виртуальным таблицам, а это значит, что мы только при необходимости рендерим соответствующий узел.

В качестве типичного примера мы часто используем компонент Tab.Нам не нужно рендерить все панели вкладки в начале, а нужно подождать, пока вкладка активируется, чтобы рендерить ее лениво.

Существует также множество сценариев, в которых используется ленивый рендеринг, например селекторы дерева, модальные всплывающие окна, раскрывающиеся списки, сворачиваемые компоненты и т. д.

Я не буду приводить здесь конкретные примеры кода и предоставлю читателю подумать над этим.


4️⃣ Подберите подходящую схему стиля

Как показано на картинке (картинка взята изTHE PERFORMANCE OF STYLED REACT COMPONENTS), эта картина 17 лет, но общая тенденция все еще одинакова.

Таким образом, с точки зрения производительности стиля во время выполнения его можно резюмировать следующим образом:CSS > 大部分CSS-in-js > inline style




избегать повторного рендеринга

Сокращение ненужного повторного рендеринга также является важным направлением оптимизации производительности компонентов React.Чтобы избежать ненужного повторного рендеринга компонентов, вам нужно сделать две вещи:

  1. Чистота компонентов гарантируется. то есть управлять побочными эффектами компонента, если компонент имеет побочные эффекты, то результат рендеринга не может быть безопасно кэширован
  2. пройти черезshouldComponentUpdateФункции жизненного цикла для сравнения состояния и свойств, чтобы определить, следует ли выполнять повторную визуализацию. Для функциональных компонентов вы можете использоватьReact.memoУпаковка

Кроме того, эти меры также могут упростить оптимизацию повторного рендеринга компонентов:


0️⃣ Упрощение реквизита

① Если свойства компонента слишком сложны, это обычно означает, что компонент нарушил «единственную ответственность», и вам следует сначала попытаться разобрать компонент.. ② Кроме того, сложный реквизит станет трудным в обслуживании, например,shallowCompareэффективности, но также затрудняет прогнозирование и отладку изменений компонентов..

Ниже приведен типичный пример, чтобы определить, активен ли элемент списка, передается текущий активный идентификатор:

Это очень плохой дизайн, после активации смены идентификатора все элементы списка будут обновлены Лучшее решение - использовать что-то вродеactivedТакой булев prop.actived теперь имеет только два изменения, то есть при изменении активного id требуется перерендерить максимум два компонента.

Упрощенные реквизиты легче понять и могут улучшить частоту попаданий в кеш компонентов.


1️⃣ Неизменяемые обработчики событий

Избегайте использования обработчиков событий в виде стрелочных функций., например:

<ComplexComponent onClick={evt => onClick(evt.id)} otherProps={values} />

Предполагая, что ComplexComponent является сложным PureComponent, здесь используются стрелочные функции, по сути, новый обработчик событий создается каждый раз при его рендеринге, из-за чего ComplexComponent всегда будет перерисовываться.

Пример Лучший способ - использовать метод:

class MyComponent extends Component {
  render() {
    <ComplexComponent onClick={this.handleClick} otherProps={values} />;
  }
  handleClick = () => {
    /*...*/
  };
}

② Даже если вы используете его сейчасhooks, буду ещеиспользоватьuseCallbackобернуть обработчик события, попробуйте выставить статическую функцию подчиненным компонентам:

const handleClick = useCallback(() => {
  /*...*/
}, []);

return <ComplexComponent onClick={handleClick} otherProps={values} />;

но еслиuseCallbackВ зависимости от большого количества статуса, вашuseCallbackможет стать таким:

const handleClick = useCallback(() => {
  /*...*/
  // 🤭
}, [foo, bar, baz, bazz, bazzzz]);

Такой способ написания действительно неприемлем, и кого сейчас волнует функциональное и нефункциональное. Я обрабатываю это так:

function useRefProps<T>(props: T) {
  const ref = useRef < T > props;
  // 每次渲染更新props
  useEffect(() => {
    ref.current = props;
  });
}

function MyComp(props) {
  const propsRef = useRefProps(props);

  // 现在handleClick是始终不变的
  const handleClick = useCallback(() => {
    const { foo, bar, baz, bazz, bazzzz } = propsRef.current;
    // do something
  }, []);
}

Создавайте реквизит для мероприятий, с которым проще обращатьсяИногда мы вынуждены использовать стрелочные функции в качестве обработчиков событий:

<List>
  {list.map(i => (
    <Item key={i.id} onClick={() => handleDelete(i.id)} value={i.value} />
  ))}
</List>

Вышеупомянутый onClick является плохой реализацией, он не несет никакой информации для идентификации источника события, поэтому здесь можно использовать только форму закрытия, лучший дизайн может быть таким:

// onClick传递事件来源信息
const handleDelete = useCallback((id: string) => {
  /*删除操作*/
}, []);

return (
  <List>
    {list.map(i => (
      <Item key={i.id} id={i.id} onClick={handleDelete} value={i.value} />
    ))}
  </List>
);

Что, если это сторонний компонент или компонент DOM? Это действительно невозможно, посмотрите, можно ли его передатьdata-*Атрибуты:

const handleDelete = useCallback(event => {
  const id = event.currentTarget.dataset.id;
  /*删除操作*/
}, []);

return (
  <ul>
    {list.map(i => (
      <li key={i.id} data-id={i.id} onClick={handleDelete} value={i.value} />
    ))}
  </ul>
);


2️⃣ Неизменяемые данные

Неизменяемые данные могут сделать состояние предсказуемым и сделать «поверхностные сравнения» shouldComponentUpdate более надежными и эффективными.Краткий обзор практики проектирования компонентов React 04 — Компонентное мышлениеВведены неизменяемые данные, заинтересованные читатели могут взглянуть.

Связанные инструментыImmutable.js,Immer, помощник неизменности и плавный неизменяемый.


3️⃣ Упростить состояние

Не все состояния должны быть помещены в состояние компонента., Например данные кеша. По моим принципам:Если компонент должен реагировать на свои изменения или данные должны быть отображены в представлении, он должен быть помещен в состояние. Это позволяет избежать ненужных изменений данных, вызывающих повторную визуализацию компонента.


4️⃣ Используйте перекомпоновку для уточнения сравнения

Несмотря на то, что рекомпозитор отключается, объявленный recompose больше не обновляется, но все же не влияет на наше использование recompose для контроля.shouldComponentUpdateТакой метод предоставляет следующие методы для точного контроля, какие PrOP следует сравнивать:

 /* 相当于React.memo */
 pure()
 /* 自定义比较 */
 shouldUpdate(test: (props: Object, nextProps: Object) => boolean): HigherOrderComponent
 /* 只比较指定key */
 onlyUpdateForKeys( propKeys: Array<string>): HigherOrderComponent

На самом деле его можно расширить, например,omitUpdateForKeysИгнорировать сопоставление некоторых ключей.



уточненный рендеринг

Так называемый уточненный рендеринг относится кСуществует только один источник данных, вызывающий повторную визуализацию компонента., например, A зависит только от данных, тогда при изменении данных отображается только A, другие изменения состояния не должны влиять на компонент A.

Vue и Mobx утверждают, что их производительность частично связана с их «реактивными системами».Это позволяет нам определить некоторые «отзывчивые данные», и когда эти отзывчивые данные изменятся, представления, которые полагаются на эти отзывчивые данные, будут повторно отображены.Давайте посмотрим, как Vue официально описывает это:


0️⃣ Уточнение рендеринга адаптивных данных

В большинстве случаев отзывчивые данные могут обеспечить улучшенную визуализацию представлений, но они все же не могут помешать разработчикам писать неэффективные программы.. По сути, это потому, что компонент нарушает «единую ответственность»..

Например, теперь есть компонент MyComponent, который использует три источника данных A, B и C для построения дерева vdom. В чем проблема сейчас? Теперь, пока любой из A, B, C изменяется, MyComponent будет повторно отображаться:

Лучший подход состоит в том, чтобы сделать обязанности компонентов более едиными и полагаться на ответные данные уточненным образом или «изолировать» ответные данные.Как показано на рисунке ниже, A, B и C все извлечены из соответствующих компонентов, и теперь A изменяется Только сам компонент A будет визуализирован, не затрагивая родительский компонент, а также компоненты B и C:


Чтобы привести типичный пример, рендеринг списка:

import React from 'react';
import { observable } from 'mobx';
import { observer } from 'mobx-react-lite';

const initialList = [];
for (let i = 0; i < 10; i++) {
  initialList.push({ id: i, name: `name-${i}`, value: 0 });
}

const store = observable({
  list: initialList,
});

export const List = observer(() => {
  const list = store.list;
  console.log('List渲染');
  return (
    <div className="list-container">
      <ul>
        {list.map((i, idx) => (
          <div className="list-item" key={i.id}>
            {/* 假设这是一个复杂的组件 */}
            {console.log('render', i.id)}
            <span className="list-item-name">{i.name} </span>
            <span className="list-item-value">{i.value} </span>
            <button
              className="list-item-increment"
              onClick={() => {
                i.value++;
                console.log('递增');
              }}
            >
              递增
            </button>
            <button
              className="list-item-increment"
              onClick={() => {
                if (idx < list.length - 1) {
                  console.log('移位');
                  let t = list[idx];
                  list[idx] = list[idx + 1];
                  list[idx + 1] = t;
                }
              }}
            >
              下移
            </button>
          </div>
        ))}
      </ul>
    </div>
  );
});

В приведенном выше примере есть проблемы с производительностью, увеличение и сдвиг одного элемента списка приведет к повторному отображению всего списка:

О причине, наверное, можно догадаться: для Vue или Mobx,Функция рендеринга компонента является контекстом для сбора зависимостей.. Вышеупомянутая функция рендеринга компонента списка имеет «доступ» ко всем данным элемента списка, тогда Vue или Mobx будут думать, что ваш компонент зависит от всех элементов списка, что приведет к повторному рендерингу до тех пор, пока значение свойства любого элемента списка изменяется. Компонент списка.

Решение также состоит в том, чтобы извлечь изоляцию данных в компоненты с единой ответственностью.Для Vue или Mobx более мелкозернистый компонент, эффект более высокого урожая можно оптимизировать производительность:

export const ListItem = observer(props => {
  const { item, onShiftDown } = props;
  return (
    <div className="list-item">
      {console.log('render', item.id)}
      {/* 假设这是一个复杂的组件 */}
      <span className="list-item-name">{item.name} </span>
      <span className="list-item-value">{item.value} </span>
      <button
        className="list-item-increment"
        onClick={() => {
          item.value++;
          console.log('递增');
        }}
      >
        递增
      </button>
      <button className="list-item-increment" onClick={() => onShiftDown(item)}>
        下移
      </button>
    </div>
  );
});

export const List = observer(() => {
  const list = store.list;
  const handleShiftDown = useCallback(item => {
    const idx = list.findIndex(i => i.id === item.id);
    if (idx !== -1 && idx < list.length - 1) {
      console.log('移位');
      let t = list[idx];
      list[idx] = list[idx + 1];
      list[idx + 1] = t;
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  console.log('List 渲染');

  return (
    <div className="list-container">
      <ul>
        {list.map((i, idx) => (
          <ListItem key={i.id} item={i} onShiftDown={handleShiftDown} />
        ))}
      </ul>
    </div>
  );
});

Эффект очевиден, приращение элемента списка только перерисовывает себя, а смещение только перерисовывает список, потому что элемент списка не изменился, поэтому подчиненный элемент списка не нужно перерисовывать. :



1️⃣ Не злоупотребляйте контекстом

На самом деле, использование Context — это полная противоположность реактивным данным.. Автор также видел много примеров злоупотребления Context API, но, в конце концов, я так и не разобрался с «проблемой области действия».

Прежде всего, мы должны понять характеристики обновления Context API,это проницаемоReact.memoилиshouldComponentUpdateСравнение, то есть после изменения значения контекста, все компоненты, зависящие от контекста, будут принудительно обновлены..

Это отличается от отзывчивых систем Mobx и Vue. Context API не может точно определить, какие компоненты зависят от каких состояний. узор» в контексте..

Подводя итог, используйте Context API, чтобы следовать следующим принципам:


  • Очистить область состояния, Контекст размещает только необходимые, критические состояния, общие для большинства компонентов.. Как правило, состояние аутентификации

    Возьмем простой пример:

    Расширение: Контекст на самом деле имеет экспериментальный или закрытый вариант.observedBits, который можно использовать для управления необходимостью обновления ContextConsumer. Подробнее см. в этой статьеObservedBits: секретная функция React Context>.Однако не рекомендуется использовать в реальных проектах, и этот API также сложен в использовании, лучше сразу перейти на mobx.

  • Грубая подписка на Context

    Как показано на рисунке ниже, мелкозернистая подписка Context приведет к ненужному повторному рендерингу, поэтому здесь рекомендуется крупнозернистая подписка, например, подписаться на Context в родительском элементе, а затем передать его подчиненному через props.


Кроме того, Ченг Мо Морган находится вИзбегайте дублирования рендеринга, вызванного React ContextВ статье также упоминается ловушка ContextAPI:

<Context.Provider
  value={{ theme: this.state.theme, switchTheme: this.switchTheme }}
>
  <div className="App">
    <Header />
    <Content />
  </div>
</Context.Provider>

Вышеупомянутый компонент будет повторно отображать все дерево компонентов при изменении состояния, оставляя читателю думать, почему.

Поэтому мы обычно не используем Context.Provider голым, а инкапсулируем его как независимый компонент Provider:

export function ThemeProvider(props) {
  const [theme, switchTheme] = useState(redTheme);
  return (
    <Context.Provider value={{ theme, switchTheme }}>
      {props.children}
    </Context.Provider>
  );
}

// 顺便暴露useTheme, 让外部必须直接使用Context
export function useTheme() {
  return useContext(Context);
}

Теперь изменения темы не будут перерисовывать все дерево компонентов, потому что props.children передается извне и не изменился.

На самом деле в приведенном выше коде есть еще одна ловушка, которую сложнее найти (в официальной документации также естьупомянул):

export function ThemeProvider(props) {
  const [theme, switchTheme] = useState(redTheme);
  return (
    {/* 👇 💣这里每一次渲染ThemeProvider, 都会创建一个新的value(即使theme和switchTheme没有变动),
        从而导致强制渲染所有依赖该Context的组件 */}
    <Context.Provider value={{ theme, switchTheme }}>
      {props.children}
    </Context.Provider>
  );
}

такЛучше всего кешировать значение, переданное в Context:

export function ThemeProvider(props) {
  const [theme, switchTheme] = useState(redTheme);
  const value = useMemo(() => ({ theme, switchTheme }), [theme]);
  return <Context.Provider value={value}>{props.children}</Context.Provider>;
}


расширять