Я пишу React на работе, чему я научился? Оптимизация производительности

внешний интерфейс React.js

предисловие

Стек технологий в моей работе в основном React + TypeScript.В этой статье я хочу обобщить, как использовать некоторые навыки React в проекте для оптимизации производительности или лучшей организации кода.

Излишне говорить о важности оптимизации производительности, многие опросы, опубликованные Google, точно показывают влияние производительности на коэффициент удержания веб-сайта, в то время как оптимизация организации кода связана с последующими затратами на обслуживание, а ваши коллеги поддерживают ваш код. читая эту статью, вы обязательно что-то приобретете.

Эта статья была впервые опубликована в "Передняя часть от продвинутого до допуска". Следуй за мной и перейди на следующий уровень~

волшебные дети

У нас есть требование передавать некоторую информацию о теме подкомпонентам через Provider:

Посмотрите на этот фрагмент кода:

import React, { useContext, useState } from "react";

const ThemeContext = React.createContext();

export function ChildNonTheme() {
  console.log("不关心皮肤的子组件渲染了");
  return <div>我不关心皮肤,皮肤改变的时候别让我重新渲染!</div>;
}

export function ChildWithTheme() {
  const theme = useContext(ThemeContext);
  return <div>我是有皮肤的哦~ {theme}</div>;
}

export default function App() {
  const [theme, setTheme] = useState("light");
  const onChangeTheme = () => setTheme(theme === "light" ? "dark" : "light");
  return (
    <ThemeContext.Provider value={theme}>
      <button onClick={onChangeTheme}>改变皮肤</button>
      <ChildWithTheme />
      <ChildNonTheme />
      <ChildNonTheme />
      <ChildNonTheme />
      <ChildNonTheme />
      <ChildNonTheme />
      <ChildNonTheme />
      <ChildNonTheme />
    </ThemeContext.Provider>
  );
}

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

По сути, это связано с тем, что React рекурсивно обновляется сверху вниз,<ChildNonTheme />Такой код будет переведен Babel вReact.createElement(ChildNonTheme)Для таких вызовов функций официальные лица React часто подчеркивают, что реквизиты неизменяемы, поэтому каждый раз, когда вызывается функциональный компонент, будет генерироваться новая ссылка на реквизиты.

посмотриcreateElementСтруктура возврата:

const childNonThemeElement = {
  type: 'ChildNonTheme',
  props: {} // <- 这个引用更新了
}

Именно из-за этой новой ссылки на реквизитChildNonThemeЭтот компонент также перерисовывается.

Итак, как избежать этого недопустимого повторного рендеринга? Ключевое слово — «умное использование детей».

import React, { useContext, useState } from "react";

const ThemeContext = React.createContext();

function ChildNonTheme() {
  console.log("不关心皮肤的子组件渲染了");
  return <div>我不关心皮肤,皮肤改变的时候别让我重新渲染!</div>;
}

function ChildWithTheme() {
  const theme = useContext(ThemeContext);
  return <div>我是有皮肤的哦~ {theme}</div>;
}

function ThemeApp({ children }) {
  const [theme, setTheme] = useState("light");
  const onChangeTheme = () => setTheme(theme === "light" ? "dark" : "light");
  return (
    <ThemeContext.Provider value={theme}>
      <button onClick={onChangeTheme}>改变皮肤</button>
      {children}
    </ThemeContext.Provider>
  );
}

export default function App() {
  return (
    <ThemeApp>
      <ChildWithTheme />
      <ChildNonTheme />
      <ChildNonTheme />
      <ChildNonTheme />
      <ChildNonTheme />
      <ChildNonTheme />
      <ChildNonTheme />
      <ChildNonTheme />
    </ThemeApp>
  );
}

Все верно, разница только в том, что я вынул компонент, управляющий состоянием, и подкомпонент, отвечающий за отображение, и отрендерил его сразу после передачи через дочерние элементы. это сказатьThemeAppВнутри этого компонента больше не будетReact.createElementТакой код, то вsetThemeПосле запуска повторного рендерингаchildrenТам нет никаких изменений, поэтому его можно использовать повторно напрямую.

Давайте еще раз посмотрим наThemeAppзавернутый<ChildNonTheme />, он будет действовать какchildrenПерейти кThemeApp,ThemeAppВнутренние обновления вообще не вызывают внешниеReact.createElement, поэтому он будет напрямую повторно использовать предыдущийelementрезультат:

// 完全复用,props 也不会改变。
const childNonThemeElement = {
  type: ChildNonTheme,
  props: {}
}

После смены скина консоль пустая! Достигнута оптимизация.

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

Волшебные дети - адрес онлайн-отладки

Конечно, эту оптимизацию также можно выполнить, обернув подкомпоненты React.memo, но это относительно увеличит стоимость обслуживания, поэтому, пожалуйста, взвесьте варианты в соответствии со сценарием.

Контекстное разделение чтения и записи

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

import React, { useContext, useState } from "react";
import "./styles.css";

const LogContext = React.createContext();

function LogProvider({ children }) {
  const [logs, setLogs] = useState([]);
  const addLog = (log) => setLogs((prevLogs) => [...prevLogs, log]);
  return (
    <LogContext.Provider value={{ logs, addLog }}>
      {children}
    </LogContext.Provider>
  );
}

function Logger1() {
  const { addLog } = useContext(LogContext);
  console.log('Logger1 render')
  return (
    <>
      <p>一个能发日志的组件1</p>
      <button onClick={() => addLog("logger1")}>发日志</button>
    </>
  );
}

