В течение последних шести лет наша команда активно использовала React и NextJS в своих проектах. Эти фреймворки предоставляли нам мощные инструменты для создания сложных веб-приложений. Однако, в процессе разработки мы всё чаще натыкались на некоторые критические недостатки данных инструментов. Особенно сильно они показали себя на недавнем большом высокоинтерактивном проекте, разработка которого велась 3 года.
React, несмотря на свою мощь и гибкость, иногда может быть сложным и запутанным, особенно при работе с большими и сложными деревьями компонентов. Мы столкнулись с проблемами производительности, недостатками многих популярных библиотек (и сложностью их подбора) и некоторыми другими проблемами, которые замедляли наш процесс разработки и ухудшали developer experience.
Стало очевидно, что нужно начать активнее рассматривать альтернативы. Мы с командой начали параллельно изучать другие фреймворки, читать документацию, смотреть простые (и не очень) примеры, статьи, сравнивающие их и т.д. Исходя из этой информации, анализировали плюсы и минусы каждого из фреймворков настолько, насколько это возможно, не имея реального опыта разработки с ними. Наш анализ показал, что Vue и Nuxt, в теории, вполне могут предложить нам то, что мы искали — простоту, гибкость, хороший набор встроенных решений и производительность, сохраняя при этом доступ к большому комьюнити и исчерпывающему набору библиотек.
Уточню, что в этой статье речь пойдёт о React 18/NextJS 13-14 и Vue 3/Nuxt 3. Некоторые проблемы, описанные в статье, команда React уже собирается решить в новой версии.
И небольшой дисклеймер — эта статья не призвана убедить Вас, что Vue лучше React, и Вам обязательно нужно на него перейти. Скорее, это описание того, как опыт нашей команды подтолкнул нас к выбору попробовать что-то новое в виде Vue и Nuxt. Выбор фреймворка — это всегда, в том числе, вопрос предпочтений команды. Я же предлагаю Вам просто ознакомиться с различиями, которые мы для себя подчеркнули и сделать на основании этого свои выводы.
Сразу скажу, мы опустим тот момент, что React — это библиотека, а не фреймворк, потому что это утверждение само по себе для разработчика ничего не значит. Вместо этого я сфокусируюсь на конкретных проблемах, которые в том числе являются следствием факта, описанного выше.
Бизнес любит React за то, что под него легко оперативно найти разработчика, т.к. их больше, чем у любого другого ui-инструмента. Однако, при таких рассуждениях очень часто опускается тот факт, что код на одном React-проекте может существенно отличаться от кода на другом React-проекте. Причина кроется в том, что React по своей натуре призван лишь предоставить нам набор инструментов, но при этом он довольно слабо регламентирует то, как оформлять код, используя их. Есть API, есть правила пользования реактивностью, описанные в документации, но это немного другое. Можно написать два принципиально разных проекта, следуя и API, и документации, и оба они будут работать одинаково правильно и хорошо, но их код при этом всё равно будет сильно отличаться. Из-за этого онбординг нового разработчика в отдельных случаях может занять неоправданно много времени. Есть ещё один момент, который является следствием такой большой свободы, — перекладывание ответственности за составление стандартов и правил использования React в конкретном проекте на разработчика. Для кого-то это может показаться показаться плюсом. Безусловно, есть разработчики, которые любят сами строить свой подход к разработке либо руководствоваться готовыми подходами из комьюнити.
Разберём объективную и субъективную сторону этого вопроса.
Субъективно, пока что нашей команде подход Vue к разработке видится весьма удобным и не противоречащим устоявшимся у нас подходам. Мы любим, когда хороший фреймворк, созданный опытными людьми, принимает такие относительно низкоуровневые решения за нас, позволяя нам сфокусироваться на нуждах бизнеса.
Объективно, каким бы ни был удобным Ваш кастомный набор правил и соглашений, который Вы внедрили в Ваши React-проекты — онбординг новых сотрудников и перепрыгивание между проектами (при условии, что не все из них именно Вы разрабатывали с нуля) всё ещё остаётся ощутимой проблемой.
React хоть и предоставляет декларативный подход к оформлению пользовательского интерфейса, но его API часто даёт нам только какой-то один инструмент для реализации принципиально разных механизмов. А мы всё же предпочитаем, когда котлеты идут отдельно от мух.
Давайте разберём некоторые примеры.
Вспомним, как в функциональных компонентах React организована работа с жизненным циклом. Нужно запускать какую-либо логику на этапе монтирования компонента? useEffect. На размонтировании? useEffect. На обновлении какого-то одного реактивного состояния? useEffect, но не каждый такой коллбэк понравится линтеру, ведь React требует в таком случае указывать все реактивные зависимости. Если зависимостей несколько, а запускать нужно только при изменении одной, пишите дополнительную логику и оборачивайте тело коллбэка в условие, чтоб он не срабатывал лишний раз.
А что насчёт более экзотических сценариев?
Например, когда мне нужно запустить какую-то логику прямо перед размонтированием компонента? Если Вы подумали про useEffect, то Вы не угадали. По крайней мере, не совсем. Такой подход не всегда может сработать, и иногда Вам будет нужен useLayoutEffect с функцией очистки. А теперь спросите себя — насколько это очевидное поведение? Насколько из названия хуков понятно, что они делают и какие у них возможности? Насколько вообще целесообразно всю работу с жизненным циклом и всю логику работы с сайд-эффектами заворачивать всего в каких-то два хука? А как насчёт того, что поведение useEffect по умолчанию — это бесконечный цикл? (речь идёт о сценарии, когда в коллбэке используется состояние компонента, а массив зависимостей не передан).
Это далеко не единственный пример проблем с семантикой в React. Но пример довольно показательный.
Кто-то может сказать: “C этим ведь всё равно можно работать” и будет прав. Но “можно работать” и “можно удобно и предсказуемо работать” — это, всё же, две разные вещи.
Несмотря на это, код на React во многих ситуациях ещё и умудряется быть довольно громоздким и переусложнённым за счёт большого количества бойлерплейта. Что приводит нас к следующему пункту:
Не секрет, что производительность React отстает от конкурентов. Часто это преподносят как чисто теоретическую проблему, но на больших проектах она, как правило, довольно быстро становится реальной. Помимо этого React возлагает большую ответственность за оптимизацию производительности на разработчика, что лишь отвлекает от решения бизнес-задач и понижает производительность уже самого разработчика, а не только библиотеки.
Выражается это в том, что от Вас требуется не только описывать ui-логику, — нужно ещё заботиться о перерисовках!
Ввиду довольно топорного механизма обновления компонентов, при каждом обновлении состояния компонента он перевызывается и перевызываются все его потомки. Помнится, кто-то назвал этот подход к обновлению состояния приложения самым настоящим брут-форсом. И этот кто-то был абсолютно прав.
Для того, чтобы не все перевызовы приводили к перерисовкам, разработчику нужно активно использовать встроенные инструменты мемоизации. ****Да, именно разработчику. При всех тех проблемах, которые React решает, эту задачу он почему-то тоже возлагает на плечи разработчика, что и приводит во многом к раздутому бойлерплейту, про который говорилось выше.
Ну и, естественно, итоговая производительность во многом зависит от компетенции/внимательности/прямоты рук разработчика, и того, сколько времени ему дали на отладку и оптимизацию этой самой производительности. И даже если сойдутся все звёзды, производительность всё равно может отставать от конкурентов вроде Vue/Solid/Svelte.
Согласитесь, если Вы ищете инструмент, который решит за Вас типовые задачи по проектированию ui, вам, скорее всего, захочется, чтобы такой функционал там был сразу.
В React нет встроенного решения для этой задачи, поэтому разработчикам приходится полагаться на сторонние библиотеки (лично мы давно остановились на Styled-Components). Однако, в этом случае есть свои подводные камни. Например, та же Styled-Components работает по принципу создания компонента-обёртки над DOM-элементом. Как итог — получаем засоренное дерево компонентов в инструментах разработчика.
Тут мы и подходим к одной из реальных проблем того, что, React вроде и не претендует на звание фреймворка и вместо этого гордо зовётся библиотекой. А значит, он и не призван избавить Вас прямо таки от всех головных болей. Но как правильно отметил автор этой статьи, React пытается занять какую-то невнятную нишу между библиотекой и фреймворком. При этом инструменты для стилизации разработчик должен выбирать и подключать отдельно, хотя, казалось бы, какой может быть ui без стилизации?
Можно, конечно, использовать обычный CSS или даже настроить поддержку CSS-модулей в сборщике, но, на мой взгляд, это будет куда менее удобно.
Этот раздел будет немного отличаться от предыдущего, т.к. с Next 13 у нас возникла только одна действительно серьёзная проблема. А именно:
Когда мы, только вышедшие с проекта на Next 11, начали новый проект на Next 13, мы и подумать не могли, что с серверными компонентами будет столько боли. И на то было несколько причин. Первая — мы очень любим использовать CSS-in-JS. Это уже давно стало стандартной частью нашего воркфлоу (не агитирую никого делать так же, это всего-лишь предпочтения нашей команды). И как же досадно было узнать, что все популярные CSS-in-JS-библиотеки не могут работать исключительно на сервере, им обязательно нужно тащить свой JS и в браузер.
Вторая причина — библиотеки ui-компонентов. С ними та же история. Почти ни одна библиотека не поддерживала исключительно серверный рендеринг. Исключением является Next UI, но его не стали брать, т.к. встроенный в библиотеку tailwind-css очень сильно засорял HTML. Так мы и познакомились с надоедливой директивой ‘use client’, которую приходилось писать в каждом компоненте.
А если Вы работаете с похожим стеком (CSS-in-JS + практически любая библиотека ui-компонентов) и Вам нужно сгенерировать статический сайт — будьте готовы к тому, что Вам либо придётся отказаться от привычного стека, либо Ваш сайт в итоге будет гибридным SSR-приложением.
Насколько я знаю, проблема актуальна и на момент написания статьи.
В общем и целом создаётся впечатление, что Vercel не хотели проектировать отрисовку компонентов на сервере, подстраиваясь под текущую экосистему библиотек, а наоборот — рассчитывали на то, что экосистема подстроится под них. И она подстроится, конечно, но сколько времени на это уйдёт? Сколько библиотек не выдержат такой переход и прекратят обновляться? Что в это время делать самим фронтендерам? Мы пока так и не нашли ответов на эти вопросы.
Были и другие, не столь значительные неудобства. Например, пришлось отключать ненужные проверки линтера и типов при билде (зачем их вообще включили по умолчанию?). Да, пришлось смириться с некоторыми странностями App Router, но это всё мелочи и не повод уходить от фреймворка, с которым давно работаем.
Казалось бы, все эти проблемы вполне себе хорошо решает какой-нибудь Svelte или Angular. Почему не выбрали их?
Так вышло, что ни Svelte, ни Angular, мы пока ещё не использовали на реальных проектах, так что познакомиться с ними слишком уж тесно у нас пока не получилось. А вот с Vue и Nuxt у некоторых из нас уже был хоть и небольшой, но реальный опыт.
Поэтому то, что будет сказано ниже про Svelte и Angular, — это информация, не подкреплённая реальным опытом разработки.
Так что прошу воспринимать следующую часть этого блока со здоровой долей скептицизма. Отнеситесь к этому, скорее, как к моментам, на которые Вы можете обратить дополнительное внимание, если решите использовать один из этих фреймворков.
Итак:
Svelte —хоть он нам чисто по документации и примерам понравился больше, чем Vue — на момент написания статьи, нам кажется, что у него всё ещё нет настолько большой экосистемы и комьюнити, чтобы объявить его универсальным инструментом для разработки веб-приложений любой сложности.
Angular — показался нам довольно громоздким в плане синтаксиса.
Проведём сравнение по тем же пунктам, по которым ругали React выше:
Даже если Вы только начинаете знакомство с Vue — Вы, скорее всего, обратите внимание на то, что Vue во многих моментах довольно таки строго навязывает разработчику то, как именно нужно использовать данный фреймворк. Это видно во всём, начиная с синтаксиса компонентов, где сходу в глаза бросается строгое разделение на HTML-шаблон, JS-логику и стили. Отмечу, что слово “навязывает” в этой статье используется исключительно в хорошем смысле. Фреймворк принимает за Вас решения, чтобы над этим не пришлось заморачиваться Вам, и Вы могли бы сконцентрироваться на выполнении бизнес-задач.
Строгие регламенты касательно того, как объявлять и использовать тот или иной кусок логики проходят тонкими нитками через весь воркфлоу на Vue. От единого синтаксиса шаблонов и двустороннего связывания логики инпутов до объявления различных видов состояний и работы с жизненным циклом компонентов.
Чем же это хорошо для разработчиков? Помимо того, что многие инфраструктурные решения и стандарты тут уже объявлены за Вас, — Вам будет легче переключаться с проекта на проект, брать на поддержку уже готовые проекты на Vue (если Вы работаете в сервисной компании, например) и онбордить новых сотрудников. А всё потому ввиду такого строгого API фреймворка и наличия встроенных стандартов на Vue намного сложнее как-то прямо очень по-разному написать два куска кода, которые делают похожие вещи.
Наиболее скептичный читатель может отметить, что меньшая свобода имеет и обратную сторону медали — это меньшая гибкость и отсутствие тонкого контроля над работой фреймворка. Что ж, и для таких нужд у Vue есть инструменты. Вы можете в отдельных местах отказаться от синтаксиса шаблонов и использовать render-функции (аналог ReactDOM.render) или вообще писать на JSX. Теоретически, так можно написать вообще весь проект, но очевидно, что тогда Вы теряете все преимущества HTML-шаблонов Vue. Даже в документации сказано, что это рекомендуется применять в отдельных случаях, когда Вам нужен более тонкий контроль над реактивностью.
Сказанное выше не единственное преимущество семантичности Vue.
Давайте вспомним примеры с useEffect’ом из предыдущего блока про семантику. Что же здесь предлагает Vue?
Хотите запускать сайд-эффект при изменении какого-либо одного реактивного состояния? Или двух состояний? Или Вы попали в ситуацию, когда Вам нужен прямо таки аналог useEffect — во Vue это всё красиво оформлено в виде вотчеров.
Хотите управлять жизненным циклом? На каждый этап жизненного цикла есть свой хук.
А теперь сравните такой код с группой из нескольких useEffect’ов из блока про семантику React. Согласитесь, намного лучше ведь, когда инструменты фреймворка позволяют Вам более явно выражать свои намерения?
Так, на примере работы с сайд-эффектами и жизненным циклом, мы видим большое семантическое преимущество у Vue. Опять же, этот пример не единственный, но наиболее показательный.
И React, и Vue оба используют паттерн Virtual DOM для обновления состояния приложения, но, несмотря на это, с производительностью у Vue дела обстоят получше.
Vue добивается этого за счёт того, что в отличие от React имеет этап компиляции. И на этапе компиляции, когда можно статически проанализировать все компоненты, Vue применяет ряд умных оптимизаций к итоговому коду, который даёт механизму сравнения двух Virtual DOM-деревьев примерную информацию о том, что могло измениться на странице. Как результат, процесс сравнения деревьев перед итоговой перерисовкой уже не сравнивает “вслепую” два огромных дерева полностью, а имеет некий контекст касательно того, какие части дерева ему нужно сравнивать, а какие — нет.
Как результат, вместо бездумного перевызова изменившегося компонента и всех его потомков, а также не менее бездумного полного сравнения двух VDOM-деревьев имеем механизм, благодаря которому компоненты не перевызываются без реальной на то необходимости, а вычисления при рендеринге тратят намного меньше ресурсов.
У этого есть и другой приятный бонус — не нужно пользоваться мемоизацией! Это, в свою очередь, хорошо сказывается на потреблении памяти Вашим приложением и вдобавок, отсылая нас к разделу про семантику, избавляет разработчика от необходимости писать весь этот бойлерплейт.
Конечно, это всё ещё не такой продвинутый и эффективный механизм как в Svelte, но команда Vue работает над этим.
Можно смело утверждать, что если разработчик сам не пишет в коде излишне частых обновлений множества состояний — проблем с производительностью на стороне Vue у Вас, скорее всего, не будет.
Vue по-умолчанию имеет возможность писать стили прямо в компонентах и использовать реактивные состояния в CSS через директиву v-bind. Инкапсуляция стилей, препроцессоры и другие любимые многими плюшки также доступны и работают без дополнительных настроек. Любителям Styled-Components и схожих библиотек должно понравиться. Нам, вот, понравилось. И никаких лишних обёрток, и читаемость дерева компонентов в инструментах разработчика не страдает.
Думаю, Вы уже догадались из раздела про стили во Vue, что в Nuxt CSS-in-JS (хотя правильнее будет сказать, что во Vue у нас JS-in-CSS) и серверные компоненты дружат намного лучше за счёт того, что во Vue этот функционал реализован нативно. Речь идёт про полностью серверные компоненты, которые не поставляют свой JS в браузер. В Nuxt они реализуются через суффикс .server, либо через компонент NuxtIsland.
Аналогично NextJS Nuxt поддерживает генерацию статических сайтов. При таком подходе все компоненты становятся серверными по умолчанию. При тестировании данной фичи со стилями, вычисляемыми в компоненте через пропсы и v-bind, проблем у меня так же не возникло.
Вдобавок, Nuxt — это официальная часть экосистемы Vue, а не сторонняя разработка, как NextJS для React. К слову, это ещё и даёт разработчику больше уверенности в том, что совместимость между ними будет максимальная.
В то же время, сторонние ui-библиотеки с полностью серверными компонентами в Nuxt тоже вполне себе дружат. Хоть и следует отметить, что иногда библиотека всё же будет грузить в браузер какой-то свой общий JS, конкретно сами компоненты (те, что не требуют клиентского JS) рисуются полностью на сервере.
Есть очень хорошее утверждение, что если Вы можете назвать только плюсы тех инструментов, которые используете, скорее всего, Вы не очень хорошо их освоили. Мы тщательно подошли к изучению не только плюсов, но и минусов нашего перехода:
Описанные выше преимущества Vue/Nuxt показались нам привлекательными. Мы увидели в них возможность разрабатывать веб-приложения быстрее и качественнее, больше концентрируясь на решении бизнес-задач и меньше на причудах фреймвока.
Также мы здесь увидели интересную возможность ещё раз проверить теорию, что переход на другой фреймворк — это не такая уж и сложная задача для разработчиков, которые хорошо владеют как абстрактными знаниями в программировании, так и углублёнными знаниями языка, на котором они работают.
При том, что Vue/Nuxt безусловно имеют свои недостатки, сейчас, когда мы только собираемся писать на них новые проекты, эти недостатки кажутся нам тем, с чем можно мириться, если Вы ищете универсальный инструмент для разработки как маленьких, так и больших проектов.
Насколько правильный выбор мы сделали и не пожалеем ли мы потом — покажет время. Возможно, через год здесь будет статья о том, что, мы всё же убедились, что можно найти компромисс между удобством и популярностью фреймворка. А, возможно, нет :)
*Изображения в статье взяты из открытых источников.