Меня зовут Назим Гафаров, я разработчик интерфейсов в Mail.ru Cloud Solutions. На дворе 2020 год, а мы продолжаем обсуждать «нововведения» ES6-синтаксиса и преимущества MobX над Redux. Существует много причин использовать Redux в своем проекте, но так как я не знаю ни одной, расскажу о том, почему мы выбрали MobX.
Как мы пришли к использованию MobX
Mail.ru Cloud Solutions — это платформа облачных сервисов. С точки зрения разработчика, у нас типичная React-админка, которая позволяет полностью управлять облачной средой: создавать виртуальные машины, базы данных и кластеры Kubernetes. Можно скачивать отчеты по балансу, смотреть графики по нагрузке и тому подобное.
Текущий стек — TypeScript, React, Redux, Formik — нас полностью устраивал, за исключением Redux. В какой-то момент к нам пришли с задачей разработать новый проект — админку для платформы интернета вещей. Так как это был новый проект — на отдельном домене и со своим дизайном, мы решили посмотреть в сторону MobX.
Почему не Redux
Многословность
Наверное, всем надоели завывания про многословность Redux, но это реальная проблема. Посмотрите код обычного счетчика. Итак, чтобы изменить состояние нам нужны экшены:
export function increment() {
return {
type: 'INCREMENT'
}
}
export function decrement() {
return {
type: 'DECREMENT'
}
}
Программисты, которым этого мало, создают actionTypes:
export const INCREMENT = 'INCREMENT'
export const DECREMENT = 'DECREMENT'
Дальше пишем редьюсеры:
export default (state = { count: 0 }, action) => {
switch (action.type) {
case 'INCREMENT':
return {
count: state.count + 1
}
case 'DECREMENT':
return {
count: state.count - 1
}
default:
return state
}
}
Мапим State и Dispatch — непонятно зачем, но почему бы и нет:
const mapStateToProps = (state) => {
return {
count: state.count
}
}
const mapDispatchToProps = (dispatch) => {
return {
onIncrement: () => {
dispatch(increment())
},
onDecrement: () => {
dispatch(decrement())
}
}
}
Дальше нам осталось всего лишь законнектить компонент:
import { connect } from 'react-redux'
export default connect(
mapStateToProps,
mapDispatchToProps
)(Counter)
А, нет, еще не все. Еще нужно проинициализировать Store:
import { createStore } from 'redux'
import reducers from './reducer'
const store = createStore(reducers)
export default store
И прокинуть наш Store дальше в приложение через Provider:
import store from './store'
import Page from './Page'
const App = () => (
<Provider store={store}>
<Page />
</Provider>
)
Теперь тот же самый пример, но на MobX. Верстка:
import { observer } from "mobx-react"
import CounterStore from "./Counter"
const App = observer(props => {
const { count, increase, decrease } = CounterStore
return (
<div>
<h1>{count}</h1>
<button onClick={increase}>increment</button>
<button onClick={decrease}>decrement</button>
</div>
)
})
Store:
import { observable, action } from "mobx"
class Counter {
@observable count = 0
@action decrease = () => {
this.count = this.count - 1
}
@action increase = () => {
this.count = this.count + 1
}
}
const CounterStore = new Counter()
export default CounterStore
Я могу понять многословность в обмен на какую-то пользу. Например, статические гарантии в обмен на многословность типизации. Но многословность ради многословности... Зачем? Для чего?
Хотя проблема частично решается хуками, никому не хочется писать лишний бесполезный код, и поэтому появилось 100500 библиотек для борьбы с этим бойлерплейтом. Например:
underscopeio/reduxable
jkeam/reduxsauce
jamesplease/zero-boilerplate-redux
redux-zero/redux-zero
MynockSpit/no-boilerplate-redux
У нас тоже была подобная обертка, она называется Redux-helper. Я думаю, что каждый уважающий себя фронтендер должен написать обертку над Redux для борьбы с бойлерплейтом. Проблема в том, что если вы пытаетесь так улучшить Redux, то у вас случайно получается MobX.
Проблема выбора
Пример со счетчиком был полностью синхронным. Давайте напишем асинхронный код на MobX:
class PostsStore {
@observable isLoading = false
@observable posts = []
@action getPosts = async () => {
this.isLoading = true
this.posts = await api.getPosts()
this.isLoading = false
}
}
Верстка:
import PostStore from 'PostStore'
const PostsPage = observer(() => {
const { posts, isLoading } = PostStore
if (isLoading) {
return <div>Загружаем список постов</div>
}
return (
<ul>
{ posts.map(post => <li>{post.title}</li>) }
</ul>
)
})
Воспроизводить тот же код на Redux я не буду пытаться даже за 300 баксов, так как это займет слишком много времени.
Отсутствие асинхронности в Redux — на самом деле странное решение. В моем детстве тебя могли избить и за меньшую оплошность.
Для решения внезапно возникшей проблемы асинхронности (никто не ожидал ее в вебе), сообщество создало redux-thunk, redux-saga, redux-observable и redux-loop.
Таким образом, любую проблему Redux можно решить с помощью другой проблемы:
Нужна асинхронность — добавь redux-thunk.
Хочешь меньше бойлерплейта — возьми redux-zero.
Reselect для мемоизации селекторов.
Normalizr для хранения данных в нормализованном виде.
Еще нужен Immutable.js, чтобы постоянно не писать спреды.
Ну и хотелось бы писать в мутабельном стиле, поэтому добавим immer (от автора MobX!)
Вам нужно всё это изучить и выбрать какую-то одну комбинацию. В React-экосистеме и так приходится выбирать библиотеку для форм, библиотеку для запросов в сеть и кучу всего еще. Redux это только усугубляет, потому что сам по себе он неработоспособен, к нему нужно подключать еще что-то.
В итоге каждый проект на Redux — множество чужих спорных решений с модными на тот момент библиотеками. Получается «Франкенштейн», поддерживать который приходится вам, а не тому, кто его создал.
Скорость разработки
Азат Разетдинов в своем докладе рассказывал, как увеличилась производительность его команды после внедрения MobX. А вот еще один отзыв, его автор утверждает, что скорость разработки увеличилась в три раза.
Это не единичные мнения. Тысячи других разработчиков на Stateofjs.com пишут, что раздутость (Bloated) и корявость стиля (Clumsy) — одни из самых нелюбимых аспектов Redux. Раздутый код дольше писать и сложнее поддерживать.
Раздутость кода — это одна из причин, почему инопланетяне еще не вышли с нами на связь.
Производительность
Скорость работы MobX не зависит от количества компонентов, потому что мы заранее знаем список компонентов, которые надо обновить, — вместо O(n) сложности Redux.
Тормознутость Redux вшита в его парадигму. В JavaScript очень дорогая иммутабельность, и вы создаете огромную нагрузку на сборщик мусора при копировании объектов на каждое изменение. Даже просто пройти по всем редьюсерам с помощью операции сравнения строк — это очень дорого, намного дороже, чем работа с объектами по ссылке.
В MobX вы не думаете о производительности, потому что она у вас из коробки.
Почему не useContext
Потому что useContext не дает производительности из коробки, вам дополнительно нужно будет обмазаться useMemo и useCallback.
Посмотрите наглядный пример от пользователя @MaZaAa — alert() выскочит только один раз, при первом рендере.
Так как MobX переопределяет shouldComponentUpdate, вам не нужно за этим следить вручную — перерендерится только то, что надо, а не всё дерево.
Ненастоящие минусы MobX
1. Декораторы еще не в стандарте, но, во-первых, можно писать без декораторов, во-вторых, можно писать на TypeScript.
2. Пятый MobX не поддерживает IE11, потому что использует ES6 Proxy, который сложно полифилить. Для нас это не было проблемой, так как мы не поддерживаем браузер семилетней давности. Но если вы его по каким-то причинам поддерживаете (хотя уже сам Microsoft перестает это делать), можно использовать MobX 4 версии (UPD: или MobX 6).
3. Redux в свое время многих подкупил своими дев-тулзами. В MobX с этим тоже нет проблем, можно использовать mobx-devtools или mobx-remotedev.
4. Для Server-side рендеринга обычно на сервере формируют стейт, сериализуют его в JSON и кладут в window.__PRELOADED_STATE__. К сожалению, MobX никак вас не ограничивает в том, что вы можете положить в стейт. Там могут быть циклические данные и другие структуры, которые не могут быть однозначно представлены в JSON.
Но в целом, если вы не храните подобные структуры в стейте, то SSR с MobX — давно решенная проблема, например, в том же Nextjs с помощью useStaticRendering.
5. Мне непонятен этот аргумент, но часто можно слышать, что в MobX слишком много магии. Если не разобраться, как устроена какая-то технология, то, конечно, она покажется магией. MobX — это просто FRP, только вам не нужно вручную подписываться на наблюдаемые объекты. MobX делает это за вас и прозрачно. Он наблюдает, к каким данным вы обращаетесь, подписывается на них и так строит объектный граф.
Создать свой аналог MobX можно за полчаса.
Настоящие минусы MobX и как с ними бороться
Единственный реальный минус MobX — он дает вам слишком много свободы в том, как структурировать код, хранить и обрабатывать данные. Для больших команд и крупных проектов это может быть проблемой, поэтому расскажу, как мы боролись с излишней свободой.
Не мутировать модель в представлении
Вернемся к нашему примеру со счетчиком:
<h1>{Store.count}</h1>
<button onClick={Store.increase}>
increment
</button>
Мы могли бы написать его вот так, то есть мы можем мутировать состояние прямо во вьюхе:
<button onClick={() => Store.count++}>
increment
</button>
Мы решили ни в коем случае так не писать, так как еще наши деды учили разделению ответственности и MVC.
Чтобы запретить мутировать состояние вне хранилища, можно использовать флаг enforceActions в настройках MobX. Но он будет предупреждать вас только в рантайме и создавать проблемы с Promise.
Второй вариант — помечать поля объекта как private. Но в этом случае на каждое приватное поле вам придется создать геттер.
Возможно, вам будет достаточно просто внутри команды договориться мутировать поля исключительно через экшены и следить за этим в код-ревью.
Не наследовать стор от стора
MobX позволяет создавать вычисляемые поля от вычисляемых полей другого стора. Одна наблюдаемая переменная может обновлять другую, которая загружает данные от третьей. Чтобы избежать этой каши, мы решили не наследовать стор от стора.
В классической MVC-архитектуре контроллер получает команды от пользователя, работает с моделью и оповещает view об изменениях. Если нужно разделить какую-то логику между контроллерами, вы создаете сервисный слой.
Мы решили придерживаться этого же пути. Если вам нужны данные одного стора в другом, вы создаете третий стор, в котором объединяется логика двух других. Потому что с точки зрения бизнес-логики это новый доменный объект.
Сервисы предоставляют данные, инкапсулируют бизнес-логику и дают возможность компонентам общаться друг с другом. Так работает Ember и Angular, но к ним мы еще вернемся.
Не внедрять модель через провайдера
Документация MobX учит подключать сторы таким образом:
@inject("CounterStore")
@observer
class App extends Component {
render() {
return (
<h1>{this.props.CounterStore.count}</h1>
)
}
}
Мы же решили просто подключать через импорты:
import CounterStore from "./Counter"
const App = observer(() => {
return <h1>{CounterStore.count}</h1>
})
Импортируем Store напрямую и получаем все бонусы от IDE, такие как автокомплит и статический тайпчекинг.
На Хабре есть огромный тред по этому поводу, там десятки сторонников и противников подобного подхода, не буду повторяться.
Почему не Vue/Angular/Ember
У команды был большой опыт с React, поэтому мы решили не менять фреймворк. Но в целом — связка React+MobX с наблюдаемыми и вычисляемыми полями может напомнить то, как работает Vue.js.
С ограничениями, которые мы наложили на MobX, организация данных стала похожа на Angular. Вот пример из официальной документации:
export class CartService {
items = [];
addToCart(product) {
this.items.push(product);
}
getItems() {
return this.items;
}
clearCart() {
this.items = [];
return this.items;
}
}
В Ember всё то же самое, один в один:
import { A } from '@ember/array';
import Service from '@ember/service';
export default class ShoppingCartService extends Service {
items = A([]);
add(item) {
this.items.pushObject(item);
}
remove(item) {
this.items.removeObject(item);
}
empty() {
this.items.clear();
}
}
Получается, что весь цивилизованный энтерпрайз-мир так разрабатывает приложения. Все данные одной доменной области в одном месте. В этом же месте методы для изменения этих данных — всё максимально прозрачно.
Redux кичится тем, что имеет один источник правды, хотя по факту — это просто глобальная переменная. В сложных проектах вам, скорее, нужно много объектов, каждый из которых отвечает за свою доменную область, чем куча хлама в глобальной переменной.
Не надо думать, что размазав чудовищный редаксовский код по пяти файлам, вы тем самым повышаете его читабельность и ремонтопригодность.
Выводы
Мы выбрали MobX, потому что это простая и эффективная библиотека, которая позволяет аккуратно упорядочить сложные модели предметной области в классы.
В свое время Redux победил, потому что был разумной альтернативой императивному jQuery. Но необязательно страдать всю жизнь, пора двигаться дальше.
P.S. Это текстовая версия доклада с React Moscow и Panda Meetup #39.
P.P.S. Друзья из разработки бэкенда просили передать, что они ищут разработчиков на Python/Go в команду IaaS, команду PaaS и для разработки IAM. Из интересного — разработка на open source, highload, kubernetes, распределенные системы.
unel
вообще-то этот код не очень корректный, последняя установка isLoading будет вызвана вне контекста выполнения экшена getPosts (mobx все обновления выполняет синхронно), соответсвенно, и перерендер после него не произойдёт.
Подобное поведение отдельно разбирается в разделе документации asynchronous actions
И, пожалуй, такое "неочевидное" поведение тоже можно записать в настоящие минусы mobx-а =)
MaZaAa
Что простите?
1) Перерендер произойдет. (https://codesandbox.io/s/determined-browser-zfjyq?file=/src/App.tsx)
2) Вы не поняли походу для чего нужны action/runInAction.
3) Не советую пристально читать то, что пишут в документации к MobX, к сожалению там имеется ересь.
P.S. вообще от action и runInAction можно легко отказаться и включить автоматический батчинг
unel
хм, Ваша правда… интересно, почему..
Возможно… я опирался на вот эти строки в документации:
Вынужден поверить в это:
А что ещё там есть из ереси?
mayorovp
Потому что цикл обработки реакций запускается при любой мутации. Назначение action строго обратное: action откладывает запуск цикла на время своего выполнения.
unel
а, и правда) век живи — век учись! =)
unel
И когда он будет запускаться? Каждый раз при изменении observable / computed?
MaZaAa
Я вам кидал ссылку на codesandbox как раз с примером, там mobx уже сконфигурирован и вы можете поиграться как угодно с ним и выяснить все нюансы, например натыкать this.count++ и поставить консоль логи на рендер и убедиться что он будет срабатывать только когда все ваши синхронные изменения завершатся)
О, я даже уже делал это codesandbox.io/s/zen-surf-g9r9t там надо смотреть в консоль и комментировать/разкоментировать конфиг mobx'a чтобы смотреть на результат.
Основной поинт такой:
1) При инициализации все работает штатно и все реакции синхронные, это обязательно нужно т.к. в момент инита важна синхронность, лень расписывать реальные примеры из жизни, можете просто поверить на слово) Ну либо однажды в этом убедиться лично)
2) После того, как весь синхронный код отработает, как раз через setTimeout будет изменен шедулер реакций, который будет откладывать выполнение реакций через setTimeout, то есть у вас синхронно что-то меняется, но реакции сразу же не вызываются, они будут запланированы через setTimeout после того, как все ваши синхронные изменения закончатся. Как раз это то, что нужно web приложению, чтобы не делать лишних рендеров и реакций, а подождать пока батч синхронных изменений закончится и уже после этого вызывать реакции на эти изменения.
JustDont
А мне идея с setTimeout(f, 1) (вообще-то тогда уж 0, если на то пошло) совсем даже не кажется хорошей. То, что в коде написано синхронно (некая цепочка реакций), и раньше бы могло отработать синхронно (и главное — могло бы оптимизироваться компилятором) — теперь у вас на каждый шаг по цепочке будет бахать новый таск в event loop, ну и JIT в такое не умеет тоже, к слову.
Короче, подписывание ВСЕГО вашего кода под асинхронное выполнение реакций выглядит хорошим только тогда, когда вы реально весь код контролируете, и считаете, что да, вам так делать норм. В больших проектах под много людей такое делать — ну чёт совсем не очень.
MaZaAa
При инициализации все работает синхронно, после инициализации асинхронно(авто батчинг), в 99.9% случаев в ходе работы приложения синхронные реакции не нужны.
В самом большом моем проекте из последних, с командой из 10+ человек вообще проблем НОЛЬ. Только удовольствие от максимально чистого и минимального кода. И от того, что монструозный проект не тормозит (общая производительность зависит только уже от быстродействия АПИ).
Так что религиозные предубеждения и забота о JIT компиляторе вам только вставляют палки в колеса.
Заставить фронтенд проект тормозить по вине MobX'a ну это надо серьезно постараться. Хотя если вы используете mobx-state-tree, тогда вы можете легко заставить свой крупный проект тормозить из-за гипер излишней работы в ран тайме.
JustDont
Ну пардон, наличие или отсутствие цепочек синхронных реакций (а так же их длина) — зависят от архитектуры проекта, а не то, что это какие-то редчайшие ситуации. Они редчайшие, если у вас фронт тупой, и, как и множество типичных фронтов — просто что-то там загружает и показывает и немного потом интерактивит в духе onclick -> запульнуть жсон на бэк. Но далеко не все фронты такие. Когда на фронте много логики — цепочки реакций у вас скорее всего будут достаточно длинные, не говоря уж про то, что еще и динамически изменяющиеся в рантайме.
Да ладно, я заставлял, и даже без особых усилий. Всего лишь отсутствие throttle в нужных местах в некоторых спамящихся мутациях (а-ля позиция скролла) способно поставить любой стейт-менеджмент на колени. Там и vanillajs даже еле справляется, если написать обработку тупенько, в лоб, и так, чтоб JIT-компайлер её не смог заоптимизировать.
MaZaAa
Так и в чем проблема-то, если реакции будут не синхронны, а забатчены автоматом?? Вы просто возьмите и засуньте этот конфиг в ваш мега сложный и крутой проект и посмотрите, сломается ли он или нет.
Вместо философских рассуждений и боязни того, что не произошло и вообще не факт что произойдет, можно проверить это легко и быстро на практике.
nin-jin
$mol_atom не поставит. Он автоматически троттлинг реакции до следующего фрейма.
mayorovp
Реакции в любом случае "бахаются" в reaction loop, что точно так же не может быть оптимизировано компилятором. Не вижу что тут меняет асинхронный запуск reaction loop по отношению в мутации.
JustDont
Согласен, я тут не на то ссылаюсь — заинлайнить что-то в коде реакций компилятор один фиг едва ли сможет. А вот не создавать новую стейт-машину на асинхронный запуск — таки можно, потому что синхронные реакции просто синхронно же выполнятся до конца без лишней пыли.
mayorovp
Вы забыли добавить антидребезг. При множественных мутациях состояния у вас будет создано слишком много таймеров.
MaZaAa
Ничего страшного, стэк не переполнится, каких-то фризов вы не увидите. А если у вас переполнится стэк, то с вашим приложением проблемы)
mayorovp
Разумеется, он не переполнится. Но почему это повод делать лишние действия? Добавление таймера, пусть даже и "пустого", всё-таки не самая дешевая операция, зачем делать её лишний раз?
MaZaAa
— Мы оперируем гигагерцами в процессоре, не мега, а гига.
— Мы оперируем гигабайтами памяти, даже на мобильных устройствах. Не мега, а гига.
Зная это, мы говорим о том, что пустой таймаут это не самая дешевая операция? При этом мы использует монструозные фреймворки и библиотеки…
Лишние действия — в замен на более чистый код, а более чистый код в этом случае побеждает. Всё просто.
mayorovp
Каким образом?
MaZaAa
Что значит каким?))
vs
Мы избавились от 1 action и от 3х runInAction
Carduelis
Почему не использовать
flow
и синтакс генераторов?MaZaAa
Потому что без них все замечательно работает и автоматически всё батчится. А если работает замечательно, то зачем писать лишний код?
kubk
Потому что генераторы не дружат с TypeScript:
nin-jin
Не то, чтобы не дружит, просто генераторы не позволяют выводить тут тип, ибо yield действительно может вернуть всё что угодно в общем случае. Поэтому надо указывать ожидаемый тип явно:
JustDont
Сейчас нет, но, учитывая объем запросов, в будущем подружатся.
Сейчас с TS можно спокойно подружить yield*, и если в принципе переход от yield X к yield* GENWRAP(X) не вызывает жжения в пятой точке, то можно действовать примерно вот так.
mayorovp
Зато получили 8 setTimeout.
MaZaAa
Какой кошмар, от этого приложение перестало работать или стало работать медленнее?
Almatyn
Какой кошмар, от этого приложение перестало работать или стало работать медленнее?
Оправдываете халтуру.
nin-jin
Откуда тут 8 таймаутов?
mayorovp
На каждое изменение любого observable вызывается reactionScheduler, который делает setTimeout.
nin-jin
Они же автобатчатся в один вызов шедулера.
mayorovp
Кем? Я не вижу этого в коде.
nin-jin
https://github.com/mobxjs/mobx/blob/5536533ed26d6bd8c7989b49990994daa3831009/src/api/autorun.ts#L67
mayorovp
Это ж индивидуальный флаг для каждой реакции. И он проверяется уже в цикле обработки реакций, т.е. внутри функции, передаваемой в reactionScheduler. На вызов reactionScheduler он никак не влияет.
nin-jin
Это локальная переменная авторана. Вот если будет несколько авторанов, то да, будет несколько таймаутов. В $mol_atom же будет один единственный requestAnimationFrame в любом случае.
mayorovp
Какая разница сколько там авторанов? reactionScheduler вызывается совсем в другом месте независимо от их количества.
nin-jin
Там во всей кодовой базе только три вхождения setTimeout два из которых не релевантны. Честно говоря мне продолжение данного спора не интересно.
mayorovp
так вызов setTimeout идёт не в кодовой базе mobx, а в одном из комментариев выше
atomic1989
Второй вариант — пример будущих тормозов. Каждое изменение observable свойств будет вызывать autorun. action и runInAction — откладывают вызов autorun до завершения. Автор mobx сам рекомендует все изменения выполнять в рамках action
0xd34df00d
Минимальная частота моего процессора — 400 мегагерц. В ней он очень холодный и ест очень мало энерии. Если ваш сайт заставляет мой девайс греться и есть батарею, то возникает желание закрыть этот сайт поскорее.
MaZaAa
По кайфу писать громоздкий код и жертвовать многим в угоду того, чтобы у дяди Пети на 1mAh меньше батарейки съел сеанс работы с приложением, пожалуйста я не против.
Только вот не надо всех остальных под эту гребенку загонять и думать что это реально имеет значение и оказывает влияние на деньги, которые целевая аудитория приносит бизнесу.
А вот поддержка «такого вот» кода РЕАЛЬНО обходится намного намного дороже и дольше по времени, для бизнеса который тебе платит, чем забота о потреблении на 1mAh меньше. Более такого на «такой вот» код ещё и надо кого-то найти, кто согласится с ним работать.
На дворе почти 2021 год, а мы не микроконтроллеры программируем с тактовой частотой 32kHz и RAM в 4kb. Вот там РЕАЛЬНО надо экономить на тактах процессора и экономии в несколько байт памяти, потому что на этом уровне это действительно заметно.
0xd34df00d
Этот код пишется один раз, разве нет? Он вообще может быть библиотечным, один раз понятие очереди реализовали, в библиотеку вынесли, всё. В этом вашем npm же даже лефтпад есть, почему бы что-то такое не запилить?
Каков масштаб проблемы-то? Сколько этих мутаций на самом деле происходит в среднем приложении?
Ага, а потом страницы хабра всего-то на тыщу комментов тормозят так, что комментарии писать нельзя — хром валится. И процессы с соответствующими вкладками жрут по пол-ядра, несмотря на то, что они просто, ну, отображаются.
А что до «по кайфу» — мне современная веб-разработка ну вот вообще не по кайфу, и я очень счастлив, что занимаюсь не ей, но это совсем другой разговор.
Louter
Мне не нравится подход дельца и его аргумент про мощные машинки (человек явно переоценивает пятый айфон или нексус семилетней давности), но скорость разработки и вложение сил в решение задач в ущерб «долбанке» — хороший фокус. Хотя тут больше смахивает на экономию на спичках.
Строго говоря добавление таймера — дешёвая операция, хоть она и Oмега(n) (всегда столько времени, сколько таймеров уже добавлено) на вставку и создание контекстов на чтение/исполнение, всё равно даже 1000 таймеров не сравнится с рендером всего одного (!) абзаца текста с кастомным шрифтом. Так что антидребезг был бы хорош, но точно не стоит такого напора)
Almatyn
В одном месте десяток лишних таймеров, в другом пару лишних циклов, где то огромный список в память засунем. А в итоге то, что должно летать еле ползает.
justboris
Тем не менее, оптимизировать нужно там где бутылочное горлышко, а не там где это интуитивно кажется по фрагменту кода, опубликованного тут в комментариях
JustDont
Это не оптимизация, это банальное «не надо делать того, что можно не делать». Особенно с учётом того, что отговорка «код будет проще» по факту сейчас не работает — можно использовать генератор и flow, который даёт точно так же «проще» выглядящий код, но без повальных setTimeout на все реакции без исключения.
Вы же, когда код пишете, не говорите «ну вот я тут заведу массив неиспользуемых значений на мегабайтик, всё равно сейчас памяти у всех гигабайты, и тут не будет бутылочного горлышка»?
faiwer
Не очень понимаю о чём весь этот тред\срач. Мне кажется ситуация простая:
Мне кажется весь этот срач можно завершить просто проверив как делает MobX, как в п4 или как в п5.
Или я не прав?
P.S. В KnockoutJS они намудрили и выдали возможность сделать как угодно. Там у каждого observable может быть своя модель обновлений.
JustDont
Попробуйте прочитать его еще раз, медленнее.
Простите за резкость, но это бред. Синхронное оповещение подписчиков (ваше immediately) не порождает никаких костылей (серьезно, вам не нужен runInAction, он даже и при асинхронных оповещениях далеко не всегда нужен), ни тем более тормозов (если вы конечно не будете синхронно делать такой громадный объем вычислений, который таки тормоза даст).
Речь в этом треде как раз идёт о том, что предложенный «элегантный выход» делает любые оповещения отложенными, без всякого разбора. Даже если они вообще-то исполняют только синхронный код, и могли бы без этого финта ушами выполняться синхронно. И всё для того, чтоб оповещения при выполнении асинхронного кода можно было бы записать чуток короче (но нет, потому что с применением flow тоже было бы чуток короче).
faiwer
WAT? В смысле не порождает? Да тот же пример выше явный пример того, что порождает. Вот возьмём связку React + MobX (либо Knockout в базовом виде).
Имеем:
Получается 3 рендера компонента. Вместо 1-го. Первые 2 не нужны были. Это ведь множество аллокаций и довольно тяжёлые реконсиляции. Вы же не будете утверждать, что 1 запись в event loop сопоставима со 2-мя лишними рендерами?
Или я тут что-то недопонимаю?
Всё же давайте без резкости. Я понимаю, русскоязычное комьюнити и всё такое. Но куда плодотворнее дело пойдёт, если приглушить эмоции и оперировать фактами\доводами.
Разумеется буду. Мы же пишем UI приложение. Ну пусть не "громадный", но так или иначе куда более тяжёлый, чем 1
setImmediate + [].push
.Я конечно могу ошибаться, но, мне кажется, синхронные уведомления это такая бомба замедленного действия, которую можно применять только тогда, когда отлично понимаешь, чего это будет стоить и подложишь где нужно соломинку. Т.е. я как раз поддерживаю асинхронные уведомления как дефолт. И не возражаю против возможности где надо — использовать синхронный вариант.
ИЧСХ то же самое сделали авторы спецификации к Promise.
JustDont
Вы же понимаете, что вы сейчас пытаетесь поговорить про две разные системы? Синхронное выполнение реакций ну никак не мешает асинхронно батчить изменения перед рендером (что, собственно, в mobx-react-lite и произойдет, вот только клей mobx и react — это не одно и то же, что и сам mobx).
Делайте. Там, где это вам нужно — делайте асинхронно, а не везде вообще, просто потому что. Для этого, в конце концов, надо только написать async и еще пару слов, и сразу всё будет шоколадно.
А мне кажется, что спам асинхронных тасков в event loop по любому поводу — это гораздо более интересная бомба замедленного действия, которая в простых случаях не рванёт и всё будет норм — но вот зато когда рванёт, то выживших не будет вообще.
MaZaAa
Вам так только кажется, если она рванет, то только потому что вы сделали что-то неправильно, в асинхронном мере JS, все должно быть асинхронно. И код должен писаться исходя из того, что мы находимся в асинхронной среде, а не в синхронной, где всё выполняется строго сверху вниз.
faiwer
А можно с этого момента поподробнее? Вот это уже интересно и конструктивно. Полагаю, нечто подобное, должно быть во Vue. А как это реализуется в случае MobX? И реализовано ли оно так? Если да, то зачем вообще нужен runInAction?
Тоже интересно. Предложите такой сценарий когда это может оказаться бомбой. Ну т.е. приведёт к неожиданному провалу по производительности. Сразу уточню — я имею ввиду вариант под п4, когда изменения batch-ятся. А не когда на любой чих вешается свой таймаут.
К примеру никакой бомбы за всё время использования мною promise-ов я не заметил. Был только 1 tricky case когда асинхронная природа вычисления sha1 браузерным стандартным API поставила крест на его использовании вообще и я взял синхронную JS реализацию. Но это прямо особенный случай.
wheercool
Имхо, вся фишка mobx как раз в том, что уведомления синхронно происходят. Если хочется по дефолту асинхронно, то лучше тогда уже использовать rxjs
MaZaAa
Не нравится, не используйте, делов то куча, кому надо тот заюзает, кому нет, тот не заюзает, нервные клетки не восстанавливаются.
Рвать волосы и пытаться доказать что эти таймауты приведут к отжиранию ресурсов — смешно, потому что это не так, ни вы, ни кто-то другой этого не заметит никогда. Да, и ваш телефон в том числе. Ресурсы отжирают другие вещи например сам браузер, его движок и многое другое, а несколько таймаутов это просто капля в океане, ну реально смешно.
Просто сам факт использования реакта + зоопарка = мега не эффективное использование вычислительных ресурсов, но вас это не волнует, вас волнует парочка таймаутов.
Вас не волнует когда оперируя иммутбильностью вы создаете целые копии объектов выделяя кучу памяти для их хранения и заставляете GC потеть постоянно, более того выделение памяти это вообще не бесплатно для процессора, это очень накладная операция, но это ни кого не волнует, ведь тут парочка таймаутов, которая убивает производительность.
Смешно. «Борцы» за производительность.
faiwer
Никогда не думал что я плюсану хоть 1 твой комментарий, но вот тут ты прав. Складывается ощущение, что JS-народ очень избирательно смотрит за производительностью.
К примеру аргумент против "иммутабельность в JS тормозит, т.к. нет иммутабельных структур данных" обычно приводят такой: "песочница в gc очень быстрая и оптимизирована под коротко живущие объекты". Хотя какой бы быстрой она не было это всё равно прорва операций аллокации и много работы для gc. Хоть как пыжся, но это приличный объём работы
Или когда ругаются против мемоизации — спорят про то что shallow-реконсиляция не бесплатна и все эти проверки отъедают CPU. Но забывают что даже 1 лишний render среднего размера компонента это ну просто в РАЗЫ большее количество работы для CPU.
Или спорят про ++i vs i++, в то время как даже два запроса параллельно выполнить забывают.
И т.д. и т.д. Один товарищ тут на хабре даже долго и упорно втирал что не использует AJAX запросы, т.к. они медленные и вместо этого у него всё на WebSocket-ах без JSON-а. А HTML на стороне бакенда он генерирует руками написанными StringBuilder.append-ми (адский код из кошмаров).
Или этот тред. Где 1 таймаут противопоставляется лишнему рендеру и таймаут рассматривается как более тяжёлая операция (WAT?). Наверное потому что "реконсиляция быстрая" :)
MaZaAa
Тут ничего не складывается, так оно и есть, это не только касаемо JS, это касаемо целой индустрии. В итоге всё это приводит к абсурдным разговорам и спорам, как комментарии в этом треде от «борцов за производительность».
JustDont
Вы уже несколько комментариев подряд пытаетесь поставить себя по другую сторону баррикад, хотя вам довольно давно сказали про flow, и про настолько же «чистый» код, как и в ваших примерах. Так что никакой дихотомии «или-или» тут просто нет, просто вы с помощью «хитрого приёмчика» делаете работу, которой можно было и не делать, и всё так же иметь «чистый» код.
MaZaAa
1) Мне не нравится заворачивать функцию в другую функцию flow и использовать синтаксис генераторов. Я предпочитаю async/await.
2) Тут дело не только в асинхронных функциях, вы вообще где либо можете менять стейт и у вас будут лишние рендеры и реакции, чтобы их не было, надо все заворачивать в action/runInAction и тут flow и генераторы не помогут.
Вопрос зачем? Если можно этого не делать и ничем при этом жертвовать не придется, я надеюсь мы уяснили что несколько таймаутов вообще ни как не связаны с ресурсами и производительностью.
0xd34df00d
Меня волнует, поэтому иммутабельность я себе позволяю в тех языках, где рантайм и GC заточен под иммутабельность (хаскель, например), и где аллокация быстродохнущего мусора не сильно медленнее аллокации на стеке в сишечке.
Однако, практика — критерий истины, а современными сайтами, написанными с подобными идеями, пользоваться невозможно. Хабр тормозит и передаёт привет из 2007-го, гмыло тормозит и передаёт привет из 2006-го, при этом почтовик на моей локальной машине сжирает в 10 раз меньше памяти, я уж не говорю о проце, чем вкладка гмыла, а может куда больше. N+1 заставляет мобильный файрфокс плавить заднюю крышку моего мобильника (при этом аналогичные новости на аналогичном опеннете я могу читать хоть с хлебопечки), ласт.фм тоже лагает (при этом что он может делать сегодня, что не мог делать в том же 2005-м или 2006-м году, непонятно), сайт юристов, через которых оформляли мне всякие визы, вообще неюзабелен, и так далее. К сожалению, я вообще малым количеством сайтов пользуюсь.
При этом да, я понимаю, что я искалечен всяким лоу-летенси, хай-перформанс, пердолингом в байтики, HFT и так далее, но я же, блин, не прошу прикидывать, как выделяемые вами объекты лягут в память, и как TLB-кэш будет к этому относиться, и как на целевом процессоре будут вести себя разные состояния с SIMD-регистрами разной ширины, вместе с таймаутами переключения между ними.
Ну и здравый смысл мне подсказывает, что есть какая-то причинно-следственная связь между отношением «да у нас тут гигабайты и гигагерцы и гигаядра» и тем, что сайты тормозят, а долбанная статическая страница с околонулевой интерактивностью жрёт почти столько же, сколько индекс проекта на плюсах на 800 килострок, и больше, чем IDE для этого проекта, и чем процесс тайпчекера для одного из самых наркоманских и тяжёлых для тайпчекинга языков:
0xd34df00d
Такими темпами можно написать код так, что бутылочные горлышки будут размазаны по всему коду.
Жизнь, увы, не ограничивается случаями, когда есть пара горячих циклов, которые можно оптимизировать вдвое и получить, скажем, 90%-й прирост производительности.
justboris
Если раскидать операции браузера в виде чего-то вроде этой диаграммы, то таймауты будут на одной стороне спектра (очень дешевые), а на другой стороне будет что-то вроде repaint операции.
Таким образом, миллион созданных таймаутов не перевесят даже один лишний рендер. Вот их и нужно отслеживать и оптимизировать, а не докапываться к таймаутам.
Я не говорю, что вариант с setTimeout самый идеальный (можно заменить его на microtask или вообще убрать, как предлагает JustDont). Но вот обвинять setTimeout в проблемах производительности точно не стоит.
nin-jin
Поделюсь своей трустори на эту тему..
В $mol_view для виртуализации необходимо отслеживать визуальное положение элемента в реальном времени. Единственный надёжный способ это сделать — дёргать getBoundingClinetRect в requestAnimationFrame. Когда такой элемент только один, то 60 раз в секунду дёргать всё это не накладно — на моей машине это где-то пол миллисекунды или три процента нагрузки на проц, когда открыта вкладка. Но когда отслеживаемых элементов становится десятки, то без группировки в один requestAnimationFrame нагрузка на проц становится уже такой, что кулер начинает подавать голос. А с группировкой всё норм, укладываемся в 1 миллисекунду.
MaZaAa
Это не таймаут, это getBoundingClinetRect нагружает проц
nin-jin
justboris
А картинка с многими элементами есть? Там случайно forced reflow не происходит?
nin-jin
10 элементов:
Нет, конечно, там же ничего не меняется.
faiwer
Это вы погорячились. 1кк таймаутов это прямо дофига. А если под рендером подразумевать рендер virtual-dom, то тем более.
На самом деле асинхронщина не такая быстрая. К примеру у меня была задача выполнять сотни тысяч sha1 операций над короткими строками. Для этого можно воспользоваться браузерным api crypto. Но тут засада. Он возвращает promise. Итог: использовать для этого дела C++-ый crypto оказалось значительно медленнее чем взять синхронную версию на JS с использованием asm.js. Разница была — небо и земля. Версия на WASM, впрочем сильно на фоне JS + asm.js не выделялась.
Так что все эти наши setTimeout, setImmediate, process.nextTick, requestAnimationFrame и пр. далеко не такие быстрые операции.
Однако соглашусь с тем, что едва ли 1 дополнительный setTimeout можно сравнивать с лишним vdom-рендером хотя бы средних размеров react компонента.
MaZaAa
Тут не все так просто, у нас есть процессор, есть операционная система, в ней как правило сотни процессов и тысячи потоков, каждому процессу и потоку операционка выделяет время на то, чтобы поработать и постоянно переключается между ними, когда она переключилась на какой-то процесс и в нем начала работать синхронная операция, то она с него уже так просто не слезает, по сравнению с тем, если бы не выполнилось ничего в этот момент, поэтому асинхронщина (отложенный вызов синхронного кода) не явлеяется чем-то медленным или нагружающим процессор, оно просто освобождает время на выполнение всех остальных процессов в системе. Поэтому если сумарно измерить время выполнения вашей задачи, то синхронный окажется быстрее по времени разумеется, но это не потому, что он работает быстрее, а потому что он захватил на себя ресурсы системы.
Возвращаясь к вашему пример с sha1: синхронный код разумеется будет работать быстрее, не в плане оптимально по процессорной нагрузке, а том плане чтобы он захватит на себя ресурсы системы. В отличии от асинхронных вызовов которые дают поработать остальной системе.
P.S.
Затраченное время не равно процессорное время.
Нагрузка измеряется именно процессорным временем, а не просто временем выполнения кода.
Не стоит об этом забывать.
faiwer
Честно говоря, я не силён в переключениях контекста и прочем системном программировании, но ЕМНИП то у Promise своя очередь в event loop c очень высоким приоритетом. И я думаю что, тут вся загвоздка именно в накладных расходах на создание и обработку промисов, нежели переключение контекста на уровне ОС и CPU.
Насколько я понимаю, ситуация когда одно приложение могло узурпировать целое ядро процессора и не отпускать его до первого прерывания, это что-то родом из 90-х и сейчас это работает более сложным образом. Тут я думаю 0xd34df00d может подсказать.
MaZaAa
Это проверить легко, while(true) { }
faiwer
while(true){}
повешает свой поток. Это я в курсе. Но как я могу "легко проверить" повешал ли этот код ядро CPU? Вы ведь вроде об этом выше писали.Разве что запустить Х worker-ов, где Х = числу ядер CPU. Согласно вашей логике выше это должно намертво повешать всю систему. Опыт показывает, что это не так.
Или я не правильно понял ваш message?
Суть в том, что скорее всего, event loop в lubuv и работает как раз синхронно всё то время, пока его очереди не пусты. Но тут я уже лезу далеко за пределы своих познаний.
MaZaAa
Не все так просто, забирать больше ресурсов и забрать целое ядро себе это разные вещи.
Короче мораль простая, не надо строить иллюзий и летать в облаках думаю что event loop тормозной, единицы и даже десятки setTimeout'ов убивают производительность и т.д. и т.п.
Это приводит к ложному понимаю картины мира, к говнокоду, к пустым и нелепым спорам и много к чему другому.
0xd34df00d
Это можно так настроить (и в некоторых областях так делается, чтобы даже линуксовое ядро никогда не шедулилось на конкретное физическое ядро, и прерывания там не обрабатывались, и вообще), но в браузерах так никто не делает (зачем?).
mayorovp
Этого как раз довольно просто избежать при проверке. Достаточно лишь не нагружать ничем остальную систему.
0xd34df00d
Ну так если бы с самого начала было сказано, что, мол, замерили, роли это не играет — это был бы совсем другой разговор. Но ведь исходный тезис, на который я отвечал, был о гигабайтах и гигагерцах, которые не жаль потратить.
MaZaAa
Не то чтобы не жаль, когда речь идёт о браузерах и современных вэб приложениях на React'e и т.п., то тут уже априори ресурсы сразу же съедаются в большом кол-ве и рассуждать о том, что setTimout заимпактит ресурсы просто смешно.
Тезис гигабайт и гигагерц о том, что давным давно уже в нашем распоряжении большие мощности, поэтому не стоит доводить коммерческую разработку до абсурда и заботится о каждом тике процессора и о каждом байте выделенной памяти.
mSnus
Конечно, не гигагерцами и не гигабайтами оперируем.
Неплохой ноут работает с 8Гб и 1.8ГГц. На всю систему. Поэтому если одно окно Хрома сожрёт гигабайт и гигагерц — всей остальной системе придётся подвинуться и потупить. А если в фоне уже висит такое окно, то ваше с такими требованиями я могу и вообще не дождаться. Например, Фейсбуком я себя не могу заставить пользоваться, хотя по работе надо. Раз в неделю захожу, и то бесит именно скоростью.
Да, монструозные фреймворки и библиотеки тоже жрут много, но это не повод считать, что так и надо, и рассчитыввать, что гигагерцы и гигабайты полностью ваши.
MaZaAa
Посмотрите на этот код и на то, сколько он выполняется по времени. Потом сравните с несколькими безобидными setTimeout'ами в качестве расплаты за более чистый код и подумайте ещё раз, действительно ли оно стоит того или нет?
Тоже жрут много? Они всё и жрут, как же смешено слушать тех, кто пишет вэб приложения используя фреймворки и библиотеки, всё это работает в браузерах которые отнимают тонны оперативной памяти и процессорного времени на рендеринг и на работу. А потом рассуждают о том, как бы сэкономить пару килобайт памяти и пару наносекунд процессорного времени, но при этом разумеется за счет ухудшения качества кода. Это как зачерпнуть кружкой из океана и думать о том, что ты его наполовину осушил.
mSnus
Я см вами согласен, что переоптимизация кода это тоже вред, и что если тянуть за собой кучу тормозных зависимостей, по мелочам уже можно не особо грустить.
Просто зацепило про "гигабайты и гигагерцы" — их нет, рассчитывать стоит именно на мегабайты и мегагерцы.
nin-jin
Я вам по секрету скажу, что ваше приложение не единственное на компьютере пользователя. А если не соблюдать чистоплотность, то легко засрать любые объёмы ресурсов.
Hardcoin
Очень плохо, что вы ради своей супер-странички пытаетесь оперировать на моем телефоне гигабайтами памяти. Скажите адреса проектов, которые вы поддерживаете, внесу их в черный список.
stardust_kid
Уже только из-за этого MobX не хочется использовать. Я хочу гуглить ответы на вопросы при разработке, а не искать в хрустальном шаре.
Louter
Напоминаете гугл-програмиста)
вообще я счас работаю с redux-проектом и mobx-проектом и разница колоссальна. Не больше, не меньше. В голом Redux 10 раз думаешь, прежде чем создать новый экшн, свойство, там прям изгаляешься, в MobX пишешь стор, экшены, свойства без боли, регистрации и и СМС.
И даже сейчас смотрю на Effector и из коментов взял на вооружение $mol_atom
stardust_kid
Ну, да я много и часто пользуюсь гуглом при работе. Senior front end, стаж 10 лет.
JustDont
Это не «минусы mobx», это минусы асинхронности в JS. Пока у вас async «красит» код (и перейти из окрашенного кода в неокрашенный просто так без дополнительных ухищрений нельзя) — то решать вопросы можно двумя способами:
1) сделать 100% кода «окрашенным» (привет redux-saga и прочие решения на генераторах) — это прекрасно, вот только быстродействие страшно просаживает, потому что async-код очень плохо оптимизируется JIT-компилятором;
2) заставлять программиста руками что-то писать в каждом случае выхода из «окрашенного» кода. Это вот как раз эти runInAction от mobx.
Louter
Строго говоря ничто не обязывает оборачивать в runInAction код, просто надо знать и понимать, где это может стать проблемой.
Строго говоря никто не мешает писать вызов экшена метода после возврата асинхронной функции. Просто теряется кусок магии лаконичности, но иногда рождается магия низкосвязности
nin-jin
3) Использовать $mol_fiber или аналоги, где не нужно ничего "окрашивать" в месте вызова.
Louter
Есть проекты с $mol_atom чтоб «на живую» изучить?) А то смотрю вы тут адептом выступаете)
nin-jin
Публичные я собираю тут: https://showcase.hyoo.ru/
Louter
Спасибо, полезно!)
babylon
Был бы $mol_flashplayer с таймлайном, иерархией, enterFrame я бы не глядя перешёл вместо _fiber. Даже на однопоточный вариант.
Одна такая фича сделала бы $mol недосягаемым в принципе.
nin-jin
Что это за фичи?
А это кто?
unel
Хм, да и posts по идее не должен установиться (ну т.е. он конечно запишется, но PostsPage об этом не узнает)… скажите, а Вы вообще запускали этот код? он правда работает?
mayorovp
Контекст тут ни при чём, перерендер происходит всегда и при любом обновлении.
Вот что правда может произойти в подобном коде — так это лишние рендеры. Но в этом коде лишних присваиваний нет, так что лишних рендеров тоже не будет.