function Logger2() {
  const { addLog } = useContext(LogContext);
  console.log('Logger2 render')
  return (
    <>
      <p>一个能发日志的组件2</p>
      <button onClick={() => addLog("logger2")}>发日志</button>
    </>
  );
}

function LogsPanel() {
  const { logs } = useContext(LogContext);
  return logs.map((log, index) => <p key={index}>{log}</p>);
}

export default function App() {
  return (
    <LogProvider>
      {/* 写日志 */}
      <Logger1 />
      <Logger2 />
      {/* 读日志 */}
      <LogsPanel />
      </div>
    </LogProvider>
  );
}

Мы использовали советы по оптимизации из предыдущей главы и отдельноLogProviderИнкапсулируйте его и поднимите подкомпонент на внешний слой для входящего.

Сначала подумай о лучшем случае,LoggerКомпонент отвечает только за генерацию логов, ему все равноlogsизменений, в любом вызове компонентаaddLogПри записи в лог в идеале должно быть толькоLogsPanelЭтот компонент перерисовывается.

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

Это точно не то, что мы ожидали.Предположим, что в коде реальной сцены есть много компонентов, которые могут писать логи, и каждый раз запись будет вызывать повторный рендеринг глобальных компонентов? Это, конечно, неприемлемо, основная причина проблемыОфициальный сайт Контекстный разделСтало ясно:

когдаLogProviderсерединаaddLogвызывается дочерним компонентом, в результате чегоLogProviderПосле повторного рендеринга это неизбежно приведет к изменению значения, переданного провайдеру, поскольку значение содержитlogsиsetLogsсвойства, поэтому любое изменение любого из двух приведет к тому, что все подпискиLogProviderДочерние компоненты перерисовываются.

Итак, каково решение? На самом деле эторазделение чтения-записи, мы положилиlogs(читать иsetLogs(Запись) проходят через разных Провайдеров, так что изменился компонент, отвечающий за записьlogs, другие "пишущие компоненты" не перерисовываются, только действительно заботятся оlogs«Компонент чтения» будет повторно визуализирован.

function LogProvider({ children }) {
  const [logs, setLogs] = useState([]);
  const addLog = useCallback((log) => {
    setLogs((prevLogs) => [...prevLogs, log]);
  }, []);
  return (
    <LogDispatcherContext.Provider value={addLog}>
      <LogStateContext.Provider value={logs}>
        {children}
      </LogStateContext.Provider>
    </LogDispatcherContext.Provider>
  );
}

Мы только что упомянули, что необходимо гарантировать, что ссылка на значение не может измениться, поэтому естественно использовать ее здесь.useCallbackПучокaddLogМетод обернут, чтобы гарантировать, чтоLogProviderПри повторном рендеринге передаетсяLogDispatcherContextЗначение не меняется.

Теперь я отправляю логи с любого «компонента записи», и он разрешает только «компонент чтения».LogsPanelоказывать.

Разделение контекста на чтение и запись — онлайн-отладка

Организация контекстного кода

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

import React from 'react'
import { LogStateContext } from './context'

function App() {
  const logs = React.useContext(LogStateContext)
}

Но есть ли лучший способ организовать код? Например:

import React from 'react'
import { useLogState } from './context'

function App() {
  const logs = useLogState()
}
// context
import React from 'react'

const LogStateContext = React.createContext();

export function useLogState() {
  return React.useContext(LogStateContext)
}

Добавить какие-то гарантии надежности?

import React from 'react'

const LogStateContext = React.createContext();
const LogDispatcherContext = React.createContext();

export function useLogState() {
  const context = React.useContext(LogStateContext)
  if (context === undefined) {
    throw new Error('useLogState must be used within a LogStateProvider')
  }
  return context
}

export function useLogDispatcher() {
  const context = React.useContext(LogDispatcherContext)
  if (context === undefined) {
    throw new Error('useLogDispatcher must be used within a LogDispatcherContext')
  }
  return context
}

Если некоторым компонентам необходимо одновременно читать и записывать журналы, сложно ли вызывать дважды?

export function useLogs() {
  return [useLogState(), useLogDispatcher()]
}
export function App() {
  const [logs, addLogs] = useLogs()
  // ...
}

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

Объединение провайдеров

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

const StateProviders = ({ children }) => (
  <LogProvider>
    <UserProvider>
      <MenuProvider>
        <AppProvider>
          {children}
        </AppProvider>
      </MenuProvider>
    </UserProvider>
  </LogProvider>
)

function App() {
  return (
    <StateProviders>
      <Main />
    </StateProviders>
  )
}

Есть ли способ решить эту проблему? Конечно же, мы ссылаемся наreduxсерединаcomposeметод, напишите свойcomposeProviderметод:

function composeProviders(...providers) {
  return ({ children }) =>
    providers.reduce(
      (prev, Provider) => <Provider>{prev}</Provider>,
      children,
    )
}

Код можно упростить до этого:

const StateProviders = composeProviders(
  LogProvider,
  UserProvider,
  MenuProvider,
  AppProvider,
)

function App() {
  return (
    <StateProvider>
      <Main />
    </StateProvider>
  )
}

Суммировать

Эта статья в основном фокусируется на Context API и рассказывает о нескольких аспектах оптимизации производительности и оптимизации организации кода, которые резюмируются следующим образом:

  1. Попробуйте продвигать рендеринг нерелевантных дочерних элементов компонентов за пределами «компонентов с отслеживанием состояния».
  2. Разделение контекста для чтения и записи, если это необходимо.
  3. Оберните использование Context, обратите внимание на обработку ошибок.
  4. Объединяйте несколько контекстов для оптимизации кода.

Добро пожаловать в "Передняя часть от продвинутого до допуска», и есть много оригинальных статей о внешнем интерфейсе~