Насколько медленны React Hooks?

React.js

С момента рождения Hooks официальные лица рассматривали вопросы производительности. Добавлены различные методы оптимизации производительности, такие как memo, хуки, deps, ленивая инициализация и т. д. И в официальном FAQ также упоминается, что компонент Function каждый раз очень быстро создает функцию замыкания, а с оптимизацией движка в будущем это время будет еще больше сокращено, поэтому нам не нужно беспокоиться о функции замыкания здесь.

Конечно, это подтверждают и мои эксперименты, он не медленный, не только закрытие функций не медленное, но даже большое количество вызовов хуков очень быстрое. Проще говоря, за 1 миллисекунду можно запустить около тысячи хуков.useState useEffectвызов. А создание функций еще больше, если быстро, в 100 000 раз.

Многие люди думают, что раз официальный представитель так сказал, то мы можем использовать его вот так, и нам не нужно слишком беспокоиться о проблемах с производительностью. Я тоже так сначала подумал. Но до недавнего времени я пытался переписать более сложный компонент в проекте компании с помощью Hooks, и с удивлением обнаружил, что время перерендеринга увеличилось с 2 мс до 4 мс. В бизнес-логике нет изменений, единственное изменение — от класса к хукам. В это мне немного трудно поверить. Я всегда чувствую, что даже если он медленнее, он не более чем в два раза медленнее. Как они могут быть похожи. Поэтому я начал сравнивать различия в производительности между двумя методами письма без разбора.

Руководство по ленивому чтению

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

почему эта статья

На самом деле, мне не очень хотелось писать статью, потому что я думаю, что это просто очень простое сравнение. Так что я просто сделал несколько случайных замечаний по поводу точки кипения Наггетсов, и в результате... решил написать статью. Основная причина в том, что я думаю, что эта группа людей хороша.Даже если вы задаете вопросы, вы должны сначала подвергнуть сомнению мой метод измерения, а не мой метод использования. Его используют уже столько лет, и я до сих пор могу использовать его неправильно) Забавная мордашка

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

Теперь, когда я это сказал, есть одна вещь, которую я должен сказать: 50% данных измерений, упомянутых в температуре кипения, действительно являются проблемой. Причин несколько: во-первых, я просто хотел попробовать, поэтому запустил прямо в режиме разработки. Во-вторых, если вы привыкли писать код, вы можете использовать его напрямую.Date.now()без использования более высокой точностиperformance.now()В результате погрешность несколько больше. Хотя ошибка немного больше, общее направление по-прежнему правильное.

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

Прежде чем мы начнем, нам нужно знать...

Допустим, сейчас естьHookCompа такжеClassCompЭти два компонента представляют собой функциональные компоненты и компоненты класса соответственно, которые позже будут заменены Hook(HC) и Class(CC).

определение функции

Чтобы быть ближе к реальности, здесь предполагается, что оба компонента должны выполнять одну и ту же функцию. То естьЛогин пользователяЭтот процесс:

  • Поле ввода имени пользователя и поле ввода пароля
  • Есть кнопка входа в систему, после ее нажатия проверьте правильно ли введено имя пользователяadminи парольadmin
  • Если проверка прошла успешно, следующее подсказывает, что вход в систему выполнен успешно, в противном случае имя пользователя или пароль неверны.
  • Каждый раз, когда вы что-то вводите, содержимое будет очищаться
  • Кроме того, для устранения ошибок добавлена ​​дополнительная кнопка для запуска 100 рендеров и регистрации среднего времени рендеринга.

DEMO

Для реализации конкретной бизнес-логики см. адрес DEMO ниже.

Кроме того, поскольку компонент класса имеет setState, он может автоматически реализовывать пакетные обновления, а хуки - нет. Поэтому при реализации здесь все операции обновления помещаются в события React для синхронного обновления. Как мы все знаем, события React поставляются с пакетными обновлениями. , чтобы гарантировать, что только один рендер. Убедитесь, что эти две функции одинаковы.

