Vue.js, Angular, Svelte, Solid, Qwik
Fluent React: Build Fast, Performant, and Intuitive Web Applications
Глубокое погружение во внутреннее устройство React: JSX и продвинутые паттерны, виртуальный DOM и реконциляция, серверный рендеринг, конкурентный режим и серверные компоненты.
Программа вечера
Антон Помазков
Артём Никифоров
Артём Никифоров
Артём Никифоров
Что такое реактивность?
Способность системы обновляться сама в ответ на изменение данных.
Электронная таблица
Меняем A1 — и все формулы, которые на неё ссылаются, пересчитываются сами. Причём каскадом: B3 зависит не от A1 напрямую, а от B1 и B2.
Никто не вызывает «пересчитай таблицу» руками — связи между ячейками описаны один раз, дальше система следит сама. В веб-фреймворках реактивность решает ту же задачу — связывает данные и интерфейс.
Как реактивность решает React
- useState состояние компонента и функция, которая просит перерисоватьофициальная документация ↗
- Virtual DOM описание интерфейса в памяти — чтобы было с чем сравниватьофициальная документация ↗
- Reconciliation сверка двух деревьев: ищем минимальное изменение для DOMофициальная документация ↗
import { useState } from 'react'; function Counter() { const [count, setCount] = useState(0);console.log('Компонент перерендеривается!');return ( <div><p>Счёт: {count}</p><button onClick={() => setCount(count + 1)}> Увеличить </button></div> ); }
React пересоздаёт всё виртуальное дерево компонента, даже если изменилась одна цифра: единица обновления — компонент, а не значение.
Развитие реактивности в React
Что окружает эту модель — и как она меняется к 2026 году.
Хуки
Состояние и эффекты
Всё, что компонент помнит и делает помимо разметки, объявляется вызовом функции прямо в его теле.
- useState — состояние компонента
- useEffect — побочные эффекты: подписки, запросы, таймеры
- useMemo, useCallback — мемоизация: не пересчитывать то, что не изменилось
React 18+
Конкурентный рендеринг
Рендеринг можно прервать ради более приоритетной задачи: ввод в поле не ждёт, пока отрисуется тяжёлый список.
- useTransition, startTransition — помечают обновление неспешным
- useDeferredValue — показывает прежнее значение, пока считается новое
- Suspense — запасной интерфейс, пока грузятся данные или код
React 19+ · 2026
Компилятор
Мемоизацию берёт на себя сборка: модель прежняя, ручной работы меньше.
- React Compiler — расставляет мемоизацию сам
- babel-plugin-react-compiler — подключение к сборке
- eslint-plugin-react-hooks — показывает код, который компилятор не смог оптимизировать
Vue.js
Прогрессивный JavaScript-фреймворк для создания интерфейсов. Эван Ю работал в Google над AngularJS: Vue вдохновлялся Angular, но хотел быть легче и проще.
Vue 2: реактивность через Object.defineProperty
Каждое свойство объекта превращается в пару геттер/сеттер: чтение записывает зависимость, запись уведомляет об изменении.
- Object.defineProperty подменяет свойство парой геттер/сеттерофициальная документация ↗
- track на чтении запоминает, кто зависит от свойстваофициальная документация ↗
- trigger на записи уведомляет всех, кто запомнилсяофициальная документация ↗
// Упрощённая реализация Vue 2 function defineReactive(obj, key, val) { Object.defineProperty(obj, key, {get() { track(key); // отслеживаем, что свойство прочитано return val; },set(newVal) { if (newVal !== val) { val = newVal; trigger(key); // уведомляем об изменении } }}); }const data = { count: 0 }; defineReactive(data, 'count', 0);
Точный, но заранее размеченный
Vue 2 знает про зависимости больше React: обновляется не компонент, а конкретные эффекты. Но перехват ставится по одному свойству — и всё, что не разметили заранее, выпадает из реактивности.
Vue 2: где ломается
Реактивным становится только то, что размечено при инициализации: каждое свойство обёрнуто в «наручники» геттера и сеттера.
data
Vue разметил 3 свойства: у каждого свой геттер и сеттер.
Vue 3: реактивность через Proxy
Object.defineProperty заменили на ES6 Proxy: он оборачивает объект целиком и перехватывает любые операции — чтение, запись, добавление, удаление.
- Proxy посредник, который стоит перед объектом целикомофициальная документация ↗
- track на чтении подписывает того, кто читаетофициальная документация ↗
- trigger на записи уведомляет подписавшихсяофициальная документация ↗
// Упрощённая реализация Vue 3 function reactive(obj) {return new Proxy(obj, {get(target, key) { track(target, key); // отслеживаем зависимость return target[key]; },set(target, key, value) { target[key] = value; trigger(target, key); // уведомляем об изменении return true; }}); } const state = reactive({ count: 0 }); state.count = 1; // работает! state.newProp = 'hello'; // тоже работает — Proxy перехватывает
Что дал Proxy
Отслеживаются добавление и удаление свойств, массивы работают без хаков, производительность выше — обёртка ставится одна на объект, а не по одной на каждое свойство.
Цена — Proxy есть только в ES6+: поддержку IE11 Vue 3 потерял.
Proxy перехватывает всё
Одна и та же операция над состоянием — и то, что о ней узнаёт Vue 3 с Proxy и Vue 2 с геттерами и сеттерами.
—
Ловушка одна на все операции с объектом
—
Реактивны только свойства, размеченные при инициализации
Vue 2 размечал свойства по одному — новых, удалённых и записанных по индексу он просто не видел. Proxy стоит перед объектом целиком, поэтому Vue.set() и Vue.delete() больше не нужны.
Vue 2026 · реактивность
Слой «кто от кого зависит» переписан на библиотеку Alien Signals. Публичный API не изменился ни на строку — меняется только то, как Vue внутри проводит изменение по графу зависимостей.
- Alien Signals движок графа: связи двусвязным списком вместо коллекцийофициальная документация ↗
- ref, computed, watch публичный API — он не меняется ни на строкуофициальная документация ↗
- push-pull запись помечает зависимых, чтение пересчитываетофициальная документация ↗
<script setup> import { ref, computed, watch } from 'vue' // источник — обычный ref const count = ref(1) // производные — считаются от него const doubled = computed(() => count.value * 2) const total = computed(() => doubled.value + 10) watch(total, v => console.log(v)) </script> <template> <p>{{ total }}</p> </template>
Этот файл в Vue 3.6 не меняется. Alien Signals живёт под ref и computed, а не вместо них.
Vue 2026 · рендеринг
Vapor Mode — вторая половина: вместо виртуального дерева компилятор сразу пишет код, который меняет нужный узел DOM. Включается атрибутом vapor в теге <script setup>.
- Vapor Mode режим компиляции без виртуального DOMофициальная документация ↗
- компилятор шаблонов связывает выражение с конкретным узлом DOMофициальная документация ↗
Как было
Vapor Mode
<script setup vapor>import { ref, computed } from 'vue'const count = ref(0) const doubled = computed(() => count.value * 2)</script> <template><p>{{ count }} × 2 = {{ doubled }}</p><button @click="count++">Увеличить</button></template>
Компонент без виртуального дерева
Обычный компонент Vue на каждое изменение строит новое виртуальное дерево и сравнивает его с прошлым. В Vapor Mode компилятор заранее знает, какое выражение живёт в каком узле, и генерирует прямое присваивание.
Angular
Платформа для разработки веб-приложений, написанная на TypeScript. Первую версию Мишко Хевери написал в Google. Запомните имя: в конце доклада оно встретится снова.
Angular: обнаружение изменений на Zone.js
Angular не следит за данными. Он держит список привязок шаблона, а Zone.js подсказывает момент, когда их пора перепроверить.
- Привязка (binding) заводится на каждое выражение шаблона и помнит прошлое значениеофициальная документация ↗
- Zone.js патчит асинхронные операции и сообщает, когда проверятьофициальная документация ↗
- Change Detector у каждого компонента свой — сверяет свои привязкиофициальная документация ↗
@Component({ selector: 'app-counter', template: `<p>{{ count }}</p><button (click)="increment()">+1</button>` }) export class CounterComponent {count = 0;increment() {this.count++;} }
Angular 2026
Сигнал сам знает, кто его читает. Значит, спрашивать «а вдруг что-то изменилось» больше не надо — и Zone.js становится не нужен.
- Signal API signal, computed, effect: значение со списком своих читателейофициальная документация ↗
- zoneless от Zone.js отказались — он не подключается вовсеофициальная документация ↗
- provideZonelessChangeDetection() этим режим и включается при старте приложенияофициальная документация ↗
import { Component, signal, computed } from '@angular/core'; @Component({ template: `<p>Счёт: {{ count() }}</p> <p>Удвоено: {{ doubled() }}</p>` }) export class CounterComponent {count = signal(0);doubled = computed(() => this.count() * 2);increment() { this.count.update(v => v + 1); }}// zoneless: Zone.js не подключается вовсе bootstrapApplication(App, { providers: [provideZonelessChangeDetection()] });
Откуда Angular знает, что обновлять
Список читателей ведёт само значение. Прочитали сигнал — записались в список; изменили — прошлись по списку. Дерево компонентов при этом не обходится ни разу.
Svelte
Не фреймворк, а компилятор: декларативные компоненты превращаются в эффективный императивный код на этапе сборки, а не интерпретируются в браузере.
Svelte 4: как работает $:
Svelte — компилятор. То, что другие фреймворки делают в браузере, он делает один раз при сборке: читает файл, находит присваивания и сам дописывает вызовы обновления.
- компилятор Svelte читает файл при сборке, а не следит за ним в браузереофициальная документация ↗
- $: метка реактивного выражения — зависимости берутся из текстаофициальная документация ↗
- $$invalidate вызов, который компилятор дописывает после присваиванияофициальная документация ↗
<script>let count = 0; let doubleCount = 0;// пересчитывается при изменении count $: doubleCount = count * 2;function increment() { count += 1; }</script><button on:click={increment}> Нажато {count} раз (удвоено: {doubleCount}) </button>
// сгенерировано компилятором function instance($$self, $$props, $$invalidate) {let count = 0; let doubleCount = 0;function increment() { $$invalidate(0, count += 1); }$$self.$$.update = () => { if ($$self.$$.dirty & /* count */ 1) { $$invalidate(1, doubleCount = count * 2); } };// и обновление конкретного текстового узла set_data(t1, /*count*/ ctx[0]);}
Svelte 4: где ломается $:
Всё держится на том, что компилятор видит присваивание в тексте файла. Стоит вынести его за границу .svelte — и дописывать $$invalidate становится некуда.
<script> let count = 0; $: doubleCount = count * 2; // присваивание здесь — компилятор его видит function increment() { count += 1; } </script>
<script> import { increment } from './utils.js'; let count = 0; $: doubleCount = count * 2; </script> // utils.js — обычный файл, компилятор Svelte его не трогает export function increment(state) { state.count += 1; // $$invalidate дописать некуда }
Код в .svelte: компилятор видит присваивание и связывает его с $:
Реактивность работает только внутри .svelte-файлов
Не видно присваивания — не видно и зависимости: arr.push() тоже не сработает
По коду не понять, что и когда обновляется: решение приняли за вас при сборке
Svelte 5: руны
Руна — явный вызов вместо неявного правила. Компилятор превращает её в сигнал, а зависимости собираются во время выполнения — по факту чтения, а не по тексту файла.
- $state, $derived, $effect явные вызовы вместо неявных правилофициальная документация ↗
- .svelte.ts обычный файл, который тоже проходит через компилятор рунофициальная документация ↗
- зависимости в рантайме подписка получается из самого факта чтенияофициальная документация ↗
let count = $state(0);let doubled = $derived(count * 2);$effect(() => { console.log('счёт:', count, 'удвоено:', doubled); });export function increment() { count++; } export function getCount() { return count; }
Что изменилось по сути
Зависимости больше не выводятся из текста файла. Их собирает сама программа во время работы: прочитал сигнал — попал к нему в читатели. Ровно та же модель, что у Solid и Vue 3.6.
Solid
Декларативная библиотека с мелкозернистой реактивностью. Синтаксис почти как у React, внутри — сигналы и компиляция в прямой DOM-код без виртуального DOM.
Solid: сигнал как пара [getter, setter]
Компонент выполняется один раз. Дальше живут только сигналы — и обновляют ровно те DOM-узлы, которые их читают.
- createSignal пара [getter, setter]: читать и писать — разные функцииофициальная документация ↗
- createMemo и createEffect производное значение и эффект — подписываются самиофициальная документация ↗
- компиляция JSX сразу в создание DOM-узлов, без виртуального дереваофициальная документация ↗
import { createSignal, createMemo, createEffect } from 'solid-js'; function Counter() {const [count, setCount] = createSignal(0);const doubled = createMemo(() => count() * 2);createEffect(() => { console.log('Счёт:', count()); });return ( <><div>{count()}</div> <div>{doubled()}</div><button onClick={() => setCount(c => c + 1)}> Увеличить </button></> ); }
React
Solid
Компонент — это функция настройки
В React функция компонента вызывается на каждое изменение. В Solid она вызывается один раз: её задача — создать узлы и подписки. Дальше работают только сигналы, а на частых точечных обновлениях это даёт 50–70% выигрыша.
Qwik
Фреймворк, построенный на возобновляемости (resumability): вместо гидратации приложение продолжает работу с того места, где остановился сервер. Автор — тот же, что придумал Angular.
Автор Angular второй раз переосмысляет реактивность — и на этот раз спорит не с обновлением DOM, а с самой загрузкой приложения.
Qwik: интерактивность без ожидания
Гидратация — это повторный запуск всего приложения в браузере ради того, что сервер уже посчитал. Qwik её не делает.
- component$ и onClick$ границы, по которым компилятор режет код на чанкиофициальная документация ↗
- сериализация состояния состояние и подписки уезжают в HTML вместе с разметкойофициальная документация ↗
- загрузчик ~1.6 КБ ловит событие и подгружает нужный чанк — больше ничегоофициальная документация ↗
<button on:click="q-a8f2.js#s_0[0]" q:id="3"> Счёт: 0 </button><script type="qwik/json"> {"objs": [0], "subs": [["3", "count"]]} </script><script> /* загрузчик Qwik, ~1.6 КБ */ </script>
React · SSR + гидратация
Qwik · возобновляемость
Как развивалась реактивность
Куда всё идёт · август 2026
За шесть лет на сигналы перешли все, кроме одного. Каждый пришёл своим путём и в свой год — но к одной и той же модели.
Перешли на сигналы
Пошёл другим путём
Реактивность перестала быть отличием фреймворков и стала общим местом. Соревнование переехало на время до интерактивности.
Что далее
Антон Помазков
Артём Никифоров
Артём Никифоров
Артём Никифоров