Комментарии 23
У меня опыт такой:
— все что влезает на экран, даже весьма сложные UI — тормозить не будет, даже если перерисовывать вообще все на каждое действие.
— если все влезает в 2-3 экрана — это оптимизируется одним-двумя shouldComponentUpdate-ами, где-то в серединке — чтобы отсечь обновления
— если у нас больше 3 экранов — всяке гриды, ленты, длинные списки — тормозит уже браузер, и это лечится всякими виртуальными скроллингами. Оптимизировать реакт в таких случая особо смысла нет.
Частая причина тормозов — отстутствие у JS-разработчиков базовых знаний про алгоритмы. Например, не привыты навыки заменять линейный поиск на lookup по хешу. Может потому что оно неудобно в JS. Могут написать алгоритм на O(N^3) на доставании данных, и потом «что-то реакт тормозит».
То, что влезает на 1 экран может содержать как 10 элементов, так и 100, так и 1000. Элементарнейший пример — excel-табличка против простого todo-списка. Да, в таких вещах используется вирт. скроллинг (иначе оно даже не загрузилось на мало-мальски среднем файле). Но даже одного экрана таблицы хватает, чтобы, при кривой реализации, выжрать всю батарейку вашего ноутбука или мобильника в краткие сроки.
Касательно асимптотики. Ну вот возьмём типичное redux+react приложение. Пусть у нас в react-древе будет всего пару сотен элементов. Мы совершили какое-то действие и store обновился. На самом верхнем уровне за счёт connect-subscribe вызывается render корневого или около-корневого react-компонента. Мы забили на здравый смысл и не используем PureComputed. Собственные shouldComponentUpdate function-ы мы не пишем тоже. Зачем? Что в итоге? render по цепочке вызывается для всех 200 элементом нашего react-древа. На любой чих. Всегда. Всегда сформировывается новое древо. И всегда сверяется со старым. В итоге в DOM улетает одно обновление, скажем, setAttribute.
А теперь у нас лимитированное кол-во connect-ов (используем с мозгами), мы используем мемоизацию для тяжёлых вычислений, не пробрасываем callback-prop-ы свежесгенерированными анонимками. Обновление store-а затрагивает несколько mapStateToProps и производится несколько shallow-сравнений. render вызывается только у одного компонента и на выходе мы получаем тот же самый setAttribute.
Разница огромная. На сложных приложениях она заметна не то, что невооружённым взглядом, а на километровом расстоянии.
Кривая реализация на авось может загубить даже простейшие приложения. Простой пример — у меня дичайше тормозило приложение при открытых devTools-ах. Просто адово. Причина? Redux-расширение. Оно с, настройками по умолчанию, сериализовывало ВЕСЬ store на любой чих туда и обратно. Соответственно оно не имело ни малейшего представления о ссылочной целостности. Съедало столько памяти сколько могло пережевать и начинало медленно помирать. Решилось всё одной единственной настройкой optoins: false
, которая will handle also circular reference
. С тех пор — порхает как бабочка.
Кривая реализация на авось может загубить даже простейшие приложения. Простой пример — у меня дичайше тормозило приложение при открытых devTools-ах.
И вы решили проблему с помощью асимптотической оптимизации, если я правильно понял.
(Патч вместо всего объекта).
Я просто поменял конфиг для redux-dev-tools-а. Там у него свой middleware подключается. И если указать options: false
, то он умеет circular references. И соответственно перестаёт делать 99.999% лишней работы.
Кто сказал, что оптимизация (в т.ч. асимптотическая) всегда требует больших усилий и ухудшения читабельности кода?
А ваш случай я понял так.
Middleware на каждое изменение стора копировало его.
Вы поменяли опции, и оно начало копировать и сохранять только диффы старого и нового состояния стора.
Если предположить, что вы пишете абстрактного коня в жидком сферическом вакууме, а вы только добавляете данные к стору примерно равными порциями, но не удаляете, то это ускорение с N^2 до N
Ну и не стоит забывать что connect создает PureComponent, который только на изменение стора и реагирует.
Но вообще, по моему опыту, большая часть зла исходит от самого реакта и его экосистемы, чем он кривых рук конечного програмиста.
Про экосистему согласен. Есть пример, когда оптимизация через shouldComponentUpdate делает код медленнее чем без него:
https://github.com/erikras/redux-form/issues/3461
Ну если в shouldComponentUpdate
пихать таких слонов как _.isEqual
то можно достичь просто фантастических тормозов :)
shouldComponentUpdate(nextProps) {
if (JSON.stringify(nextProps) !== JSON.stringify(this.props) {
....
}
}
этот пример оттуда просто шикарен :)
Оптимизация когда придет время — это хороший принцип, но все равно нужно выбрать какой-то начальный вариант. И в качестве такого начального варианта мне PureComponentMinusHandlers
совершенно не нравится: он порождает трудноуловимые баги.
Типовой компонент обязан перерендериться при изменении любого из своих свойств.
У меня "пригорело" ещё от оригинала этой статьи, а тут ещё и перевод подоспел. Честно говоря я не совсем понимаю, что это за приложение у автора было такое,
Apparently, most of my components changed most of the time, so on the whole, my app got slower. Oops.
что его компоненты настолько часто изменялись, что shallow-проверки в shouldComponentUpdate оказались overhead-ом, который перекрывает пользу от отсутствия построения лишнего virtualDOM-а.
Мой ещё не богатый опыт был строго противоположным. Проверки в shouldComponentUpdate ускоряли приложение на порядок (раз в 10), что было хорошо видно на fire-графиках.
Типичный набор props состоит из 3-7 полей. В итоге типичная shallow-проверка это 3-7 ===
. Как это может быть настолько медленным, чтобы не оправдать избавление от лишнего render-а и последующего сравнения двух vdom-деревьев? render — это множественные аллокации и присвоения. Сравнение двух деревьев это куда большее количество ===
, чем в shallow-проверке.
Действительно, единственный вариант, который мне приходит в голову, это когда props-ы и правда меняются практически всегда. Но что это за приложение то такое?
Касательно преждевременной оптимизации. Где он её тут увидел? Разница только в нотации method(){}
против method = (){}
. 3 символа?
Ну а аргумент про то, что callback вынесен далеко от render-метода, и заставляет бегать по файлу… Ну тут на вкус и цвет товарищей нет. Лично я предпочитаю держать render-методы как можно более мелкими, простыми и очевидными. А это значит я не нагромождаю их вычислениями, callback-ми, сложными условиями и пр… Мне кажется, в идеале, render метод должен быть куском html-like кода.
Статья с намеренно неправильной аналогией в самом начале — это грязный демагогический приём.
Вдобавок к вышесказанному товарищами faiwer и andy128k: уж если автор советует пихать лямбды в jsx (в то время как все автоматически избегают создания лишних замыканий где угодно, не говоря уж про циклы и часто вызываемые методы, такие как render
), то боюсь представить, какова будет его реакция на плагин react-constant-elements, который идёт ещё дальше, и хоистит jsx-элементы.
P.S. А точнее он начал статью с фотографии своей новой спальни. Спальня красивая, мне понравилась. Но это не спасло статью.
Есть подход с мемоизацией.
https://github.com/timkendrick/memoize-weak
https://github.com/timkendrick/memoize-bind
Вторая как раз про реакт. А т.к. объект функции будет тем же самым, то souldComponentUpdate корректно отработает.
Плюс там используется weak-map, по этому не будет проблем с мусором.
В es6 есть WeakMap. Ключем является объект. Эта связь слабая, если сборщик мусора удалит объект, то map потеряет этот ключ. За счет этого память и не течет.
https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/WeakMap
React, встроенные функции и производительность