Как оптимизировать React-приложение с помощью крючков и AG Grid

Эту заметку в блог AG Grid прислал Кэмерон Пэйви.

React описывает себя как «библиотеку JavaScript для создания пользовательских интерфейсов». Ее внутренняя работа довольно сложна, но по сути это две основные части: сам React и рендерер React, который является react-dom в случае веб-браузеров.

Основная библиотека React отвечает за получение вашего кода и преобразование его в структуру, которую рендерер React, такой как react-dom, может затем использовать для согласования желаемого состояния с текущим состоянием и внесения необходимых изменений для сближения этих двух состояний. То, как вы пишете свой код, может сильно повлиять на величину этих изменений. Нередко при согласовании объектной модели документа (DOM) React вносит больше изменений, чем это необходимо. Эти изменения, или «рендеры», обычно можно уменьшить, оптимизировав код различными способами. Такая оптимизация в целом желательна, но еще более желательна при работе с большими объемами данных или большим количеством узлов DOM. Хотя неоптимизированный код может не вызывать проблем в небольших объемах, при больших масштабах он может быстро повлиять на пользовательский опыт.

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

AG Grid, высокопроизводительная сетка данных, играет большую роль в этом руководстве. Она хорошо работает с большинством ведущих фронтенд-библиотек, включая React, и представляет собой хороший пример для оптимизации, обсуждаемой здесь, по двум причинам. Во-первых, AG Grid сама оптимизирована внутри, и поэтому влияние плохо оптимизированного кода вокруг нее более очевидно. Во-вторых, AG Grid способен работать с огромными объемами данных, которые при неправильном обращении часто оказывают негативное влияние на производительность, но это также отличный способ проверить эффективность предполагаемых оптимизаций кода в масштабе.

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

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

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

  • последняя версия Node.js и npm
  • редактор кода (VS Code — хороший выбор, если у вас нет существующего предпочтения).

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

Чтобы установить базовый уровень для этих оптимизаций, необходимо посмотреть, как работает текущее неоптимизированное приложение. Есть несколько различных способов сделать это. Самый распространенный способ — использовать расширение браузера React Developer Tools, которое включает в себя специально разработанный профилировщик. Этот профилировщик позволяет увидеть подробную информацию о вашем приложении, например, какие компоненты были перерендерированы в данном коммите, почему они были перерендерированы и сколько времени на это ушло. Если вы хотите следовать этому руководству, добавьте это расширение в свой браузер.

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

npm install
npm run start

Войти в полноэкранный режим Выйти из полноэкранного режима

Эта команда устанавливает зависимости кода и запускает сервер разработки. После запуска сервер разработки укажет, на каком порту он работает (обычно порт 3000), и откроет демонстрационное приложение в вашем браузере по умолчанию.

Когда страница загрузится, вы должны увидеть что-то вроде этого:

Откройте инструменты разработчика, щелкнув правой кнопкой мыши где-нибудь на странице и выбрав Inspect. По умолчанию откроется вкладка Elements. Вы можете найти профилировщик React DevTools, выбрав вкладку Profiler. Возможно, вам придется нажать на значок стрелки в конце вкладки, чтобы увидеть ее:

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

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

  • Запустите запись профилировщика
  • Для каждой из первых четырех ячеек в столбце Имя_Первое щелкните по ячейке один раз.
  • Затем четыре раза нажмите кнопку Изменить столбцы.
  • Остановите запись профилировщика

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

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

Этот график отображает коммиты, сделанные React, и их относительную продолжительность. Чем больше полоса, тем дольше длился коммит. В данном сценарии коммиты не занимают много времени (самый большой коммит занимает всего около 12 мс). Однако принципы, использованные здесь, применимы и к большим приложениям React, которые могут быть затронуты более серьезными проблемами производительности, с рендерингом, занимающим от 100 мс до целых секунд.

По мере того, как вы просматриваете коммиты, различные части графика пламени будут подсвечиваться, представляя собой компоненты с повторным рендерингом в данном коммите. Глядя на предыдущий снимок экрана, вы можете увидеть, что в выделенном коммите (и в других подобных всплесках на графике) все строки были перерендерированы. Это приводит к самым медленным коммитам из тех, которые зафиксировал профилировщик. Если вы вернетесь назад и посмотрите на первые несколько фиксаций, то увидите, что каждый раз, когда вы щелкали на одной из ячеек, это приводило к повторному отображению двух ячеек — той, на которой вы щелкнули, и той, на которой вы уже были сосредоточены:

Это может привести к неэффективности, если эти ячейки используют дорогие, плохо оптимизированные пользовательские рендереры ячеек. В столбце First_name используется пользовательский рендерер ячеек для отображения счетчика в круглых скобках. Этот счетчик увеличивается на единицу при каждом повторном рендеринге компонента. Это довольно недорогая операция, но вы можете увидеть, как часто она выполняется, щелкнув по этим ячейкам. Если бы это была более дорогая операция, она могла бы оказать значительное влияние. Аналогично, каждый раз, когда вы нажимаете кнопку Change Columns, параметр columnDefs компонента AG Grid обновляется на аналогичное (хотя и не идентичное) значение. Как побочный эффект этого, объект, определяющий окраску столбца, каждый раз создается заново со случайным цветом:

Оптимизация сетки

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

Memoized Components

Если вы еще не сделали этого, откройте клонированную кодовую базу в выбранном вами редакторе. Первая оптимизация, на которую следует обратить внимание, связана с пользовательскими рендерами ячеек. Счетчик, включенный в этот компонент, увеличивается при каждом повторном рендеринге, но все эти повторные рендеринги проходят впустую, поскольку содержимое ячейки не меняется. Вы можете решить эту проблему с помощью компонента высшего порядка React.memo (HOC), который оборачивает ваши компоненты и возвращает ранее вычисленное значение, если ни один из входов не изменился.

Начните с открытия файла по адресу src/components/name-formatter.jsx, который в настоящее время является обычным функциональным компонентом. Чтобы он перестал без необходимости повторно вычислять свой вывод, достаточно обернуть его в HOC следующим образом:

import * as React from 'react';

const NameFormatter = React.memo(({ value }) => {
  const renderCountRef = React.useRef(1);
  return (
    <strong>
    {`(${renderCountRef.current++}) ${value}`}
    </strong>
  );
});

export default NameFormatter;

Войти в полноэкранный режим Выйти из полноэкранного режима

Обратите внимание: не рекомендуется просто обертывать все ваши компоненты этим HOC. Есть время и место для этого, но обычно, если у вас есть компонент, который рендерится часто и рендерится детерминированно, то есть для заданного набора входных реквизитов, он всегда будет выдавать один и тот же результат. Обернув его React.memo, вы сможете избавиться от лишних рендеров.

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

Кэширование дорогих значений

Возможно, вы заметили, что при взаимодействии с неоптимизированной сеткой Grid и нажатии кнопки Change Columns строки в таблице меняются. Эти данные генерируются случайным образом с помощью библиотеки @faker-js/faker. В реальной жизни эти данные, скорее всего, поступают из конечной точки API. Для простоты этот генератор данных используется вместо реального API. Однако принцип, лежащий в основе этой оптимизации, остается неизменным.

В этом случае значение, созданное генератором поддельных данных, не сохраняется при повторном рендеринге компонента Grid. Каждый раз при изменении реквизитов ввода все данные генерируются заново. Если бы это был вызов API, то, скорее всего, при каждом изменении реквизитов выполнялись бы сетевые запросы. Такое поведение не является оптимальным из-за его влияния на производительность и, в большинстве случаев, из-за нерационального использования ресурсов. Обычно лучше кэшировать это значение и повторно использовать его между рендерами. В некоторых случаях может потребоваться регенерация или повторная выборка данных, но это должно быть сделано намеренно, а не как побочный эффект плохо оптимизированного кода.

Существует несколько различных крючков React, которые можно использовать для кэширования данных в зависимости от сценария. Для справки, в текущей неоптимизированной реализации в src/components/grid.jsx функция генератора данных вызывается без каких-либо хуков, поэтому она будет вызываться при каждом рендере:

// Unoptimized
function Grid({ columnDefs, defaultColDef }) {
  // This will be called on each render  
  const data = getData(10);

  return (
    <div className="ag-theme-alpine" style={{ height: '98vh' }}>
    <AgGridReact
        maintainColumnOrder
        defaultColDef={defaultColDef}
        rowData={data}
        columnDefs={columnDefs}
    />
    </div>
  );
}

Вход в полноэкранный режим Выход из полноэкранного режима

Если ваши данные поступают из синхронной функции, как в данном случае, вы можете обернуть значение хуком React.useMemo. Он ведет себя аналогично вышеупомянутому React.memo, но вместо того, чтобы быть HOC, это хук, который вы можете применить к значениям, отличным от компонентов, и его подход выглядит следующим образом:

function Grid({ columnDefs, defaultColDef }) {
  // This value will now persist between renders
  const data = React.useMemo(() => getData(10), []);

  return (
    <div className="ag-theme-alpine" style={{ height: '98vh' }}>
    <AgGridReact
        maintainColumnOrder
        defaultColDef={defaultColDef}
        rowData={data}
        columnDefs={columnDefs}
    />
    </div>
  );
}

Войти в полноэкранный режим Выход из полноэкранного режима

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

function Grid({ columnDefs, defaultColDef }) {
  const [data, setData] = React.useState([]);

  // This effect will be invoked the first time the component renders
  React.useEffect(() => {
    (async () => {
            // This value will be persisted between renders
    setData(getData(10));
    })();
  }, []);

  return (
    <div className="ag-theme-alpine" style={{ height: '98vh' }}>
    <AgGridReact
        maintainColumnOrder
        defaultColDef={defaultColDef}
        rowData={data}
        columnDefs={columnDefs}
    />
    </div>
  );
}

Вход в полноэкранный режим Выйти из полноэкранного режима

