1000 и 1 способ сломать DevEx React — или почему я выбираю Svelte
- суббота, 20 декабря 2025 г. в 00:00:06
React — самый популярный фреймворк среди фронтенд‑разработчиков. Его подходы к разработке приложений часто воспринимаются как единственные правильные. Но что, если такие «стандарты» — это не необходимость, а вредная привычка, ломающая Developer Experience?
В статье разберем типичные проблемы Developer Experience в React: избыточный бойлерплейт, сложность управления состоянием и неочевидные оптимизации производительности. Покажем, как эти же задачи решаются в Svelte и обсудим, как смена фреймворка может упростить разработку.
Статья будет полезна разработчикам, которые хотят расширить свой кругозор и критически переосмыслить привычные подходы к фронтенд‑разработке.
Безусловно, React справляется с задачами фронтенд‑фреймворка. С помощью него можно создавать реактивные веб‑приложения и поддерживать все современные стандарты. Бизнес до сих пор выбирает React и разрабатывает рабочие сервисы.
Однако, помимо предоставляемых возможностей, в оценке фреймворка важно учитывать и другие факторы. Например, Developer Experience (или DevEx). Он отражает скорость входа в проект, предсказуемость поведения кода и потраченное время от идеи до работающего прототипа.
Для оценки DevEx предлагаю использовать критерии:
1. Простота — усилия для освоения технологии. Будет оцениваться исходя из близости инструмента к JS/HTML и количестве базовых механизмов, требующих изучения;
2. Удобство — усилия для решения задач. Определяется объемом служебного кода по отношению к полезному, для решения задачи и когнитивной нагрузкой при написании.
Фокус внимания будет направлен на отдельные концепции React:
Синтаксис разметки — JSX
Механизм рендеринга проекта — Virtual DOM
Эти концепции выбраны не случайно. Они составляют архитектурную основу React и напрямую влияют на все три метрики DevEx.
Недостаточно просто перечислить плюсы и минусы React. Важно доказать, что проблемы находят решение в других фреймворках. Для этого обзор концепции React будет сопровождаться анализом подобных механизмов в Svelte.

