Создал тут пример чтобы оценить влияние на производительность иммутабельного подхода redux-а — codesandbox.io/s/7yvmx50m06 против обсерверов mobx-а codesandbox.io/s/1qrvz4qp57. При клике на любой subtask будет происходить анимация движения подзадачи по экрану. На 11 тысячах подключенных компонентов фпс (инструмент в chrome devtools) в примере с redux у меня где-то 20, а если взять продакшн-сборку реакта и redux то больше 30 не поднимается. У mobx стабильно 59. Советую сделать замер на вкладке performance в chrome devtools и посмотреть гребешки выделения памяти. 11 тысяч компонентов это конечно много но это когда уже тормозит а значит для 60 фпс надо не больше 5 тысяч. А учитывая что обработчики могут быть посложнее обновления свойства то на обновление компонентов останется еще меньше времени а если еще будет несколько обработчиков высокочастотных событий или анимаций одновременно (а это частый случае потому что компоненты хочется делать независимыми а не шарить один обработчик анимации на всех) то падение производительности будет еще больше и количество компонентов надо еще больше уменьшить. Но это на компьютере, а если взять мобилки с медленным процессором и небольшой памятью то ситуация будет еще хуже. Может я чего-то пропустил но я даже не вижу способов что-то закешировать, замемоизировать или что там еще можно сделать с иммутабельностью в примере с redux
Этот вариант уменьшает количество работы чтобы обновить объект, но с точки зрения производительности проблема остается. В нормализованном виде, если я правильном вас понял, у нас вместо дерева будет только два уровня — объект AppState будет хранить список таблиц а каждая таблица будет хранить хеш объектов по их айдишнику
Теперь если нам нужно обновить комментарий то мы можем "просто" написать так AppState = {...AppState, comments: {...AppState.comments, [comment.id]: {...comment, text: 'new text'}}}
А с точки зрения производительности мы все равно выполняем кучу работы — а) создаем новый объект комментария и копируем туда остальные свойства, б) создаем новый объект AppState и копируем туда все остальные таблицы в) — это самое важное — создаем новый объект хеша и копируем туда все айдишники со ссылками на другие объекты. В варианте с деревом количество комментариев которые надо скопировать ограничивался только одним таском (а их обычно немного) то теперь мы совместили все комментарии всех тасков всех проектов со всех папок в одном большом хеше и нам теперь придется их все копировать каждый раз при обновлении. Можно сказать что это микроптимизации приведя цитату Кнута, но когда вы столкнетесь с высокочастотными обработчиками событий вроде перемещения мыши или скролла этот подход с копированием тысячи айдишников и созданием лишних объектов (особенно когда redux-у при любом обновлении стора нужно вызвать mapStateToProps абсолютно всех подключенных компонентов, а что мы делаем внутри mapStateToProps? — мы создаем новый объект указывая дополнительные пропсы) будет вызывать тормоза. Поэтому тут подход с обсерверами mobx выигрывает потому что у нас: a) не будет создан ни один лишний объект б) обновление свойства любого объекта все равно короче и проще — comment.text = 'new text';
Но основная проблема с нормализованным подходом в другом — мы теряем возможность обращаться к другим частям состояния просто обращаясь по ссылке. Поскольку связи мы теперь моделируем через айдишники, то каждый раз когда нам нужно обратится к родительской сущности или вложенным сущностям нам нужно каждый раз вытаскивать объект по его айдишнику из глобального стора. Например, когда нужно узнать рейтинг родительского комментария мы не можем просто написать как в mobx comment.parent.rating — нам нужно вытащить объект по айдишнику — AppState.comments[comment.parentId].raiting. А как мы знаем ui может быть сколь угодно быть разнообразным и компонентам может потребоваться информация о различных частях состояния и такой код вытаскивания по айдишнику на каждый чих легко превращается в некрасивую лапшу и будет пронизывать все приложение. Например, нам нужно узнать самый большой рейтинг у вложенных комментариев, то вариант с обсерверами и ссылками между объетами — comment.children.sort((a,b)=>b.rating - a.rating))[0] а в варианте с иммутабельностью и айдишниками нужно еще дополнительно замапить айдишники на объеты — comment.children.map(сhildId=>AppState.comments[childId]).sort((a,b)=>b.rating - a.rating))[0]. Или вот, сравните пример когда у нас есть объект комментария нужно узнать имя папки в котором он находится: 1) — вариант c ссылками — comment.task.project.folder.name 2) вариант с айдишниками — AppState.folders[AppState.projects[AppState.tasks[comment.taskId].projectId].folderId].name
И с точки зрения производительности — операция получения объекта по ссылке это O(1), а операция вытаскивания объекта по айдишнику это уже O(log(n))
Да, вы совершенно правы, mobx в @observer декораторе действительно переопределяет shouldComponentUpdate возвращая true если не изменились пропсы потому что вложенные компоненты будут обновляться отдельно. Я просто забыл добавить этот момент в статье. Более того для того чтобы не получилась ситуация когда мы обновляем более глубокий компонент раньше его родителя (в случае когда нужно обновить обоих) mobx использует специальный метод react-а ReactDOM.unstable_batchedUpdates который обновит компоненты в правильном порядке. В redux кстати существует точно такая же проблема и необходимо вручную добавлять middlerare с вызовом unstable_batchedUpdates (https://github.com/reactjs/redux/issues/1415)
Тьху, я перепутал — в shouldComponentUpdate он возвращает
falseа неtrueконечно же.Этот вариант уменьшает количество работы чтобы обновить объект, но с точки зрения производительности проблема остается. В нормализованном виде, если я правильном вас понял, у нас вместо дерева будет только два уровня — объект AppState будет хранить список таблиц а каждая таблица будет хранить хеш объектов по их айдишнику
Теперь если нам нужно обновить комментарий то мы можем "просто" написать так
AppState = {...AppState, comments: {...AppState.comments, [comment.id]: {...comment, text: 'new text'}}}А с точки зрения производительности мы все равно выполняем кучу работы — а) создаем новый объект комментария и копируем туда остальные свойства, б) создаем новый объект AppState и копируем туда все остальные таблицы в) — это самое важное — создаем новый объект хеша и копируем туда все айдишники со ссылками на другие объекты. В варианте с деревом количество комментариев которые надо скопировать ограничивался только одним таском (а их обычно немного) то теперь мы совместили все комментарии всех тасков всех проектов со всех папок в одном большом хеше и нам теперь придется их все копировать каждый раз при обновлении. Можно сказать что это микроптимизации приведя цитату Кнута, но когда вы столкнетесь с высокочастотными обработчиками событий вроде перемещения мыши или скролла этот подход с копированием тысячи айдишников и созданием лишних объектов (особенно когда redux-у при любом обновлении стора нужно вызвать mapStateToProps абсолютно всех подключенных компонентов, а что мы делаем внутри mapStateToProps? — мы создаем новый объект указывая дополнительные пропсы) будет вызывать тормоза. Поэтому тут подход с обсерверами mobx выигрывает потому что у нас: a) не будет создан ни один лишний объект б) обновление свойства любого объекта все равно короче и проще —
comment.text = 'new text';Но основная проблема с нормализованным подходом в другом — мы теряем возможность обращаться к другим частям состояния просто обращаясь по ссылке. Поскольку связи мы теперь моделируем через айдишники, то каждый раз когда нам нужно обратится к родительской сущности или вложенным сущностям нам нужно каждый раз вытаскивать объект по его айдишнику из глобального стора. Например, когда нужно узнать рейтинг родительского комментария мы не можем просто написать как в mobx
comment.parent.rating— нам нужно вытащить объект по айдишнику —AppState.comments[comment.parentId].raiting. А как мы знаем ui может быть сколь угодно быть разнообразным и компонентам может потребоваться информация о различных частях состояния и такой код вытаскивания по айдишнику на каждый чих легко превращается в некрасивую лапшу и будет пронизывать все приложение. Например, нам нужно узнать самый большой рейтинг у вложенных комментариев, то вариант с обсерверами и ссылками между объетами —comment.children.sort((a,b)=>b.rating - a.rating))[0]а в варианте с иммутабельностью и айдишниками нужно еще дополнительно замапить айдишники на объеты —comment.children.map(сhildId=>AppState.comments[childId]).sort((a,b)=>b.rating - a.rating))[0]. Или вот, сравните пример когда у нас есть объект комментария нужно узнать имя папки в котором он находится: 1) — вариант c ссылками —comment.task.project.folder.name2) вариант с айдишниками —AppState.folders[AppState.projects[AppState.tasks[comment.taskId].projectId].folderId].nameИ с точки зрения производительности — операция получения объекта по ссылке это O(1), а операция вытаскивания объекта по айдишнику это уже O(log(n))
Да, вы совершенно правы, mobx в
@observerдекораторе действительно переопределяетshouldComponentUpdateвозвращаяtrueесли не изменились пропсы потому что вложенные компоненты будут обновляться отдельно. Я просто забыл добавить этот момент в статье. Более того для того чтобы не получилась ситуация когда мы обновляем более глубокий компонент раньше его родителя (в случае когда нужно обновить обоих) mobx использует специальный метод react-а ReactDOM.unstable_batchedUpdates который обновит компоненты в правильном порядке. В redux кстати существует точно такая же проблема и необходимо вручную добавлять middlerare с вызовом unstable_batchedUpdates (https://github.com/reactjs/redux/issues/1415)