Контрастная константа

  • 15-дюймовый MacBook Pro 2018 года начального уровня, i7-8750H, 6 ядер, 12 потоков, 16 г и 256 г
  • Chrome Stable 79.0.3945.117
  • react 16.12.0PS: Вообще-то я из16.8.0Я начал тест, и приступ ленивого рака не продолжился.
  • react-dom 16.12.0

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

Метрика: время от вызова функции до рендеринга в DOM.

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

Для ХК

  • Время запуска записывается, как только компонент запускается

  • использоватьuseLayoutEffectОбратный вызов как время окончания. Этот хук будет вызываться синхронно после того, как компонент смонтирует или обновит DOM. а такжеuseEffectОн будет вызван в следующем тике, если использовать этот хук, то окончательный результат измерения, как правило, будет медленнее.

    function Hooks() {
    	const now = performance.now()
    	useLayoutEffect(() => console.log(performance.now() - now))
    	return (/* ui */)
    }
    

Для CC

  • При запуске метода рендеринга запишите время
  • при бегеcomponentDidUpdateилиcomponentDidMount, печать требует времени. Оба этих хука вызываются синхронно после того, как компонент монтирует или обновляет DOM.useLayoutEffectВремя звонка такое же.
class Class extends Component {
	componentDidMount = () => this.log()
	componentDidUpdate = () => this.log()
	log = () => console.log(performance.now() - this.now)
	render() {
		this.now = performance.now()
		return (/* ui */)
	}
}

Ход теста и расчет результатов

  • Страница обновляется. В это время для тестового контента необходимо выполнить 5 раундов прогревочных тестов. Цель состоит в том, чтобы позволить Chrome оптимизировать код точки доступа для достижения максимальной производительности.
  • Каждый раунд содержит несколько рендеров, например 100 или 50. Для каждого раунда тестирования будут отброшены 5% самых высоких и 10% самых низких данных, будут сохранены только промежуточные значения, а среднее из этих значения будут рассчитаны для получения результатов раундового теста
  • Затем проведите 5 раундов обычных тестов, каждый раз записывайте результаты и подсчитывайте среднее значение.
  • Рассчитайте значение в это время как окончательное значение данных

ДЕМО-адрес

PS: CodeSandBox, похоже, не работает в рабочем режиме, но вы можете развернуть его в ZEIT или netlify одним щелчком мыши, чтобы увидеть эффект в производственной среде.

Appetizer — повторный рендеринг результатов теста

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

Avg. Time(ms) Hook Hook(Self) Class Class(Self) Self Hook Slow
первое среднее время 0.2546703217776267 0.04549450906259673 0.20939560484263922 0.02357143663115554 93.0069421499% 21.6216175927%
второе среднее время 0.23439560331158585 0.045824176785382593 0.2072527365001676 0.02346153545019391 95.3161884168% 13.0965058748%
третье среднее время 0.22417582970644748 0.04109888910674132 0.1931868181410399 0.022967028748858104 78.9473490722% 16.0409555184%
Четвертое среднее время 0.22082417118516598 0.04082417709159327 0.18879122377096952 0.02120880942259516 92.4868873031% 16.96739222%
пятое среднее время 0.22747252228577713 0.04126375367107627 0.1941208809532307 0.024725271102327567 66.8889837458% 17.1808623414%
Среднее время пять 0.23231 0.0429 0.19855 0.02319 85.329% 16.981%

Кратко объясните данные, Hook и Class — это данные, рассчитанные указанным выше методом, а Hook(Self) Class(Self) — для расчета времени вызовов функций HC и CC, а последние Self и Hook Slow сравниваются с Hook Процент медленного выполнения класса. Здесь вам нужно только сосредоточиться на данных без Self.

Присмотримся повнимательнее: Hook на 16% медленнее, чем Class.

так далее? ? ? 16%, эммм... какая удивительная цифра на первый взгляд, падение производительности на 5% трудно проглотить, не говоря уже о 16%. Если у вас на странице сотни таких компонентов, подумайте об этом... Эй~~~ Это кисло

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

