Я указал сложность для бинарного дерева. В общем случае словарь где айдишнику соответствует объект можно хранить либо в виде дерева и тогда чтобы найти айшишник нужно сделать двоичный поиск на глубину дерева высота которого равна log(n) либо есть еще способ хранить в виде хеш-таблицы когда выделяем массив какого-то размера и вычисляя некое смешение по айдишнику храним объект с ссылкой на объект с данными а поскольку размер массива меньше чем количество всех возможных айдишников то появятся коллизии и новый объект сохранится в виде ссылки от предыдущего объекта (с таким же значением хешфункции) и поиск объекта по айдишнику либо скатится в линейный поиск по связанному списку объектов либо вызвовет аллокацию нового массива побольше (чтобы коллизий было меньше) и перезапись всех элеементов массива заново вычисляя их смещения. В этом случае поиск объекта по айдишнику недетерминирован и только в большом колиестве операций можно оценить сложность но в любом случае это будет медленней чем гарантированное получение объекта по ссылке за O(1)
Для того чтобы обновить сущности когда приходит ответ от сервера (также как и при получении обновлений по вебсокетам) когда данные приходят в нормализированном виде — хеше объектов где айдишнику соответствует объект с данными то нужно по айдишнику обновить нужный объект в нашем вложенном дереве объектов связанных ссылками. В статье этот способ не описан в деталях я лишь упомянул что нужно добавить учет каждого созданного объекта в глобальной хеш-мапе. То есть нужно просто в конструкторе базового класса сгенерировать айдишник для нового созданного объекта и закешировать его в глобальном словаре.
И теперь при получении данных от сервера всегда можно вытащить нужный объект по его айдишнику и обновить нужные в нем данные а сама структура всех данных в состоянии остается в древовидном виде или точнее в виде графа (если учитывать обратные ссылки на родительские объекты).
То есть нормализация остается только для нужд общения с сервером а для компонент и всего остального данные у нас удобно вложены и ссылаются друг на друга по ссылкам. Это упрощает использование данных как и в шаблонах компонентах так и в обработчиках в отличии от организации данных изначально в нормализованном виде в плоском хеше таблиц как это принято делать используя redux в котором мы теряем возможность обращаться к другим частям состояния просто обращаясь по ссылке.
Поскольку с нормализованным подходом ссылок на объекты больше нет то связи мы теперь вынуждены моделировать через айдишники, и как следствие каждый раз когда нам нужно обратиться к родительской сущности или вложенным сущностям нам нужно каждый раз вытаскивать объект по его айдишнику из глобального стора. А это неудобно.
Например, когда нужно узнать рейтинг родительского комментария мы не можем просто написать как comment.parent.rating — нам нужно сначала вытащить объект по айдишнику — AppState.comments[comment.parentId].raiting. А как мы знаем ui может сколь угодно быть разнообразным и компонентам может потребоваться информация о различных частях состояния и такой код вытаскивания по айдишнику на каждый чих легко превращается в некрасивую лапшу и будет пронизывать все приложение. Например, нам нужно узнать самый большой рейтинг у вложенных комментариев, то через ссылки можно просто записать как
Или когда требуется достать глубоко вложенные данные (например у объекта комментария нужно узнать имя папки в котором он находится где схема сущностей выглядит как user->folder->project->task->comment) то используя ссылки все просто и лаконично
Ну наконец есть момент производительности — операция получения объекта по ссылке это O(1), а операция вытаскивания объекта по айдишнику это уже O(log(n)) что может сказаться на обработке большого количества данных
Поделюсь своим взглядом — сначала нужно выяснить цель изучения английского — пассивное (читать, понимать на слух) или активный (писать и говорить) потому что на мой взгляд эти способы лучше не смешивать для более эффективного погружения. Для пассивного тут подходит любое погружение — фильмы с субтитрами, книги с двойным переводом, аудиокниги и т.д а для активного — изучение грамматики, перевод предложений с русского на английский, пополнение словарного запаса, занятия с репетитором (когда например репетитор спрашивает «а как будет сказать то-то и то-то на английском» и после твоей попытки перевода разбирает твои ошибки и так много часов практики). Надо уточнить что для активного изучения английского важна именно практика перевода потому что самую главную трудность при общении составляет избавление от пауз в попытке сформулировать предложения и вспомнить слова. А возникает это из-за того что мысленно сознательно или подсознательно предложение сначала формируется на русском а потом уже в зависимости от количества ассоциативных связей (перевод слов, правила грамматики, просто заученные фразы) на английском. А перевод с русского на английский как раз нацелен на увеличение эти связей (в отличии от пассивного подхода увеличиваются ассоциативные связи наоборот с английского на русский)
Этот подход сильно ограничен в применении. Например с таким подходом нельзя написать самую главную часть бекенда — базу данных. А база данных либо полностью либо часть данных в виде кеша хранит в некой структуре состоящую из объектов в оперативной памяти. И если на запись из-за race-condition можно разрешить обновлять данные только одному потоку то нет никаких причин не распараллелить чтение на все ядра процессора. Но только вот проблема — в ноде нет возможности читать к одну и ту же структуру объектов из разных потоков а хранить копию структуры в каждом потоке (а потом еще и синхронизировать изменения) никакой оперативки не хватит
Например пришел запрос на вычисление cpu-bound задачи — мы отправили эту задачу в отдельный воркер (не создавая каждый раз новый и не убивая после завершения вычисления). Но тут вдруг запрос прерывается или сообщает что задача уже неактуальна а значить нет смысла ждать пока этот воркер закончит работу над этой задачей и надо как-то сообщить ему чтобы он прекратил вычисления. А исключение это просто как способ прервать какие-то синхронно вычисляемые задачи как например стек вызываемых функций или просто цикл.
Насколько я знаю используя си на есть возможность послать сигнал процессу или потоку ( а потоки в линуксе тоже процессы просто разделяют адресное пространство) и это прервет синхронно вычисляемые задачи даже если там бесконечный цикл и управление сразу передастся обработчику сигнала и дальше уже он сможет либо вернутся к вычислениям либо прервать если задача стала неактуальной.
У веб-воркеров есть недостаток что невозможно как-то прервать вычисления (если задача вдруг стала неактуальной) не убив воркер полностью (это могло бы выглядеть как например бросание исключение работающему воркеру через worker.throw() чтобы прервать синхронные вычисления). Можно конечно прервать через worker.terminate() но создавать на каждую задачу новый воркер слишком медленно и нерационально а нужно иметь пулл потоков равный количеству ядер и распределять задачи между ними. В итоге в воркере нужно разбивать вычисления на асинхронные части и проверять проверять пришло ли сообщение на остановку задачи что сильно усложняет и замедляет вычисления в целом. Будут ли иметь этот недостаток веб-воркеры для ноды?
А как вы с GraphQL решаете проблему дублирования глубоко вложенных данных? Например есть соцсеть и нужно вывести список всех друзей друзей для юзера, например
то согласно graphql этот запрос будет формировать вложенный json в котором возможны очень много дублей юзеров (потому что много людей могут быть знакомы между собой), что выливается мегабайты json-а, траффик и медленную передачу и долгий парсинг на клиенте а также в лишние запросы в базу данных (за дублирующими данными). В то время как через rest можно гибко настроить правильный формат передачи данных чтобы избежания дублирования и лишних запросов в базу
Для меня ключевое понимание наследования и отличие его от композиции пришло после понимания механизма их работы. Приведу пример. Допустим у нас есть класс DBConnection объект которого представляет собой соединение с базой данных и базовые операции работы с базой. Применяя композицию обычно создают отдельный класс Repository который представляет собой crud-операции с базой, который в конструкторе создает объект соединения и сохраняет его каком-то поле и использует потом его для взаимодействия c базой данных в crud-методах. А вот применяя наследование вместо композиции класс Repository отнаследуется от DBConnection и добавит нужный код работы с crud. И здесь принципиальное отличие — в случае композиции при создании объекта Repository будет создано два объекта в рантайме (сам Repository и объект DBConnection) а применяя наследование — только один объект. А в случае если у нас будет цепочка из 10 различных сущностей которые что-то добавляют и переопределяют то с композицией это будет уже 10 объектов а с наследованием только один вне зависимости от длины этой цепочки (да хоть тысячу сущностей). В этом и суть наследования — оно позволяет вынести в compile-time декораторную логику экономя cpu-циклы и память
Структура хранится в одной горутине — выделенном «менеджере» этой структуры. А все остальные горутины получают доступ к ней общаясь по каналам с горутиной-менеджером
В этом случае решение превратится в однопоточное — только одна горутина (менеджер структуры) будет работать а все остальные будут только посылать инструкции. Понятно что при обработке запросов и формирования инструкций будут задействованы все ядра, но дальше все они будут выстраиваться в очередь и ждать пока один единственный поток их не обработает. Если на обновление структуры объектов глобальный лок еще оправдан для избежания race-condition, то инструкции на чтения структуры (базы данных или кеша) могут безопасно выполняться параллельно но не могут из-за такого вот подхода когда только одна горутина будет работать со структурой
JSX — это не нативный js. Это не более чем миф. Это такой же DSL как и остальные. Более того, JSX не только не валидный JS, но и не валидный HTML.
Суть не в самом jsx а в использовании js для условий или циклов чтобы тайпскрипт или flow могли тайпчекать шаблоны. Помимо jsx это может быть подход шаблонных строк как в lit-html фреймвоке
Который уж точно на 100% нативный и няшный js и нам даже не нужно для запуска настраивать babel для компиляции "ужасного" jsx и мы получаем возможность полного тайпчекинга шаблонов
faiwer, интересно а как тот же jsx-control-statements работает с тайпскриптом или флоу — разве он не будет ругаться на необъявленную переменную item в этом примере
и сможет проверить наличие нужных свойств id и title у item? Да, можно сначала скомпилировать шаблон в js и потом проверить но даже если настроить вотчер время отклика будет далеко от юзабельного. И получается что суть этих jsx-хаков ( ?:, &&, .map(el=>)) не сколько в каком-то в том визуальном удобстве или даже не в отсутствии необходимости учить кастомный синтаксис а в банальной возможности протипизировать шаблон
Меня больше всего удивляет синтаксис этого фреймворка
{#each todos as todo, i (todo.id)}
<li class="{todo.done && 'checked'}">
<input type="checkbox" bind:checked="todo.done">
<input class="inline-input" bind:value="todo.body">
{#if todo.nextSibling}
<div>
> {todo.nextSibling.text}
<div>
{/if}
</li>
{/each}
Объясните пожалуйста как этот синтаксис типизируется? Как typescript или flow могут проверить что я не опечатался и в bind:checked="todo.done" идет обращение к объекту todo из массива todos у которого есть свойство "done"? Или как обращение свойству "nextSibling" которое имеет тип "null | todo" будет безопасным после проверки и чтобы без проверки компилятор надавал по рукам? Вот в реакте есть киллер-фича — в нем все байндинги (экспрешены, условия, циклы) прекрасно типизируются и это значительно упрощает жизнь разработчику но тем не менее некоторые все равно продолжают выдумывать фреймворки построенные на шаблонах с кастомным синтаксисом и спец-аттрибутами (и весь этот синтаксис еще надо дополнительно изучать новичку помимо знания js) вместо того чтобы условия и циклы реализовать нативным js-синтаксисом как в реакте в виде
В статье «Less is exponentially more» Роб Пайк почему-то относит обобщённые типы и наследование к «типизированному программированию» и говорит, что предпочитает композицию, а не наследование.
Наследование нужно для того чтобы оптимизировать логику декораторов которые в случае композиции будут создавать отдельные объекты в рантайме. Допустим у нас есть класс DBConnection объект которого представляет собой соединение с базой данных. Применяя композицию обычно создают отдельный класс Repository который представляет собой crud-операции с таблицами базы данных, который в конструкторе создает new DBConnection(...) и использует его для взаимодействия c базой данных. А вот применяя наследование вместо композиции класс Repository отнаследуется от DBConnection и добавит нужный код работы с crud. И здесь принципиальное отличие — в случае композиции при создании объекта Repository будет создано два объекта в рантайме (сам Repository и объект DBConnection) а применяя наследование — только один объект. А в случае если у на будет цепочка из 10 различных сущностей которые что-то добавляют и переопределяют то с композицией это уже 10 рантайм-объектов а с наследованием только один вне зависимости от длины этой цепочки (да хоть тысячу сущностей). В это и суть наследования — оно позволяет вынести в compile-time много работы экономя cpu-циклы и память
Здесь применяется мантра «Не взаимодействую с помощью общей памяти, делай память общей с помощью взаимодействия».
Этот принцип не подходит для одной из самой важной части бэкенда — базы данных или разного рода кешей. Когда есть большая структура-граф объектов в оперативной памяти то нет никакого смысла хранить копию этой структуры в разных горутинах или потоках не говоря уже о необходимости синхронизации частичных обновлений этой структуры между ними и опасности race condition. Для таких задач нужна именно общая память и параллельный доступ к памяти из разных потоков
SharedArrayBuffer даже если и позволит шарить данные между процессами (кстати для этого нужна реализация процессов или точнее виртуальной адресации в JsOS) не позволяет работать с объектами. То есть это просто массив чисел. И вместо создания объектов и обновления их свойств, удаления в общем вместо работы с деревом или графом связанных объектов из разных потоков нужно будет все это реализовывать самостоятельно через массив чисел — собственные классы, объекты, наследование и главное собственный сборщик мусора и все это с диким оверхэдом. В итоге от js останется жалкое подобие Cи без указателей.
А кто-нибудь в курсе как там обстоят дела с многопоточностью? Насколько я знаю v8 жестко завязан на однопоточной модели то есть запускать отдельные инстансы, (isolates, или как они там называются) чтобы обеспечить параллельность для вебворкеров — пожалуйста, но вот настоящей многопоточности — а именно c доступ к разделяемой памяти c zero-copy v8 в принципе не поддерживает. Поэтому что если задействовать в итоге можно будет только одно ядро то мне кажется не было никакого смысла вообще начинать разработку JsOS
На мой взгляд в сравнении фреймвоков и самописных решений упущен очень важный момент — количество кода и зависимостей и при при прочих равных когда баги есть и в устаревших фреймворках и в самописных решениях второй вариант лучше потому что там просто меньше кода. Для наглядности — возьмем популярные шаблоны для создания приложений. При создании нового пустого проекта с популярным нынче create-react-app устанавливается 969 зависимостей, а с vue-cli устанавливается 1434 зависмости. 1434, карл!!! Мне кажется, имея 1434 пакетов, придется чуть ли не каждый день изучать ченджлоги потому что обновился пакет и исправил какие-то баги или добавил какие-то фичи. А что насчет безопасности? Сколько вы потратите времени на аудит, учитывая что каждый из этих пакетов может либо содержать увязвимости, либо быть скомпрометированным и достаточно добавить post-install-скрипт в один из этих 1434 зависимостей чтобы при выполнении команды "npm i" этот скрипт получит полный доступ к компьютеру с возможностями начиная от воровства токенов закачивая шифровальщиками или всякими вирусами. И при всем этом я что-то ни в одном проекте не видел чтобы зависимости коммитили в гит (чтобы избежать подмены исходников на npm, компрометации, или вообще взлома серверов по ссылках в lock-файлах) А какой процент фич от этих тысячи пакетов реально будет использоваться? Учитывая что каждый пакет имеет тенденцию добавлять все новые фичи на все возможные случаи, то чем больше комбинаций из этих пакетов — тем больше будет дублирования этих фич и попытка для большого бизнес-проекта, которому нужны гибкие и специализированные решения и быстрое развитие, выбрать какой-то жирный фреймворк как минимум столкнется с конфликтами, ограничениями и костылями. А теперь если сравнить с самописными решениями? Реакт (до 16 версии имел 20-25к теперь 18к строк) можно написать на 200 строчек кода, получив diff виртуального дома + компоненты. Имея js-парсер можно на 100 строчках реализовать сборку в бандл и webpack вместе с 431 зависимостями будет не нужен. В итоге, если сравнивать по количеству багов, то для того чтобы исправить какой-то баг — что проще — разобраться в устаревшем фреймворке на 20к строчек (собранный вдобавок из кучи других зависимостей) или в маленьком файле на 200 строчек? Я уверен что самописные решения (хотя правильно это было бы назвать "специализированный инструмент заточенный под нужды бизнеса") всегда будут гибче и проще как в поддержке так и развитии
Для того чтобы обновить сущности когда приходит ответ от сервера (также как и при получении обновлений по вебсокетам) когда данные приходят в нормализированном виде — хеше объектов где айдишнику соответствует объект с данными то нужно по айдишнику обновить нужный объект в нашем вложенном дереве объектов связанных ссылками. В статье этот способ не описан в деталях я лишь упомянул что нужно добавить учет каждого созданного объекта в глобальной хеш-мапе. То есть нужно просто в конструкторе базового класса сгенерировать айдишник для нового созданного объекта и закешировать его в глобальном словаре.
И теперь при получении данных от сервера всегда можно вытащить нужный объект по его айдишнику и обновить нужные в нем данные а сама структура всех данных в состоянии остается в древовидном виде или точнее в виде графа (если учитывать обратные ссылки на родительские объекты).
То есть нормализация остается только для нужд общения с сервером а для компонент и всего остального данные у нас удобно вложены и ссылаются друг на друга по ссылкам. Это упрощает использование данных как и в шаблонах компонентах так и в обработчиках в отличии от организации данных изначально в нормализованном виде в плоском хеше таблиц как это принято делать используя redux в котором мы теряем возможность обращаться к другим частям состояния просто обращаясь по ссылке.
Поскольку с нормализованным подходом ссылок на объекты больше нет то связи мы теперь вынуждены моделировать через айдишники, и как следствие каждый раз когда нам нужно обратиться к родительской сущности или вложенным сущностям нам нужно каждый раз вытаскивать объект по его айдишнику из глобального стора. А это неудобно.
Например, когда нужно узнать рейтинг родительского комментария мы не можем просто написать как comment.parent.rating — нам нужно сначала вытащить объект по айдишнику —
AppState.comments[comment.parentId].raiting
. А как мы знаем ui может сколь угодно быть разнообразным и компонентам может потребоваться информация о различных частях состояния и такой код вытаскивания по айдишнику на каждый чих легко превращается в некрасивую лапшу и будет пронизывать все приложение. Например, нам нужно узнать самый большой рейтинг у вложенных комментариев, то через ссылки можно просто записать кака в варианте с айдишниками нужно еще дополнительно замапить айдишники на объеты —
Или когда требуется достать глубоко вложенные данные (например у объекта комментария нужно узнать имя папки в котором он находится где схема сущностей выглядит как user->folder->project->task->comment) то используя ссылки все просто и лаконично
а вот через айдишники это превращается в
Ну наконец есть момент производительности — операция получения объекта по ссылке это O(1), а операция вытаскивания объекта по айдишнику это уже O(log(n)) что может сказаться на обработке большого количества данных
Например пришел запрос на вычисление cpu-bound задачи — мы отправили эту задачу в отдельный воркер (не создавая каждый раз новый и не убивая после завершения вычисления). Но тут вдруг запрос прерывается или сообщает что задача уже неактуальна а значить нет смысла ждать пока этот воркер закончит работу над этой задачей и надо как-то сообщить ему чтобы он прекратил вычисления. А исключение это просто как способ прервать какие-то синхронно вычисляемые задачи как например стек вызываемых функций или просто цикл.
А как вы с GraphQL решаете проблему дублирования глубоко вложенных данных? Например есть соцсеть и нужно вывести список всех друзей друзей для юзера, например
то согласно graphql этот запрос будет формировать вложенный json в котором возможны очень много дублей юзеров (потому что много людей могут быть знакомы между собой), что выливается мегабайты json-а, траффик и медленную передачу и долгий парсинг на клиенте а также в лишние запросы в базу данных (за дублирующими данными). В то время как через rest можно гибко настроить правильный формат передачи данных чтобы избежания дублирования и лишних запросов в базу
В этом случае решение превратится в однопоточное — только одна горутина (менеджер структуры) будет работать а все остальные будут только посылать инструкции. Понятно что при обработке запросов и формирования инструкций будут задействованы все ядра, но дальше все они будут выстраиваться в очередь и ждать пока один единственный поток их не обработает. Если на обновление структуры объектов глобальный лок еще оправдан для избежания race-condition, то инструкции на чтения структуры (базы данных или кеша) могут безопасно выполняться параллельно но не могут из-за такого вот подхода когда только одна горутина будет работать со структурой
Суть не в самом jsx а в использовании js для условий или циклов чтобы тайпскрипт или flow могли тайпчекать шаблоны. Помимо jsx это может быть подход шаблонных строк как в lit-html фреймвоке
Который уж точно на 100% нативный и няшный js и нам даже не нужно для запуска настраивать babel для компиляции "ужасного" jsx и мы получаем возможность полного тайпчекинга шаблонов
faiwer, интересно а как тот же jsx-control-statements работает с тайпскриптом или флоу — разве он не будет ругаться на необъявленную переменную item в этом примере
и сможет проверить наличие нужных свойств id и title у item? Да, можно сначала скомпилировать шаблон в js и потом проверить но даже если настроить вотчер время отклика будет далеко от юзабельного. И получается что суть этих jsx-хаков ( ?:, &&, .map(el=>)) не сколько в каком-то в том визуальном удобстве или даже не в отсутствии необходимости учить кастомный синтаксис а в банальной возможности протипизировать шаблон
Меня больше всего удивляет синтаксис этого фреймворка
Объясните пожалуйста как этот синтаксис типизируется? Как typescript или flow могут проверить что я не опечатался и в bind:checked="todo.done" идет обращение к объекту todo из массива todos у которого есть свойство "done"? Или как обращение свойству "nextSibling" которое имеет тип "null | todo" будет безопасным после проверки и чтобы без проверки компилятор надавал по рукам? Вот в реакте есть киллер-фича — в нем все байндинги (экспрешены, условия, циклы) прекрасно типизируются и это значительно упрощает жизнь разработчику но тем не менее некоторые все равно продолжают выдумывать фреймворки построенные на шаблонах с кастомным синтаксисом и спец-аттрибутами (и весь этот синтаксис еще надо дополнительно изучать новичку помимо знания js) вместо того чтобы условия и циклы реализовать нативным js-синтаксисом как в реакте в виде
Наследование нужно для того чтобы оптимизировать логику декораторов которые в случае композиции будут создавать отдельные объекты в рантайме. Допустим у нас есть класс DBConnection объект которого представляет собой соединение с базой данных. Применяя композицию обычно создают отдельный класс Repository который представляет собой crud-операции с таблицами базы данных, который в конструкторе создает new DBConnection(...) и использует его для взаимодействия c базой данных. А вот применяя наследование вместо композиции класс Repository отнаследуется от DBConnection и добавит нужный код работы с crud. И здесь принципиальное отличие — в случае композиции при создании объекта Repository будет создано два объекта в рантайме (сам Repository и объект DBConnection) а применяя наследование — только один объект. А в случае если у на будет цепочка из 10 различных сущностей которые что-то добавляют и переопределяют то с композицией это уже 10 рантайм-объектов а с наследованием только один вне зависимости от длины этой цепочки (да хоть тысячу сущностей). В это и суть наследования — оно позволяет вынести в compile-time много работы экономя cpu-циклы и память
Этот принцип не подходит для одной из самой важной части бэкенда — базы данных или разного рода кешей. Когда есть большая структура-граф объектов в оперативной памяти то нет никакого смысла хранить копию этой структуры в разных горутинах или потоках не говоря уже о необходимости синхронизации частичных обновлений этой структуры между ними и опасности race condition. Для таких задач нужна именно общая память и параллельный доступ к памяти из разных потоков
На мой взгляд в сравнении фреймвоков и самописных решений упущен очень важный момент — количество кода и зависимостей и при при прочих равных когда баги есть и в устаревших фреймворках и в самописных решениях второй вариант лучше потому что там просто меньше кода. Для наглядности — возьмем популярные шаблоны для создания приложений. При создании нового пустого проекта с популярным нынче create-react-app устанавливается 969 зависимостей, а с vue-cli устанавливается 1434 зависмости. 1434, карл!!! Мне кажется, имея 1434 пакетов, придется чуть ли не каждый день изучать ченджлоги потому что обновился пакет и исправил какие-то баги или добавил какие-то фичи. А что насчет безопасности? Сколько вы потратите времени на аудит, учитывая что каждый из этих пакетов может либо содержать увязвимости, либо быть скомпрометированным и достаточно добавить post-install-скрипт в один из этих 1434 зависимостей чтобы при выполнении команды "npm i" этот скрипт получит полный доступ к компьютеру с возможностями начиная от воровства токенов закачивая шифровальщиками или всякими вирусами. И при всем этом я что-то ни в одном проекте не видел чтобы зависимости коммитили в гит (чтобы избежать подмены исходников на npm, компрометации, или вообще взлома серверов по ссылках в lock-файлах) А какой процент фич от этих тысячи пакетов реально будет использоваться? Учитывая что каждый пакет имеет тенденцию добавлять все новые фичи на все возможные случаи, то чем больше комбинаций из этих пакетов — тем больше будет дублирования этих фич и попытка для большого бизнес-проекта, которому нужны гибкие и специализированные решения и быстрое развитие, выбрать какой-то жирный фреймворк как минимум столкнется с конфликтами, ограничениями и костылями. А теперь если сравнить с самописными решениями? Реакт (до 16 версии имел 20-25к теперь 18к строк) можно написать на 200 строчек кода, получив diff виртуального дома + компоненты. Имея js-парсер можно на 100 строчках реализовать сборку в бандл и webpack вместе с 431 зависимостями будет не нужен. В итоге, если сравнивать по количеству багов, то для того чтобы исправить какой-то баг — что проще — разобраться в устаревшем фреймворке на 20к строчек (собранный вдобавок из кучи других зависимостей) или в маленьком файле на 200 строчек? Я уверен что самописные решения (хотя правильно это было бы назвать "специализированный инструмент заточенный под нужды бизнеса") всегда будут гибче и проще как в поддержке так и развитии