Обратите внимание, что хотя getData не является асинхронной функцией, этот паттерн работает здесь и может быть использован с асинхронными вызовами API. Решение о том, какой паттерн лучше использовать, зависит от специфики вашего приложения; однако между React.useState, React.useEffect и React.useMemo вы сможете найти решение для большинства сценариев. Если вы еще не сделали этого, неплохо было бы ознакомиться с React Hooks, так как они имеют некоторые нюансы поведения, но в целом являются довольно мощными.

После применения одной из этих оптимизаций (подход useMemo или подход useEffect), вы обнаружите, что строки больше не меняются при нажатии кнопки Change Columns. Теперь данные сохраняются между рендерами:

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

Извлечение статических значений

Если вы посмотрите на src/app.jsx, то увидите следующий блок кода:

  const updateColumns = () => {
    setColumnDefs([
    { field: 'id', cellStyle: { background: randomColor() } },
    { field: 'first_name', cellRenderer: NameFormatter },
    { field: 'last_name' },
    { field: 'email' },
    { field: 'gender' },
    { field: 'ip_address' },
    ]);
  };

Вход в полноэкранный режим Выход из полноэкранного режима

Первый объект в этом массиве имеет свойство cellStyle. Это свойство содержит объект стиля CSS, который будет применен ко всем ячейкам в этом столбце. В данном случае значение этого свойства динамически вычисляется каждый раз при вызове функции updateColumns, поэтому цвет столбца меняется каждый раз, когда вы нажимаете кнопку Change Columns. Это надуманный пример, демонстрирующий, что передача объектов по значению таким образом приводит к тому, что каждый раз создается новый экземпляр объекта, и это нежелательное поведение. Даже если определения столбцов изменятся, нет необходимости пересчитывать все значения для всех их свойств. Вы можете устранить поведение столбца, изменяющего цвет, выполнив следующие оптимизации:

// 1. Extract the value of the cellStyle property to outside of the App component
const cellStyle = { background: randomColor() };

function App() {
…
// 2. Update the updateColumns function to use this extracted value
  const updateColumns = () => {
    setColumnDefs([
    { field: 'id', cellStyle },
    { field: 'first_name', cellRenderer: NameFormatter },
    { field: 'last_name' },
    { field: 'email' },
    { field: 'gender' },
    { field: 'ip_address' },
    ]);
  };

Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь при каждом вызове updateColumns, хотя свойство columnDefs все еще будет меняться, объект стиля, примененный к столбцу Id, останется неизменным, что устранит случайные изменения цвета. Следует отметить, что после первого нажатия кнопки Change Columns цвет все равно изменится, так как начальное значение, переданное хуку useState, не имеет cellStyle, заданного для этого столбца.

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

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

  • Запустите запись профилировщика
  • Для каждой из первых четырех ячеек в столбце First_name щелкните по ячейке один раз.
  • Затем четыре раза нажмите кнопку Изменить столбцы.
  • Остановите запись профайлера

После остановки профилировщика вы должны увидеть что-то вроде этого:

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

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

Увеличение объема данных

В реальном приложении вы, скорее всего, будете иметь дело с гораздо большими объемами данных, чем десять строк в этом демонстрационном приложении. Чтобы убедиться, что эти оптимизации выдерживают нагрузку, вы можете легко настроить вызов генератора случайных данных, находящийся в src/components/grid.jsx, чтобы генерировать 100 000 строк данных или больше. Для этого настройте блок useEffect следующим образом:

  React.useEffect(() => {
    (async () => {
    setData(getData(100000));
    })();
  }, []);

Войти в полноэкранный режим Выйти из полноэкранного режима

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

Источником проблем с производительностью для многих браузеров является DOM. Когда в DOM слишком много узлов (например, 100 000 строк таблицы), производительность легко может пострадать, если эти узлы обладают какой-либо сложностью, выходящей за рамки простых текстовых контейнеров. Одним из наиболее распространенных способов решения этой проблемы является виртуализация DOM, при которой отображаются только видимые элементы. По мере того, как пользователь прокручивает страницу, React будет отображать новые элементы по мере их появления, а старые элементы будут удаляться, как только они перестанут быть видимыми. Вы можете увидеть это на практике, используя React DevTools.

Помимо профилировщика, есть также вкладка Components, к которой вы можете получить доступ. Эта вкладка покажет вам все компоненты React, отображаемые на странице, и подробную информацию о них. Если вы перейдете к этому представлению и прокрутите вниз Grid, вы заметите, что количество компонентов строк не сильно увеличивается или уменьшается (есть небольшие колебания, когда строки видны наполовину), но сами строки меняются. Это и есть виртуализация в действии:

Заключение

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

Помимо виртуализации DOM, AG Grid применяет множество внутренних оптимизаций, чтобы гарантировать отсутствие ненужных рендеров в самой библиотеке. Оптимизированная производительность — это лишь одно из преимуществ. AG Grid также имеет множество мощных функций, от обработки огромных объемов данных до потокового обновления данных и интегрированных графиков. Если вы ищете надежное решение «все в одном» для работы с сетками данных, и производительность является обязательным условием, попробуйте AG Grid.

Все примеры кода в этой статье можно найти в этом репозитории GitHub.

Оцените статью
devanswers.ru
Добавить комментарий