Давайте сначала ответим на вопрос об измерении.Как упоминалось выше, useLayoutEffect и CDU/CDM в основном одинаковы, и для доказательства, здесь непосредственно отображаются данные панели Performance.Хотя эту часть данных можно увидеть только в разработке режим, но все еще есть ссылка

Hooks

Class

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

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

  • Как производительность маунта? То есть при первом рендеринге компонента
  • Как насчет производительности рендеринга больших списков? Может быть слишком много компонентов для рендеринга, и производительность не будет линейно накладываться?
  • А как насчет того, когда Class обернут многими HOC?

Другие сравнения

Производительность монтирования

Среднее время рассчитывается путем быстрой выгрузки и монтирования 40 раз, и они располагаются горизонтально, чтобы уменьшить разницу в Chrome Layout&Paint каждый раз, когда они монтируются и выгружаются. Не говорите много, сразу переходите к результатам

Avg. Time(ms) Hook Hook(Self) Class Class(Self) Hook Slow(%)
первое среднее время 0.5608108209295047 0.04027024968653112 0.5409459180727199 0.025810805980015446 3.6722530281%
второе среднее время 0.6013513618224376 0.041216209128096294 0.5285134916571347 0.02486483395301007 13.7816482105%
третье среднее время 0.5672973001728187 0.04797298587053209 0.5154054158845464 0.024729729252489837 10.0681682204%
Четвертое среднее время 0.5343243404216057 0.04378377736822979 0.5293243023491389 0.025405410073093465 0.9446076914%
пятое среднее время 0.5371621495263802 0.041081066671255474 0.5078378347428264 0.025540552529934292 5.774346214%
Среднее время пять 0.56019 0.04286 0.52441 0.02527 6.848%

Приведенная выше таблица получена путем чередования 5 последовательных запусков 40 тестов. Можно обнаружить, что независимо от того, какой запуск, время класса будет меньше, чем время крючка. Из расчета видно, что Hook в среднем медленнее Class (0,53346 - 0,49811)/0,49811 = 7%, а абсолютная разница составляет 0,03535 мс.

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

Производительность большого списка

Среднее время рассчитывается путем рендеринга данных для 100 списков.

Avg. Time(ms) Hook Hook(500) Class Class(500) Hook Slow(%) Hook Slow(%,500)
первое среднее время 2.5251063647026077 9.55063829376818 2.335000020313136 8.795957447604296 8.1415992606% 8.5798601307%
второе среднее время 2.6090425597701934 9.59723405143682 2.3622340473168073 8.702127664211266 10.4480973312% 10.286063613%
третье среднее время 2.5888297637488615 9.64329787530005 2.344893603684737 8.731808533218313 10.4028668798% 10.438723417%
Четвертое среднее время 2.567340426662184 9.604468084673615 2.334893631570517 8.76340427574642 9.95534837% 9.5974553092%
пятое среднее время 2.571702087694343 9.597553207756992 2.230957413012994 8.719042523149797 15.273472846% 10.075770158%
Среднее время пять 2.5724 9.59864 2.3216 8.74247 10.844% 9.796%

Не будем подсчитывать, насколько это медленнее, давайте сначала посмотрим на это значение, 100 рендеров занимают в общей сложности более 2 мс, в среднем 0,02 мс, и когда мы тестировали выше, мы обнаружили, что в среднем требуется 0,2 мс. для рендеринга компонента в одиночку.Разница немного огромна.

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

Другими словами, мы не знаем, сколько времени React Core занимает в середине, но мы знаем, что это определенно много.

HOC

Рождение Hook на самом деле должно уменьшить повторное использование логики, Короче говоря, это должно упростить путь HOC, так что HOC на самом деле HOC. Самый простой пример, инжект Mobx, нужно обернуть инжектом компонентов более высокого порядка, но для Hook это совершенно не нужно.

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

