Comments 28
Как человек который вообще не знает React, статья даже была понятно, и теперь я задумался а если перерендер дочерних элементов на моей фреймворке. Так же на собесе могу кратко ответить зачем мемоизация, вдруг когда то перейду на react. Спасибо!
Перерендеры переоценены, пока они не начнут вызывать явное замедление проекта.
Силы и время, потраченное на бездумное написание useRef/useCallback/memo в каждом компоненте чаще всего только вредят.
Нагрузка на чтение кода с useRef/memo/useCallback возрастает в разы, а связанные с этим баги (забыли пропс в зависимости добавить и т.п.) трудно отловить сразу.
Пишите простой код, пока ваше приложение не начнёт явно замедляться. Потом лучше сделать рефакторинг и пересобрать композицию компонентов, чтобы созависимых компонентов было меньше.
P.S. React 19 движется в этом направлении - React Compiler
Удивителен путь которым пошёл современный фронтенд. Как будто не хватало всей ущербности VDOM с его dirty checking-ом, так нужно было ещё придумать функциональные компоненты с их необходимостью обмазываться мемоизацией на каждый чих. Теперь бесконечно боремся и превозмогаем, а если надоело, тоже не проблема, вон выше у человека не тормозит, значит и у пользователя не тормозит и батарейка на телефоне не жрётся, пусть перерендеривается сколько хочет, это теперь норма. Ощущение, как будто смотрю репортаж из психушки.
Весело наблюдать, как ЕДИНСТВЕННОЕ преимущество реакта обернулось против него самого и теперь весь мир героически пытается решить выдуманную проблему. Как же заставить работать рендеринг адекватно
Если вы прочитаете внимательнее, что пишет команда React, то поймёте зачем эти фичи нужны. ФК вместо классов было нужно, чтобы развязать руки разработчикам для декомпозиции и переиспользования кода. С классами не получалось выносить пласт дублирующейся логики, как сейчас хуками. Очень сильный буст DevEx.
А что с VDOM не так? В самой документации описано, какой прирост алгоритмический даёт такой подход. Более того, в тредах Svelte периодически приходят ребята из computer science со своими статьями и исследованиями, насколько была бы трава зеленее у жёлтого брата с виртуальным DOM
А что с VDOM не так?
Вроде написал уже, давайте разверну - огромное колличество dirty checking-а при сравнении старого и нового VDOM. При достаточно тяжёлой вёрстке и слабой машине проблема вполне себе начинает ощущаться (я уж молчу про смартфоны с их батарейкой). Визуально вроде всё норм, но постоянные микролаги на любое действие, ощущение забитости процессора, как будто он занят какой-то тяжёлой задачей, а ты к нему ещё со своим сайтом лезешь. Разработчикам реакта это всё не раз предьявляли и тут они молодцы - спокойно соглашаются со словами "Да, вероятно React не лучший выбор для многих проектов" (в отличии от разработчиков первого ангулара, которые долгое время при критике их dirty checking-а в данных компонента уходили в полное отрицалово, рассказывая как это всё быстро и никак не может тормозить).
Более того, в тредах Svelte периодически приходят ребята из computer science со своими статьями и исследованиями, насколько была бы трава зеленее у жёлтого брата с виртуальным DOM
Я не знаю на счёт Svelte, слишком далёкий от реакта фреймворк/библиотека. Вон ниже SolidJS предлагают. Либа максимально похожая на React, но сделанная на других принципах. Расскажите, что в нём стало бы лучше, начни он использовать VDOM.
Если вы прочитаете внимательнее, что пишет команда React, то поймёте зачем эти фичи нужны. ФК вместо классов было нужно, чтобы развязать руки разработчикам для декомпозиции и переиспользования кода.
Я читал это и работал с хуками вот из этой библиотеки https://react-spectrum.adobe.com/react-aria/hooks.html . Думаю у меня появилось неплохое понимание преимуществ такого подхода. Проблему я вижу не в функциональном подходе, а в конкретной реализации этого подхода в реакте. Опять же посмотрите на SolidJS, там нет необходимости каждую строчку логики в компонентах оценивать на необходимость мемоизации. При этом все преимущества функциональных компонентов присутствуют в полной мере. Ну то есть можно же нормально сдалать.
С классами не получалось выносить пласт дублирующейся логики, как сейчас хуками.
А классы ли тут мешают? Может быть проблема опять в классовых компонентах конкретно реакта?
В целом моя позиция такова: хороший фреймворк может требовать некоторого общего понимания принципов его работы, но не должен требовать этого при написании каждой строчки кода. Реакт зашёл слишком далеко в этом плане. Буквально каждый чих нужно оценивать пропуская через внутренние механизмы этой библиотеки. После долгой работы с ним берёшь что-то нормальное и первое время есть постоянное ощущение нарастающего техдолга, ведь ты не выполняешь постоянно все эти необходимые присяды к которым так привык. Вместо этого ты просто пишешь суть происходящего в компоненте, а как это эффективно отрендерить не твоя проблема. Сейчас выросло уже целое поколение react-фронтендеров которые изначально выучены на всё это и просто не осознают ненормальность происходящего. Им очень сложно объяснить, что есть какая-то проблема, ведь для них так было всегда.
Все эти решения полумеры, в большом приложении будет много изменений, которые иногда должны запускать рендер, иногда нет, в зависимости от фазы луны, курса доллара и десятка настроек. И правильно сказано выше, что когнитивная нагрузка растет очень быстро.
Решения для этого существуют давно - стейт менеджеры: mobx, jotai, redux в конце концов. Можно долго холиварить, какой из них плох, но я не встречал никого, кто скажет, что лучше useState+useContext чем самый ужасный state manager.
И какую же именно проблему решит стейт-менеджер?
И какую же именно проблему решит стейт-менеджер?
MobX полностью снимает проблему лишних перерендеров + повышает производительность, и всё это из коробки и за бесплатно, не надо расплачиваться за это говнокодом. Ну а бонусом нативный код и ноль бойлерплейта.
Вот тут все наглядно видно:
https://stackblitz.com/edit/stackblitz-starters-eowyumpg?file=src%2FApp.tsx
Нажмите на консоль и увидите что рендер происходит только в тех компонентах, в которых нужно
Круто, мне нравится) Только вот есть один нюанс, если мы посмотрим исходники, то увидим:
Вот тут https://github.com/mobxjs/mobx/blob/main/packages/mobx-react/src/observer.tsx#L30 вызов передается observer-у, который в свою очередь просто мемоизирует компонент:
https://github.com/mobxjs/mobx/blob/main/packages/mobx-react-lite/src/observer.ts#L160
То есть memo используется всегда! Насколько это хорошо? Наверное, если бы было очень хорошо, то разработчики React-а бы сделали компоненты мемоизированными по умолчанию. А есть у вас еще аргументы в пользу Mobx?
То есть memo используется всегда!
Всё правильно, в этом и весь смысл
Насколько это хорошо?
Для использования реакта в связке с MobX это отлично. Но для голого реакта или в связке с другими стейт менеджерами такой трюк не прокатит, т.к. они слишком топорные не умеют трекать зависимости умным способом.
Наверное, если бы было очень хорошо, то разработчики React-а бы сделали компоненты мемоизированными по умолчанию.
Они бы рады, и это бы работало, если бы в реакте не существовало такой вещи как React.Context, т.к. там всё топорно, компонент родитель просто не может знать когда контекст изменился, поэтому никаких memo по умолчанию быть не может.
А в MobX напротив, даже когда мы используем контекст, мы засовываем в него экземпляр состояния(инстанс класса) который не меняется и ссылка на него не меняется, внутри него мы можем делать все что угодно и MobX все эти изменения трекает и точечно обновляет нужны компоненты.
Вот пример все завернуто в контекст, но ререндерятся только те, кто должен:
https://stackblitz.com/edit/stackblitz-starters-b9zww5vj?file=src%2FApp.tsx
А есть у вас еще аргументы в пользу Mobx?
Ну вам показали код, вы на него посмотрели, неужели самого этого кода уже не достаточно? Он же просто тупо нативный.
Ну а так обычно все по классике:
- Нативный код
- Минимум лишней когнитивной нагрузки
- Минимум кода(отсутствие лишней лапшы)
- Всё оптимизировано из коробки, про оптимизации можно забыть, я уже больше 7 лет о них не думаю, все летает.
- Не привязан к реакту, можно читать/изменять переменные откуда угодно
Для использования реакта в связке с MobX это отлично
Да, потому что MobX сам решает когда перерендерить. То есть, у нас теперь есть memo, который на каждый рендер будет сравнивать пропсы, и еще сама система реактивности MobX-а, которая не бесплатна.
А по аргументам
Нативный код
Ну приходится изучать MobX, и уже работать с ним:
Минимум лишней когнитивной нагрузки
Спорный момент, где-то может быть даже легче useState, а не писать целый класс
Минимум кода(отсутствие лишней лапшы)
Ну тут посложнее именно в MobX, так как получается легко написать классы, которые будут в 1000 строк в высоту. И потом с ними разбираться будет ой как больно. Это в целом зависит не от инструмента, а от навыков разработчков.
Всё оптимизировано из коробки
Перерендеров нет, но зато есть потребление памяти. Повторюсь - реактивность не бесплатная.
Не привязан к реакту,
Да, это хороший пункт. И тестировать такое легче.
Я не против MobX, даже наоборот, я за MobX. Но называть решения приведенные в статье полумерами, как-то некорректно. Часто нужно понимать как именно реакт работает, и я думаю, что это хорошая шпаргалка
То есть, у нас теперь есть memo, который на каждый рендер будет сравнивать пропсы
Как думаете, что быстрее, сравнить пропсы или лишний перерендер сделать?
и еще сама система реактивности MobX-а, которая не бесплатна.
Да, но по сравнению с иммутабильностью как основным атрибутом на котором построено по умолчанию реактовское управление состоянием, да и в redux и т.п. тоже самое (иммутабильность). Накладные расходы на реактивность MobX'a можно назвать бесплатными и даже профицитными, потому что в совокупности факторов на реальных проектах экономят процессорное время и оперативку, да и с garbage collector'a снимают лишнюю нагрузку, иными словами мы просто не платим налог на иммутабильность.
Ну приходится изучать MobX, и уже работать с ним:
30 мин хватит за глаза, а если посветить ему день, то вы уже асс.
99% случаев покрывают 2 функции:makeAutoObservable()
observer()
Остальные 1% покрываютreaction()
autorun()
when()
А для понимаю принципов его работы достаточно знать getters/setters как работает.
Спорный момент, где-то может быть даже легче useState, а не писать целый класс
В приложениях уровня Hello World да, а в реальном мире конечно же нет) Ну если вам конечно нравится срать в реакт компоненты лишним кодом, а не держать их чистыми, то да) Ну и конечно каждый раз используя ущербное встроенно управление состоянием реакта мы платим производительностью и платим говнокодом. Вообще не вижу ни одной причины платить эту цену.
Ну тут посложнее именно в MobX, так как получается легко написать классы, которые будут в 1000 строк в высоту. И потом с ними разбираться будет ой как больно
1) 1000 строк класса === 1000 строк в компоненте реакта, и что лучше?)
2) Классы можно разбивать как угодно, можно использовать композиции и т.п.
Перерендеров нет, но зато есть потребление памяти. Повторюсь - реактивность не бесплатная.
См. пункт выше, по сравнению с налогом на иммутабильность всё в ажуре наоборот. Тем более я специально замерял в браузере (Вкладка Pefromance в DevTools) потребление больших продакшен приложений которые мы писали с MobX'ом, цифры не отличаются от любых других сайтов и все в диапазоне 85Mb находится. Ну и главное не забывать руками нажимать Collect Garbage чтобы не попадать в фазы луны срабатывания gc.
В сухом остатке потребление памяти даже если какое-то и есть, то накладывает минимум оверхеда. Поэтому аргумент не аргумент.
Но называть решения приведенные в статье полумерами, как-то некорректно.
Ну по сути React + MobX, по сравнению с голым react это просто небо и земля. Поэтому не использовать MobX это принудительно лишать себя возможности лучшей жизни. Поэтому решения в статье это фактически костыли и борьба с реактом, которых можно элементарно избежать просто добавив MobX. Опять же не вижу не единого смысла страдать) Ну в целом я то и не страдаю, да и проблемы описанные в статье для меня никогда проблемами не были из-за MobX'a)
Информативно и без воды!!!
Небольшое уточнение по поводу причин ререндера: их на самом деле всего одна - useReducer и его упрощенная производная useState. Контекст сам по себе не имеет никакого отношения к ререндерам (мы можете прокинуть в него обычную let-переменную, объявленную на уровне модуля и изменять ее, и вы увидите, что ререндера не произойдет, хотя контекст присутствует).
И сам ререндер родителя это лишь следствие того, что я написал выше. Поэтому можно говорить, что он влияет лишь косвенно.
Отсюда вывод: одной единственной причиной ререндера является изменение состояния.
Попробуйте solidjs. Для реактеров привычен, есть хуки и контексты, а все проблемы реакта отсутствуют как класс. Просто пишешь код и не паришься. Только не надо мне говорить, что босс не разрешит
Идеальное название для deathmetal команды из Нижнего Тагила: «Реактивный перерендер».
Прошу прощения, но не смог удержаться.
В статье упущен важный момент о том как работает memo под капотом. Для сравнения новых и старых пропсов используется Object.is. Из-за этого, если в пропсах есть объект или массив, то Object.is, всегда будет возвращать false, даже если содержимое объекта не менялась. Что делает использование memo бессмысленным.
Чтобы memo могло сравнивать объекты, последние нужно оборачивать в useMemo (та же логика что с useCallback)
Возможно, было бы не лишним в контексте перерендеров упомянуть о React Developer Tools и расширении для браузеров, которое поможет наглядно увидеть перерендеры в интерфейсе
Во-первых, стоит оговариваться что мемоизация - это не бесплатно и ререндер чаще гораздо дешевле. Статья во многом перекликается с докладом Абрамова про оптимизации без React.memo и пропущен важный пример, когда чтобы избежать у дочернего элемента ререндер, можно родительский сделать враппером и покинуть в children. Тогда он будет считать его пропсом, а не продолжением ноды.
мемоизация - это не бесплатно
Согласен, но зависит от компонентов. Если в компоненте используется несколько хуков, useState/useEffect, то скорее мемоизация оправдана.
важный пример, когда чтобы избежать у дочернего элемента ререндер, можно родительский сделать враппером и покинуть в children
Вот вообще не важный, так как заставляет создавать структуру компонентов, оглядываясь на мемоизацию. Ну очень редко такое возможно применить. А вот кастомный хук useCallbackRefed, который в конце статьи приводится, я раньше нигде не видел, а он очень удобен для мемоизации
Я посмотрел на Fusor. В React тоже можно так сделать с помощью ref-ов. Все данные можно хранить в ref-ах, а когда нужен перерендер, использовать для этого useReducer:
const [_, forceUpdate] = useReducer((c) => c + 1, 0);
const myData1 = useRef();
const myData2 = useRef();
Но это шаг назад, в отключение реактивности. Это может помочь только в тех случаях, когда нам ну очень важна производительность, и считаем уже каждый такт.
Fusor - не разделение ответственности, потому что философия React в том, чтобы реактивно показать данные, сматченные на верстку. И тут как раз и получается, одна ответственность - реактивно отображать данные, с чем React отлично справляется.
Во Fusor у вас есть выбор. Использовать реактивные сущности: useState/signal/observable, либо обычные переменные там где это не нужно и обновлять вручную. В React нет такой возможности, отсюда и родилась эта статья.
К слову о разделении ответственности, попытаюсь объяснить псевдокодом:
| Stage | Fusor | React |
| -------------- | ---------------- | ------------------------------------- |
| Initting state | `let state = 0;` | `create(); useState(0);` same stage 1 |
| Creating DOM | `create();` | `create(); useState(0);` same stage 1 |
| Changing state | `state = 1;` | `setState(1); update();` same stage 2 |
| Updating DOM | `update();` | `setState(1); update();` same stage 2 |
Борьба с перерендерами в React