Главная » Статьи » Путь к увеличению производительности приложения на React в 10 раз

Путь к увеличению производительности приложения на React в 10 раз

Путь к увеличению производительности приложения на React в 10 раз

От автора: когда-либо получали ошибку «Aw! Snap» в вашем приложении? Пытались это решить? Вы просто погуглили и не нашли ничего, кроме статьи о ошибках chrome? Как правило, простое обновление страницы поможет вашему приложению снова запускаться.

Путь к увеличению производительности приложения на React в 10 раз

Год назад в Technogise у меня появилась возможность поработать над тяжелым фронт-энд приложением ReactJS с множеством функций. Мы унаследовали эту кодовую базу, внесли в нее серьезные улучшения и начали добавлять в приложение более интересные функции.

Однако мы часто получали жалобы от наших QA и конечных пользователей, что они видят вышеуказанную ошибку. После анализа мы обнаружили, что это происходило из-за того, что приложение занимало 1,5 ГБ памяти.

В этой статье я расскажу о нашей практике улучшения объема памяти, используемого этим приложением, с ~ 1,5 ГБ до ~ 150 МБ, тем самым повысив производительность приложения в 10 раз и избавившись от лишних ошибок Aw Snap!!!

Поиск узких мест в производительности

Существует множество инструментов и библиотек, позволяющих выявить узкие места в вашем приложении. Мы перепробовали множество из них, и для нас были очень полезны следующие три:

1. Профилирование компонентов с помощью расширения браузера Chrome

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

Путь к увеличению производительности приложения на React в 10 раз

2. Снимки памяти из Firefox

Этот инструмент дает нам снимок хипа памяти текущей вкладки. Его древовидная карта обеспечивает визуальное представление следующих вещей:

Объекты: объекты JavaScript и DOM, такие как функция, объект или массив, и типы DOM, такие как Window и HTMLDivElement.

Скрипты: источники JavaScript, загружаемые страницей.

Строки.

Другое: сюда входят внутренние объекты SpiderMonkey.

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

Путь к увеличению производительности приложения на React в 10 раз

3. Используя пакет why-did-you-render

Это npm пакет от Welldone Software, который уведомляет вас о повторных рендерингах, которых можно избежать . Его легко настроить, и он дает подробную информацию о том, почему и когда определенный компонент выполняет повторный рендеринг. Он помог нам определить улучшения.

Путь к увеличению производительности приложения на React в 10 раз

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

Итак, как мы улучшили производительность?

Мы узнали, что все это происходит из-за ненужного повторного рендеринга компонентов. Компонент повторно отрисовывается 50 раз, просто обновляя страницу и не взаимодействуя с ней!

Итак, мы обсудили и придумали стратегию, чтобы делать маленькие шаги, улучшать что-то одно за раз. Мы посмотрели на самые маленькие из компонентов и спросили себя: если данные не меняются, то почему этот компонент повторно отрисовывает себя? Мы начали изучать закономерности и пришли к следующим решениям:

1. Удалите все встроенные функции.

Встроенная функция — это функция, которая определяется и передается внутри метода рендеринга компонента React. Пример встроенной функции:

import React, { Component } from 'react'; class Parent extends Component { render() { return ( <Child onClick={() => console.log('You clicked!')} /> ); }
}

Наш код был наполнен встроенными функциями. У встроенных функций есть 2 большие проблемы:

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

Они увеличивают объем памяти, занимаемой приложением. (См .: Функциональные пространства в снимке памяти Firefox)

Это в первую очередь связано с тем, что передача метода всегда осуществляется «по ссылке », поэтому он создает новую функцию и меняет ее ссылку для каждого цикла визуализации. Это происходит, даже если вы использовали PureComponent или React.memo().

Решение: переместить все встроенные функции за пределы render(), чтобы они не переопределялись в каждом цикле отрисовки. Это уменьшило объем памяти с 1,5 ГБ до 800 МБ.

Улучшенная встроенная функция:

import React, { Component } from 'react'; class Parent extends Component { onClick = () => console.log('You clicked!'); render() { return ( <Child onClick={this.onClick} /> ); }
}

2. Избегайте изменения состояния, если в вашем хранилище Redux нет изменений в состоянии.

Обычно мы храним ответ API в хранилище Redux. Предположим, мы снова вызываем этот API и получаем те же данные, стоит ли обновлять хранилище Redux? Короткий ответ — нет. Если вы обновите данные, компоненты, которые их используют, будут отображены снова, поскольку ссылка на эти данные будет изменена.

Поскольку наш код был унаследован, в компоненты были добавлены средства для решения этой проблемы. Хак был в следующей строке: «JSON.stringify (prevProps.data)! == JSON.stringify (this.props.data)». Мы получили эту подсказку через Firefox Memory Snapshot, поскольку строка занимала больше памяти на нескольких страницах.

Решение: используйте эффективное сравнение перед обновлением состояния или хранилища Redux. Мы нашли 2 хороших пакета — deep-equal и fast-deep-equal. Выбор за вами! Это уменьшило объем памяти с 800 МБ до 500 МБ.

3. Условный рендеринг компонентов

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

Распространенный способ написания модальных окон:

import React, { Component } from 'react'; class Parent extends Component { render() { return ( <Modal show={isOpen}> <Modal.Header> Hello </Modal.Header> <Modal.Body> I am a Modal </Modal.Body> </Modal> );
}

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

Решение: избегать рендеринга этих компонентов до тех пор, пока они не понадобятся, то есть использовали условный рендеринг. Это уменьшило объем памяти с 500 МБ до 150 МБ.

Улучшение приведенного выше примера, показано ниже:

import React, { Component } from 'react'; class Parent extends Component { render() { if(!isOpen) { return null; } return ( <Modal show={isOpen}> <Modal.Header> Hello </Modal.Header> <Modal.Body> I am a Modal </Modal.Body> </Modal> );
}

4. Удалите ненужные ожидания и используйте Promise.all() везде, где это возможно.

Исходя из бекграунда Java, Python, Golang (или любого языка с последовательным выполнением), мы используем await во многих местах. Однако это может иметь огромное влияние на производительность при выполнении любых длительных операций, таких как вызов API для сохранения или обновления данных. Обычно мы вызываем API при загрузке приложения для получения исходных данных. Представьте, что вашему веб-приложению требуется большой объем данных от 3–5 вызовов API, как в примере ниже. Методы get в приведенных ниже примерах связаны с вызовом API.

const userSubscription = getUserSubscription();
const userDetails = getUserDetails();
const userNotifications = getUserNotifications();

Решение. Мы определили, что большинство вызовов API можно выполнять параллельно. Итак, мы использовали Promise.all(), чтобы помочь нам отправлять все эти вызовы API параллельно. Это уменьшило время загрузки начальной и других страниц на 30%.

const [ userSubscription, userDetails, userNotifications
] = await Promise.all([ getUserSubscription(), getUserDetails(), getUserNotifications()
]);

В проекте ReactJS можно сделать ещё больше улучшений. Я перечислю их в другой статье.

Заключение

Чтобы повысить производительность приложения React:

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

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

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

Всегда вызывайте несколько API параллельно. Последовательные вызовы влияют на время загрузки.

И после трех недель усилий (включая тестирование) мы, наконец, развернули наш код в производстве. До сих пор у нас нет «Aw! Snap». Приложение стало намного шустрее…

Автор: Mayank Sharma

Источник: medium.com

Редакция: Команда webformyself.

Читайте нас в Telegram, VK, Яндекс.Дзен