Какой? Вы говорите, что невозможно установить 10 слоев? На самом деле, это также очень просто.Вы должны обратить внимание на тот факт, что 10 слоев, о которых мы здесь говорим, на самом деле означают, что есть 10 слоев компонентов, которые обертывают окончательные используемые компоненты. Например, все знают, что метод mobx inject или метод подключения redux вроде бы завернуты только в один слой, но на самом деле это два слоя, потому что есть еще один слойContext.Consumer. Точно так же, если вы считаете HOC of History, вам также придется прийти к двум уровням. Добавьте еще кое-что, и немного преувеличений мало, мануал смешной)

Avg. Time(ms) Class With 10 HOC
первый раунд 0.2710439444898249
второй раунд 0.2821977993289193
третий раунд 0.278846147951189
четвертый раунд 0.27269232207602195
пятый раунд 0.25384614182697546
Среднее время за пять раундов 0.27173

Этот результат также очень ясен: когда много вложенных HOC, производительность класса на самом деле не очень хорошая, она увеличивается с 0,19855 мс до 0,27173 мс, а время приближается к увеличению на 26%. И эта низкая производительность не из-за класса, а из-за слишком большого количества отображаемых компонентов. С другой точки зрения, у хуков нет этой неприятности, и производительность даже большого количества вызовов хуков все еще находится в приемлемом диапазоне.

Количественные развлечения?

С приведенными выше данными давайте проделаем интересную вещь и оценим данные количественно.

Предположим, что существуют следующие константы,rПредставляет ядро ​​React и другие константы, не зависящие от количества компонентов,hконстанта, представляющая компонент крюка, в то время какcконстанта, представляющая компонент класса,TУказывает окончательное прошедшее время. Известно, что эти четыре параметра определенно не являются отрицательными.

С помощью простых предположений можно получить следующее уравнение:

T(n,m) = hn + cm + r
// n 表示 hook 组件的数量
// m 表示 class 组件的数量

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

h = 0.0184907294
c = 0.01674766395
r = 0.4146159332
RSS = 0.249625719
R^2 = 0.9971412136

Результат этой подгонки немного неудовлетворителен, потому что, если вы подставите результаты одного класса или крючка, вы обнаружите, что отклонение более чем удвоилось. Так я же выше сказал, что это просто для развлечения, и времени на изучение причин не хватает. Однако по результатам подгонки также можно обнаружить такое явление, как h больше, чем c.

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

Суммировать

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

  • При использовании Hook общая производительность будет снижена на 10-20% по сравнению с компонентом Class.
  • Нет снижения производительности при использовании только функциональных компонентов без хуков. Это означает, что вы можете безопасно использовать чисто функциональные компоненты.
  • Снижение производительности Hook не только отражается в процессе рендеринга, даже в процессе первого монтирования, но и в определенной степени снижается по сравнению с Class
  • Снижение производительности хука состоит из трех частей
    • Первая часть — это вызов хука, напримерuseStateЭти. Но здесь следует отметить, что призыв здесь относится к присутствию, а не к количеству. Проще говоря, от 0 до 1 деградация производительности намного выше, чем от 1 до n.
    • Вторая часть заключается в том, что из-за введения хука нам приходится создавать большое количество замыканий функций, временных объектов и т. д.
    • Третья часть — это дополнительное потребление, вызванное обработкой хуков React, например, управление связанным списком хуков, обработка зависимостей и так далее. По мере увеличения хуков увеличивается и время, которое занимает этот маргинальный контент.
  • Но у хука есть сильная сторона — с точки зрения повторного использования логики он намного выше, чем у метода HOC, что можно расценивать как выигрыш.

Так что Крюк действительно медленный, и медлительность оправдана. Но решать, использовать хуки или нет, я не принимаю. У всего есть две стороны, хуки решают некоторые недостатки Class, но и привносят некоторые недостатки. Если мне нужно что-то порекомендовать, я рекомендую Hooks+Mobx.

Refs

One More

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

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