Каждый разработчик, хотя бы раз запускавший проект на React, знает о преимуществах JSX. Тем не менее, следует выделить главное — гибкость. Мы можем применять все возможности JS для работы с элементами разметки (!).
Например, можно собрать шаблон письма из функций, разместив их в массиве:
const greetings = (name) => <h1>Привет, {name}!</h1>
const mainPart = (text) => <p>{text}</p>
const farewell = (name) => <p>С уважением, {name}</p>
const letterParts = [greetings, mainPart, farewell]
Если захотим — сможем дополнительно обработать наши JSX‑переменные: обернуть их в декоратор и сделать компонентом высшего порядка или назначить какие либо пропсы.
const propsMap = {
0: { name: 'Хабр' },
1: { text: 'Рад сообщить о написании новой статьи!' },
2: { name: 'Григорий Деревянных' }
}
const Letter = () => (
<article>
{letterParts.map((Part, index) => (
<Part key={index} {...propsMap[index]} />
))}
</article>
)
Такая гибкость особенно ценна в ряде сценариев. Например, для генерации компонентов по конфигу с сервера, создании сложных HOC‑компонентов и интеграции с внешними библиотеками
JSX раскрывает свой потенциал в типизации. Компонент в React — обычная функция, а это значит, что к нему применимы все возможности TypeScript. Особенно ярко это проявляется в работе с дженериками:
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map(renderItem)}</ul>;
}
<List
items={[{ id: 0, name: 'Letter' }, { id: 1, name: 'Audio' }]}
renderItem={item => <li key={item.id}>{item.name}</li>}
/>
Спектр всех возможных действий с компонентом не ограничен, и в этом кроется сила JSX. Не каждый фреймворк способен на такое. Впрочем, обо всем по порядку.
Следует упомянуть и о другом важном преимуществе. React не навязывает нам паттерны разделения кода на компоненты и модули. Мы получаем полную свободу в построении архитектуры проекта. Можем, например, реализовать несколько компонентов в одном файле, как показано ниже:
function Button({ label }) {
return <button>{label}</button>
}
function Form() {
return (
<form>
<p>Сохранить форму?</p>
<Button label="Сохранить" />
</form>
)
}
С другой стороны, никто не запрещает исходить из концепции «1 файл = 1 компонент» или вообще начать подражать Angular и раздробить один компонент на несколько файлов.
Итак, React предоставляет гибкий синтаксис и свободу организации компонентов. Все, что умеет JS, — доступно для работы с UI. Архитектура проекта остается на усмотрении команды. Казалось бы, это естественная среда для фронтенд‑разработчика. Однако такая близость к JS‑синтаксису порождает ряд неочевидных проблем. Настало время поговорить о минусах.
Первая группа недостатков связана с синтаксисом разметки. Для того, чтобы описать простейшие классы элемента, нужно напечатать доллар, четыре скобки и шесть кавычек:
const isAdmin = true
<Button className={`button ${isAdmin ? 'button_active' : 'button_disabled'}`} />Ситуация усугубляется при работе со сложными компонентами. Представьте карточку товара, в которой нужно учитывать несколько состояний. Строка классов превращается в многострочную конструкцию, в которой легко потерять закрывающий символ. Примерно так это выглядит:
<div className={`
card
${status === 'available' ? 'card_available' : ''}
${status === 'low-stock' ? 'card_low-stock' : ''}
${status === 'out-of-stock' ? 'card_out-of-stock' : ''}
${category === 'premium' ? 'card_premium' : ''}
${hasDiscount ? 'card_discounted' : ''}
${isFavorite ? 'card_favorite' : ''}
`}
>
<img src="product.jpg" className="card__image" alt="Товар" />
<h3>Товар</h3>
<button>В корзину</button>
</div>Другая проблема — ненативный синтаксис. Концепция написания HTML в JS приводит к пересечению зарезервированных слов. React вынужден придумывать новые имена для стандартных HTML‑атрибутов.
Вместо привычного class приходится писать className. То же самое касается и атрибута for, который превращается в htmlFor. Проблема затрагивает и атрибуты, написанные в kebab‑case. Например, stroke‑width превращается в strokeWidth.
Самое интересное — aria‑атрибуты, которые также прописаны в kebab‑case могут быть использованы в JSX без изменений!
Кроме того, inline‑стили в JSX работают не как в HTML. Вместо строки придется передать объект. Такая «особенность» превращает простую запись в громоздкую конструкцию, которая визуально перегружает код.
<Combobox className="combobox" style={{ width: '10px' }} key={i} />Все перечисленное — не просто вопрос предпочтений. Ведь на практике возникают большие трудности при работе с HTML. Миграция ванильного кода на React, работа с HTML‑разметкой электронных писем, интеграция готовых HTML‑шаблонов превратятся в головную боль или же станут черной дырой, поглощающей токены.
Следующий недостаток JSX — это условный рендеринг. Для того, чтобы описать простую логику отображения элементов, приходится создавать несколько уровней вложенности:
<div>
{hasPermission ? (
isLoading ? (
<Spinner />
) : error ? (
<Error message={error} />
) : data.length > 0 ? (
<Table data={data} />
) : (
<EmptyState />
)
) : (
<AccessDenied />
)}
</div>
Декомпозиция и вынос условий в отдельные компоненты решает проблему. Но насколько это удобно?
Вопрос риторический. Предлагаю вернуться к метрикам DevEx и оценить, как JSX влияет на них.
Во‑первых, JSX прост. «Порог вхождения» достаточно низкий: новичку нужно запомнить несколько условностей и привыкнуть писать «className» вместо «class». Остальное — дело привычки.
Во‑вторых, синтаксис разметки снижает удобство написания кода. Из‑за визуального усложнения, в коде труднее ориентироваться. Иногда особенности JSX могут заставить разработчика совершать лишние действия: декомпозировать компонент из‑за условий, искать наименование HTML‑атрибутов в React. Если нужно работать с HTML‑разметкой, задача может сильно разрастись.
В отличие от React, некоторые фреймворки диктуют свои правила организации компонентов. Так, в Svelte и Vue используется концепция «1 файл = 1 компонент» (Single File Component — SFC).
Обычно это выглядит следующим образом. Файл разбивается на 3 секции:
Скрипт
Шаблон
Стили
<script>
let name = "мир";
</script>
<h1>Привет, {name}!</h1>
<style>
h1 {
color: tomato;
}
</style>
Такой подход позволяет разграничить «область видимости» между JS, HTML и CSS. Соответственно, исчезает проблема пересечения зарезервированных имен: и в HTML, и в JS можно использовать нативный синтаксис. Кроме того, открываются возможности добавления «синтаксического сахара» в разметку.
Нативный синтаксис
Svelte использует привычный HTML‑подобный синтаксис: классы, строковые стили, шаблонные выражения прямо в разметке.
<script>
let isAdmin = true
</script>
<p class="w-50 h-50 {isAdmin ? 'text-xl' : 'text-sm'}">
Настройки проекта
</p>
У данного подхода есть очевидные преимущества. Мы не обязаны использовать фигурные скобки и форматированную строку для применения JS‑выражения. Меньше закрывающих символов => удобнее описывать классы.
Если же вы привыкли к JSX, то с этим проблем не будет. Svelte позволяет писать классы и в таком стиле.
Синтаксический сахар
Svelte вводит специальные конструкции, которые облегчают работу с разметкой. Они обычно начинаются со знака «#» и ключевого слова. На первый взгляд может показаться, что такой код ненативный.
Действительно, едва ли можно представить что‑то подобное в HTML.
Я не считаю синтаксический сахар минусом, так как он привносит новые фичи в разметку и помогает эффективно решать задачи.
Рендер списка. Назначаем переменную‑итератор и записываем ключ рендера в скобки. Заметьте, индексирование списка не требует использования зарезервированного пропса.
<script>
const users = ['Сергей', 'Степан']
</script>
{#each users as userName, index (userName + index)}
<p>Имя: {userName}</p>
{/each}
Работа с промисами. Никаких промежуточных переменных состояния, лестницы условий и микро‑компонентов.
<script>
const promise = fetch(...)
</script>
{#await promise}
<p>Ждём выполнение…</p>
{:then value}
<p>Результат: {value}</p>
{:catch error}
<p>Ошибка: {error.message}</p>
{/await}
Директивы для описания классов. Скажем «нет» огромным форматированным строкам с тернарными операторами! Вот, во что превратился рассматриваемый ранее компонент карточки товара.
<div
class="card"
class:card_available={status === 'available'}
class:card_low-stock={status === 'low-stock'}
class:card_out-of-stock={status === 'out-of-stock'}
class:card_premium={category === 'premium'}
class:card_discounted={hasDiscount}
class:card_favorite={isFavorite}
>
<img src="product.jpg" class="card__image" />
<h3>Товар</h3>
<button>В корзину</button>
</div>Работа с условиями
Вместо громоздких выражений в JSX, в Svelte условия читаются сверху вниз, с одним уровнем вложенности. Такой синтаксис ближе к естественному языку, и это одна из причин, почему Svelte воспринимается проще и понятнее.
<script>
const count = 0;
</script>
{#if count === 0}
<p>Ноль</p>
{:else if count % 2 === 0}
<p>Чётное</p>
{:else}
<p>Нечётное</p>
{/if}
Сниппеты — почти JSX
В Svelte 5 добавили сниппеты — куски кода, которые можно передавать в шаблон и рендерить с аргументами. Они упрощают структуру, но пока остаются менее гибкими, чем JSX.
Помните, как мы складывали JSX‑переменные в массив? Попробуем сделать нечто подобное.
Вот так объявляются сниппеты:
<script>
// Использование сниппетов в JS
const letterParts = [greetings, mainPart, farewell]
const args = [
['Хабр'],
['Я очень рад, что вы читаете эту статью'],
['Григорий'],
]
</script>
// Объявление "сниппетов"
{#snippet greetings(name)}
<h1>Привет, {name}!</h1>
{/snippet}
{#snippet mainPart(text)}
<p>{text}</p>
{/snippet}
{#snippet farewell(name)}
<p>С уважением, {name}</p>
{/snippet}
// Рендер сниппетов
{#each letterParts as Part, i}
{@render Part(args[i])}
{/each}
Интересный вопрос — можно ли сказать, что сниппеты отодвинули Svelte от использования «чистого» подхода «1 файл = 1 компонент»? Ведь теоретически в одном файле можно сгенерировать несколько сниппетов и экспортировать их
Синтаксис разметки Svelte имеет свои недостатки. Следует начать с самого главного — у нас нет такой свободы, которую предоставляет JSX. Мы сильно ограничены в числе операций со сниппетами. Хоть они похожи на обычную JS‑функцию, работа с ними требует соблюдения многих условностей
Например, нельзя использовать spread‑синтаксис внутри «@render» блока. Такое выражение выдаст ошибку:
<script>
let { children } = $props
const args = ["Mad Frontend"]
</script>
{@render children ?.(...args)}
Мы также не можем отрендерить массив сниппетов вместе с пропсами, как в примере ниже. Придется держать 2 массива и устанавливать соответствие индекса сниппета и аргумента.
Работа с вложенными элементами:
<script>
const letterParts = [greetings, mainPart, farewell]
const args = ['Хабр', 'С Новым годом :)', 'Всего доброго!']
const snippets = args.map((arg, i) => {
return () => letterParts[i](arg)
})
</script>
{#snippet greetings(name)}
<h1>Привет, {name} !< /h1>
{/snippet}
{#snippet mainPart(text)}
<p>{text}</p>
{/snippet}
{#snippet farewell(name)}
<p>С уважением, {name}</p>
{/snippet}
// так не работает
{#each snippets as snippet, i}
{@render snippet}
{/each}
// так работает
{#each letterParts as snippet, i}
{@render snippet(args[i])}
{/each}
Типизация
Svelte 5 полностью поддерживает типизацию как в секции кода, так и в разметке. Однако для справедливой оценки необходимо затронуть объявление дженериков в компонентах. В отличие от React, Svelte‑компонент не является функцией и нативно задать дженерик не получится. Поэтому дженерики в Svelte указываются как атрибуты, строкой.
<script lang="ts" generics="T">
interface Props {
items: T[];
onSelect: (item: T) => void;
getLabel: (item: T) => string;
}
let { items, onSelect, getLabel }: Props = $props();
</script>
<ul>
{#each items as item}
<li onclick={() => onSelect(item)}>
{getLabel(item)}
</li>
{/each}
</ul>
Едва ли такую реализацию можно назвать удобной.
Специфичные концепции
С одной стороны, «синтаксический сахар» — это хорошо. С другой, он повышает порог входа в проект. Svelte насчитывает 18 таких конструкций, 7 рун и 7 специальных директив. На старте это может сбивать с толку. На самом деле большая часть из всего этого является факультативными фичами. К примеру, вы можете организовать обработку промисов обычными средствами реактивности. Здесь не обязательно использовать конструкцию {#promise}.
Тем не менее, синтаксические конструкции Svelte зачастую далеки от нативного HTML/CSS/JS. Для запоминания потребуется время и немного практики.
Подводя итоги, надо отметить, что Svelte предоставляет более удобный синтаксис разметки, чем JSX. Нативный HTML, интуитивная работа с условиями и циклами, директивы для классов — все это ускоряет разработку и улучшает читаемость кода. Однако за это приходится платить ограниченной гибкостью: сниппеты не дают той свободы манипуляций, которую обеспечивает JSX, а специфичные концепции вроде рун требуют времени на изучение.
Простота | Удобство | |
|---|---|---|
JSX | Очень просто | Неудобно |
Svelte | Немного сложно | Удобно |
В этой секции мы доберемся до сердца фронтенд‑фреймворка — механизма реактивности. React использует концепцию Virtual DOM для декларативной отрисовки изменений в UI.
Virtual DOM — это легковесная копия DOM. Когда состояние компонента меняется (state, props, context), React сначала обновляет Virtual DOM, затем сравнивает его с предыдущей версией, вычисляет набор изменений и применяет их к реальному DOM. И все это в рантайме!

Идея хорошая, да и на схеме все выглядит красиво. Однако концептуально нерешенной остается одна проблема. Когда React видит, что измененная нода имеет дочерние элементы, он не может знать, как целесообразнее поступить с этими элементами:
Запустить сравнение дочерних элементов и отрендерить только измененные? А если дочерних элементов слишком много?
Записать дочерние ноды из памяти? А вдруг там неактуальное состояние?
Перерндерить все?
По умолчанию React будет делать ре‑рендер измененной ноды и всех ее дочерних элементов…

Чтобы избежать лишних ре‑рендеров, нужно вручную оборачивать компоненты в memo, функции — в useCallback, а вычисления — в useMemo. Из‑за этого код быстро обрастает хуками оптимизации:
const ComponentMemo = React.memo(VerySlowComponent)
export const SimpleCase2Memo = () => {
const [isOpen, setIsOpen] = useState(false)
const onSubmit = useCallback(() => {}, []);
const data = useMemo(() => [{ id: 'bla' }], [])
return (
<div>
<ComponentMemo onSubmit={onSubmit} data={data} />
</div>
)
}
React Compiler призван облегчить жизнь разработчиков и взять оптимизацию на себя. Он анализирует код на этапе сборки и мемоизирует все, что может. Тем не менее, у этого решения есть подводные камни.
Для использования компилятора придется следовать определенным правилам написания кода (Rules of React). Иначе компилятор не станет мемоизировать компонент или сломает логику его работы. Далее, сгенерированный код отличается от исходного. Из‑за этого могут возникать сложности в отладке. Наконец, концептуальная проблема React Compiler — агрессивный алгоритм мемоизации. В некоторых случаях нам не надо мемоизировать компонент. Тогда нам придется сделать обратное — написать ключевое слово для того, чтобы код не компилировался.
React Compiler вводит много условностей, которые придется соблюдать. Любые условности мешают DevEx. Всегда проще и удобнее писать код без дополнительных ограничений.
Virtual DOM также не позволяет применять некоторые методы напрямую к DOM. Например, нельзя использовать innerHTML, appendChild, textContent,removeChild insertBefore, replaceChild.
Такое ограничение логично: если мы изменим структуру DOM напрямую, то возникает рассинхрон между DOM и Virtual DOM. React при следующем рендере будет опираться на устаревшее представление, что приведет к непредсказуемому поведению — от потери изменений до ошибок.
Для того, чтобы реализовать портал, приходится импортировать специальную функцию и создавать дополнительный компонент:
import { createPortal } from "react-dom"
function Portal({ children }) {
return createPortal(children, document.body)
}
function Component() {
return (
<Portal>
<p>Содержимое портала здесь</p>
</Portal>
)
}
Итак, Virtual DOM делает React мощным, но сложным. Для понимания, как он работает, нужно помнить десятки нюансов и следить за тем, когда ре‑рендер происходит и почему.
Все это бьет по Developer Experience. Вместо того, чтобы сосредоточиться на продукте, мы вынуждены решать React‑специфичные задачи. React требует от разработчиков думать на двух уровнях одновременно: о бизнес‑логике и о том, как она будет проинтерпретирована фреймворком. Это накапливающаяся когнитивная нагрузка. Новичкам будем сложно войти в проект, а опытным — объяснить, почему «простой» компонент ведет себя неочевидно. Возвращаясь к метрикам, Virtual DOM — это сложно и неудобно.
В Svelte нет Virtual DOM. Совсем.
Здесь работает компилятор, который анализирует, какие части DOM зависят от реактивных переменных, и генерирует код, который обновляет DOM напрямую. При изменении значения обновляются только связанные с ним узлы. То есть нам не нужно пересобирать дерево и вычислять отличия — обновляется конкретный узел в DOM.
В Svelte | ||
Обновляются только нужные элементы | Не требуется мемоизация и хуки оптимизации | Любые операции с DOM доступны напрямую |
Например, если меняется текст или атрибут, Svelte не делает полный ререндер, а просто вызывает:
element.textContent = newValue;
element.setAttribute('disabled', true);
Таким образом, компонент, который уже смонтирован, не монтируется заново, а получает обновление.
В Svelte нет тех, кто «борется с ререндерами» — просто потому что никаких реренеров нет. Не нужно оборачивать функции в useCallback и вычисления в useMemo. Код остается коротким и прозрачным.
Стоит оговориться, что в Svelte могут возникать проблемы с синхронизацией состояний. Например, если вы в одной функции изменяете переменную, которая является зависимостью для «производного стейта», а затем оперируете обновленным значением этого «производного стейта». По умолчанию, перерасчет произойдет уже после завершения работы функции. То есть вы фактически получите старое значение «производного стейта». Но и эта проблема лечится благодаря:
Смене способа взаимодействия состояний. Одна функция изменяет стейт. Другая функция подписывается на перерасчеты «производного стейта» и выполняет небходимую логику;
Использованию функции await tick(), которая дожидается выполнения всех сторонних эффектов.
Svelte не запрещает прямую работу с DOM. Можно использовать innerHTML, appendChild, textContent — никаких ограничений. То, что в React требовало дополнительных прослоек, здесь делается напрямую.
Например, портал:
function portal(node) {
document.body.appendChild(node)
}
<div use:portal>Содержимое портала</div>
Никаких createPortal и HOC‑компонентов — достаточно одной функции, которую можно записать в утилиту и забыть.
Этот фреймворк проще и предсказуемее. Он не заставляет думать о ре‑рендерах и оптимизации реактивности, а позволяет сосредоточиться на логике приложения.
Простота | Удобство | |
|---|---|---|
React | Сложно | Неудобно |
Svelte | Очень просто | Удобно |
React — это мощный инструмент разработки, и он справляется со своими задачами. Но «справляется» — не значит, что позволяет вести разработку быстро и удобно. JSX заставляет писать лишний код и бороться с синтаксисом. Virtual DOM требует думать о ре‑рендерах вместо бизнес‑логики. Все это накапливается в когнитивную нагрузку, которая замедляет разработку.
Svelte решает эти проблемы: нативный синтаксис, точечные обновления DOM, минимум служебного кода. Этот фреймворк доказывает, что реактивный UI можно строить без Virtual DOM, хуков оптимизации и лишнего бойлерплейта.
React не сломан, но Developer Experience в нем — сломан давно. Возможно, пора перестать воспринимать сложности React как норму и начать требовать от инструментов большего.