React — популярная библиотека JavaScript, которая заслуженно пользуется популярностью у фронтендеров. А вот Angular часто называют избыточно усложненным и даже отчасти устаревшим. Мне довелось поработать на реальных проектах и с тем, и с другим, каждый раз проходя путь от «да как на этом вообще можно работать» до «человечество не придумало ничего лучше».

Привет! Я Полина, фронтенд-разработчик в Selectel. В этой статье я решила залезть в темные уголки React и Angular, чтобы лично посмотреть, что же там происходит. Для чистоты эксперимента я выбрала шесть типовых несложных задач, для решения которых подойдет и фреймворк, и библиотека. Подробности под катом.

Почему я усомнилась в непогрешимости Angular

Во фронтенд я пришла со знанием React.js и вообще-то с ним и собиралась работать. Но меня посадили за Angular + TypeScript, которые пришлось изучать на ходу на реальном проекте. Все оказалось не так уж плохо, я даже прикипела к этому фреймворку. Потом перешла в Selectel, где до сих пор с удовольствием продолжаю писать код на Angular.

Спустя какое-то время я поймала себя на мысли, что когда смотрю на React, вижу спагетти-код. Он вызывает головную боль, отрицание, гнев, торг, депрессию (и только потом принятие). Однако это мое субъективное мнение, с которым я сама начинаю спорить, когда вижу хорошие проекты, написанные на React.

Ангел и демон на моих плечах борются за мнение о React.
Ангел и демон на моих плечах борются за мнение о React.

 

Но! За последний год мне пришлось-таки снова иметь дело с React. И не просто править или ревьюить чужой код, а написать с нуля полноценный портал SelectOS Manpages.

И пока я работала с React, в инспекторе заметила неожиданную для меня вещь — React не генерировал кастомных компонентов типа <my-button>. В Angular же я постоянно встречалась с подобными проявлениями и начала считать их нормой для любых фронтовых фреймворков.

И тут мне стало интересно — а почему так? Когда я увидела разный вид DOM, то решила: наверное, библиотека React как набор импортируемых функций просто работает ближе к ванильному JavaScript, и потому в DOM все выглядит иначе. Но это объяснение оказалось слишком поверхностным. Мне стало интересно разобраться глубже — в чем же на самом деле различие в подходах этих двух технологий к решению одной и той же задачи: обновлению UI.

Что такое фреймворк и библиотека?

Библиотека — набор готовых функций для решения конкретных задач. Для использования библиотеки достаточно импортировать из нее необходимые функции и дальше вызывать их в коде. Например, React отвечает за отрисовку UI. При этом роутинг, архитектуру, работу с сетевыми запросами разработчик продумывает сам или использует дополнительные библиотеки. Иногда React называют фреймворком из-за jsx-файлов, но под капотом файлы компонентов превращаются в вызовы JS-функций.

Фреймворк — готовая среда со встроенными инструментами. Если рассматривать Angular, он сразу содержит решения для роутинга, работы с запросами, предоставляет свою архитектуру приложения. Помимо этого, Angular компилируется через свой движок Ivy в инструкции на JS.

Что ж, пора заглянуть в потроха JavaScript.

Задача 1: как генерируется компонент в DOM

Рассмотрим простой шаблон. Переменная пусть задается внутри этого же компонента.

<h1>Hello {name}</h1>

React

В React этот компонент выглядит следующим образом:

function Hello() {
  const [name] = React.useState('React');
  return <h1>Hello {name}</h1>;
}

В React для обновлений DOM используется Virtual DOM, который создается заново и сравнивается с предыдущим при каждом рендере компонента. Посмотрим, как код компонента превращается в DOM.

Первый шаг — Initial Render. React вызывает функцию Hello() и получает JSX. Затем библиотека создает Fiber Node — объект, представляющий компонент в дереве. Внутри хранятся тип, props, ссылки на детей:

React.createElement(
  "h1",    // type - первый аргумент
  null,    // props - второй аргумент  
  "Hello ", // children - третий аргумент
  name     // children - продолжение третьего аргумента
)
Что такое Fiber Node?

Fiber — внутренняя структура данных, появившаяся в React 16. Каждый Fiber — это объект в памяти, который содержит:

  • type — какой это элемент (div, FunctionComponent, и т. п.);

  • pendingProps и memoizedProps — новые и старые пропсы;

  • child, sibling, return — ссылки на соседей и родителя в дереве;

  • stateNode — реальный DOM-узел (для HostComponent) или ссылка на экземпляр класса;

  • updateQueue – очередь апдейтов (setState);

  • флаги, говорящие, нужно ли этот узел перерисовать;

  • Virtual DOM — чистое описание интерфейса (<div>Hello</div>).

Fiber — «рабочая версия» этого описания, которая хранит состояние рендера и связи с реальным DOM.

Следующий этап — Reconciliation, или сверка. React строит дерево Virtual DOM и сравнивает его с предыдущей версией.

И наконец — Commit Phase. Для новых узлов React вызывает document.createElement("h1") и appendChild для изменений в существующих node.textContent = ….

Результат в DOM:

<h1>Hello React</h1>
React сравнивает VDOM и обновленный VDOM при создании компонента.
React сравнивает VDOM и обновленный VDOM при создании компонента.

Angular

В Angular этот же компонент будет выглядеть так:

@Component({
  selector: 'hello',
  template: `<h1>Hello {{ name() }}</h1>`,
  standalone: true
})

export class HelloComponent {
  name = signal('Angular');
}

В Angular используется Incremental DOM — вместо того, чтобы создавать промежуточный Virtual DOM, компоненты генерируются в набор инструкций для создания и изменения DOM-дерева на месте. Сперва Angular берет шаблон и генерирует функцию:

function HelloComponent_Template(rf, ctx) {
  if (rf & 1) {  // Если это фаза создания 
    ɵɵelementStart(0, "h1"); // Начинаем элемент h1 с индексом 0
    ɵɵtext(1); // Создаем текстовый узел с индексом 1 
    ɵɵelementEnd();   
  }
  if (rf & 2) { // Если это фаза обновления 
    ɵɵadvance(1); // Переходим к узлу с индексом 1 (текстовый узел) 
    ɵɵtextInterpolate1("Hello ", ctx.name(), ""); // Обновляем текст 
  }
}
Что за непонятные штучки?
  • rf — render flags, флаги, которые указывают фазу рендеринга. 1 — фаза создания DOM-структуры, 2 — фаза обновления данных.

  • ctx — контекст компонента, ссылка на экземпляр компонента, содержит все его свойства и методы.

  • Странные функции, начинающиеся с ɵɵ — внутренние функции Angular Ivy Runtime, движка рендеринга и компиляции Angular 9+. 

При первом создании (rf=1) Angular вызывает создание DOM-ноды h1 и тестового узла внутри нее. Фаза обновления (rf=2) триггерится сигналом name.set('Angular'), текстовый узел точечно обновляется без Virtual DOM.

После компиляции приложения происходит инициализация TView — структуры, которая содержит необходимую информацию о компоненте. Создается RootView (из условного AppComponent). Root Tree — первый узел TView. Для дочерних компонентов генерируются дочерние узлы ComponentView. Каждый узел — это ViewRef, содержащий в себе ссылки на свой AppComponent-класс, DOM-элементы и ViewRef дочерних компонентов.

Упрощенная схема TView до срабатывания отслеживания изменений.

Теперь происходит первое срабатывание Change Detection, Angular обходит дерево сверху вниз, вызывая update-инструкции:

После срабатывания инструкций и изменений.
После срабатывания инструкций и изменений.

При любых новых изменениях в компонентах Angular помечает конкретный View «dirty», Change Detection срабатывает только для этого поддерева.

Результат в DOM:

<h1>Hello Angular</h1>
Angular точечно применяет изменения в DOM.
Angular точечно применяет изменения в DOM.

Выводы

React:

  • хранит состояние в Virtual DOM, по нему рендерит и отрисовывает реальный DOM;

  • не знает напрямую, какую конкретную ноду DOM нужно поменять;

  • обновления вычисляются через сравнение двух версий (diff) Virtual DOM, до и после вызова функций компонентов;

  • компонент представляет собой функцию, которая возвращает поддерево компонента Virtual DOM. При изменении информации/состояния приложения, функция компонента вызывается заново, строит новое поддерево и обновляет изменившиеся части после diff.

Angular:

  • работает напрямую с реальным DOM, используя Incremental DOM;

  • знает конкретную ноду TView, которую нужно изменить, и обновляет только ее, без перерисовки всего компонента;

  • изменения применяются напрямую в DOM через сгенерированные инструкции, без глобального diff;

  • компоненты описываются классами, которые компилятор Ivy превращает в набор инструкций. Компонент — поддерево в общем TView, содержащее ссылку на шаблон, методы и зависимости для обновления.

Почему это важно

Понимание того, как React и Angular создают и обновляют DOM, помогает писать более производительный код и предсказывать, какие операции действительно вызывают перерисовку. 

В React важно помнить, что каждое изменение состояния пересоздает поддерево Virtual DOM, поэтому лишние рендеры можно оптимизировать через memo и useCallback. В Angular же выгодно использовать сигналы и отслеживать, какие именно участки TView становятся «dirty». Так можно избежать ненужных проходов Change Detection.

Если вы понимаете внутреннюю механику рендера, вы начинаете говорить с движком на его языке, а не просто юзать фичи фреймворка. А это может сэкономить ресурсы и нервы.

Бесплатный базовый курс по JS

Рассказываем, как работать с переменными, типами данных, функциями и о многом другом!

Начать изучение →

Задача 2: как работать с асинхронными данными

Мы хотим отрисовать список пользователей, полученных с сервера:

Простой список пользователей.
Простой список пользователей.

React

В React этот компонент выглядит следующим образом:

function UsersList() {
  const [users, setUsers] = React.useState([]);

  React.useEffect(() => {
    fetch('/api/users')
      .then(res => res.json())
      .then(data => setUsers(data));
  }, []);

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Как это превращается в DOM:

  • Initial Render. React вызывает функцию UsersList() и строит Virtual DOM для <ul> без <li>, потому что users пока пустой. На странице рисуется только <ul></ul>.

  • Асинхронный запрос. После монтирования компонента срабатывает useEffect. Пока fetch() выполняется, React ничего не перерисовывает – текущий Virtual DOM остается валидным.

  • State update. fetch() завершает свою работу, вызывается setUsers(data), React помечает компонент и вызывает функцию UsersList() заново, начинается новый рендер.

  • Reconciliation+Commit. React создает новое Virtual DOM с <li> для каждого пользователя. Дальше происходит diff, React находит, какие элементы добавились или изменились, и обновляет реальный DOM.

Результат:

<ul>

  <li>Kotik</li>

  <li>Frog Lover</li>

  <li>Polexka</li>

  ...

</ul>

Angular

В Angular компонент будет выглядеть так:

@Component({
  selector: 'users-list',
  standalone: true,
  template: `
    <ul>
      <li *ngFor="let user of users(); trackBy: trackByUserFn">{{ user.name }}</li>
    </ul>
  `
})
export class UsersListComponent {
  private http = inject(HttpClient);
  users = toSignal(this.http.get<User[]>('/api/users'), { initialValue: [] });
}

Что происходит при рендере компонента?

  • Генерация TView. Angular генерирует TView для компонента. Сигнал users() пока равен [], *ngFor ничего не отрисовывает.

  • Async Flow. toSignal() внутри Ivy создает обертку над Observable. Когда HttpClient отдает результат, сигнал обновляет свое значение.

  • Change Detection. Angular автоматически помечает соответствующий View как dirty и перерисовывает только часть шаблона, где используется этот сигнал.

  • Unsubscribe и очистка. DestroyRef отписывается автоматически при удалении компонента.

Результат:

<ul>
  <li>Kotik</li>
  <li>Frog Lover</li>
  <li>Polexka</li>
  ...
</ul>

Выводы

React

  • Управление асинхронностью — вручную через useEffect и setState;

  • Гибко, но легко забыть про отписку или ловушки состояния.

Angular

  • Асинхронность встроена в фреймворк через RxJS и Signals;

  • Не нужно писать дополнительный код для отписок или обновлений шаблона.

Почему это важно

Асинхронность — база для любого современного веб-приложения. Понимая, как фреймворк обновляет интерфейс при приходе данных, можно контролировать время рендера и предотвращать «моргание» страниц. 

В React осознанное управление эффектами и состоянием поможет предотвратить избыточные рендеры. В Angular встроенные механизмы асинхронности и отписки от зависимостей снижают вероятность ошибок, но требуют понимания Change Detection.

Понимание правильной работы с «асинхронщиной» делает ваш UI предсказуемым и плавным, в каком бы фреймворке или библиотеке вы ни работали.

Задача 3: как работает жизненный цикл компонента

Этот пункт будет немного отличаться от других — мы будем рассматривать не конкретный пример компонента, а детали жизни компонента внутри библиотеки и фреймворка. И в React, и в Angular существует механизм, который позволяет выполнять код на разных этапах жизни компонента — от его создания до удаления. Посмотрим, как это работает.

React

Жизненный цикл компонента реализован по-разному в разных фреймворках и библиотеках. Напомню, как это сделано в React.

В React есть два вида компонентов: классовые и функциональные. До версии 16.8 только классовые компоненты имели доступ к управлению жизненным циклом и состоянием. Шаблон JSX возвращался из метода render(), а фазы жизненного цикла — Mounting, Updating и Unmounting — поддерживались методами componentDidMount(), componentDidUpdate() и componentWillUnmount(). После релиза 16.8 функциональные компоненты получили поддержку хуков жизненного цикла и стали новым стандартом в React.

Компоненты — это функции. А хуки дают доступ к состоянию и эффектам внутри этих функций. Для жизненного цикла главная роль у useEffect и useLayoutEffect. Остальные хуки помогают контролировать состояние и оптимизацию. Давайте напомню основные понятия и поведение:

  • useEffect — эффект «после рендера и после покраски». Выполняется асинхронно, когда DOM уже обновлен. Подходит для сетевых запросов, подписок, таймеров.

  • useLayoutEffect — эффект, который выполняется синхронно сразу после изменения DOM, но до покраски. Используется для чтения размеров элементов или синхронных правок.

  • Очистка эффектов: эффект может вернуть функцию очистки — она вызывается перед следующим запуском этого же эффекта и при размонтировании компонента. Это основной инструмент для отписок и освобождения ресурсов.

  • Массив зависимостей (deps) управляет тем, когда именно сработает эффект: пустой массив — один раз при монтировании; зависимости — при изменении любой из них; без массива — после каждого рендера.

Прежде чем пойдем дальше, важно вспомнить, что у хуков есть свои тонкости. Например, застрявшие замыкания. Это эффект «захватывает» значения из замыкания на момент объявления: если в эффекте используются старые значения, нужно добавить их в массив зависимостей или пользоваться функциональным обновлением.

Напоминаю, что такое замыкание

Замыкание — это способность функции «запоминать» переменные из того контекста, в котором она была создана, даже если этот контекст уже закончился.

Другая тонкость — неправильные зависимости. Это частая причина лишних рендеров или пропущенных обновлений. А еще зависимости могут быть неописанными либо вообще лишними.

Наконец, вы можете выполнять тяжелую синхронную работу в useLayoutEffect, но она блокирует отрисовку — не делайте там тяжелых вычислений.

Что же в результате происходит внутри:

  1. При первом рендере React вызывает компонент-функцию и создает для нее Fiber Node — узел дерева, в котором хранится состояние и эффекты.

  2. Все вызовы хуков записываются внутрь Fiber в том порядке, в котором они были вызваны.

  3. Когда компонент завершает рендер, движок React добавляет эффекты в специальный список effectList этого Fiber.

  4. После commit-фазы React проходит по этому списку и вызывает сначала useLayoutEffect (синхронно), затем useEffect (асинхронно, после отрисовки браузером).

  5. При обновлении Fiber сравнивает зависимости (deps) и решает, нужно ли перезапустить эффект. При удалении компонента вызывает все cleanup-функции.

Все эти операции выполняются через планировщик (Scheduler), который решает, когда именно безопасно запустить эффекты, чтобы не блокировать главный поток.

Что такое Scheduler?

Scheduler — это внутренняя подсистема React, которая управляет тем, когда именно выполнять обновления и эффекты.

Она появилась вместе с Fiber, чтобы сделать рендеринг прерываемым. Например, если пользователь скроллит или печатает, Scheduler может отложить выполнение тяжелого эффекта, чтобы не блокировать интерфейс.

Эффекты  и обновления состояния ставятся в очередь и выполняются, когда у браузера есть свободное «окно времени», чтобы интерфейс оставался отзывчивым.

Зачем знать, как работают хуки? Я столкнулась с задачкой на React – отрисовывать md-файл, переводя его в HTML и применяя соответствующее форматирование к заголовкам, <strong>, <em> — в общем, ко всем возможным базовым тегам текста. А в параграфах нужно было экранировать некоторые строки. Из большой строки md-текста генерировалось огромное количество элементов в DOM. А еще на этой же страничке был контролируемый input, который при вводе вызывал обновление состояния. Так получалось, что при выводе результата преобразования из md в HTML страница начинала сильно зависать, плохо скроллиться, а ввод в input появлялся с задержкой ~500ms.

Я, мало знакомая тогда с React, не знала, почему такое поведение могло возникнуть, и главное — как пофиксить это без выдумывания виртуального скролла и костылей, используя только возможности React и минимум кода.

Почему все лагало? Потому что React на каждом нажатии клавиши проходил diff через все огромное HTML-дерево.

Решением моей проблемы было просто обернуть компонент-парсер в useMemo.

И, о чудо, страница перестала зависать вообще. Что изменилось?

  • useMemo подсказал React, что если HTML не менялся — пересоздавать этот DOM-узел не нужно.

  • diff перестал сравнивать сотни вложенных элементов на каждом рендере.

  • Контролируемые инпуты снова начали работать мгновенно.

Angular

В Angular жизненный цикл строго структурирован: рантайм Ivy создает внутренние структуры View и вызывает набор предопределенных методов в определенном порядке. Эти хуки дают контроль над этапами: прием входных данных, построение контента, доступ к DOM дочерних компонентов, и очистка.

Порядок выполнения и значения хуков:

  1. ngOnInit — вызывается один раз после инициализации компонента.

  2. ngOnChanges — вызывается при изменении входных данных компонента.

  3. ngDoCheck — вызывается при каждом цикле обнаружения изменений.

  4. ngAfterViewInit — вызывается после инициализации представления компонента.

  5. ngOnDestroy — вызывается перед уничтожением компонента.

С Angular тоже не все так просто. Если в компоненте или в его дочерних компонентах происходят изменения, которые не касаются отображаемых данных, Angular может все равно запускать стратегию обнаружения изменений ChangeDetection для всего дерева компонентов. Это может привести к потерям производительности. Чтобы этого избежать, можно использовать ChangeDetectionStrategy.OnPush, чтобы Angular проверял только те компоненты, которые явно должны быть обновлены.

Что происходит под капотом? Сначала Ivy компилирует шаблон. В это время он создает для компонента структуру TView, где хранится информация о привязках, шаблонах и хуках. Для каждого компонента создается ViewRef, содержащий ссылки на DOM, контекст и списки хуков:

  • initHooks – хуки, выполняемые один раз (ngOnInit),

  • checkHooks – хуки, выполняемые при каждом проходе Change Detection (ngDoCheck, ngAfterViewInit),

  • destroyHooks – хуки, выполняемые при уничтожении (ngOnDestroy).

При первом проходе Change Detection Angular вызывает все initHooks и помечает View как «инициализированный». При последующих проходах вызываются только checkHooks. Если компонент помечен как dirty, Angular обновляет его шаблон и снова проходит хуки. При уничтожении View (например, *ngIf становится false), Angular вызывает destroyHooks и очищает все подписки, связанные с этим ViewRef.

Выводы

React

  • Компоненты в React управляются через хуки, которые позволяют контролировать побочные эффекты после рендера или до покраски DOM. 

  • Хуки дают гибкость, но требуют внимательности к зависимостям и очистке.

Angular

  • Angular использует структурированный жизненный цикл и предоставляет готовый интерфейс реализации жизненного цикла. 

  • Система Change Detection отвечает за обновление компонента, но без должной оптимизации могут происходить избыточные перерисовки.

Почему это важно

Понимание жизненного цикла важно для контроля за состоянием и производительностью компонентов. В React ключевым является управление зависимостями хуков, а в Angular — настройка Change Detection для минимизации ненужных обновлений. Это помогает избежать лишних рендеров и оптимизировать использование ресурсов, что напрямую влияет на скорость работы приложения. 

Задача 4: как работает управление состоянием

Теперь рассмотрим компонент, содержащий кнопку, на которой будет отображаться счетчик нажатий:

Количество нажатий изменяется с каждым кликом.
Количество нажатий изменяется с каждым кликом.

React

В React этот компонент будет выглядеть так:

function Counter() {
  const [count, setCount] = React.useState(0);

  return (
    <button onClick={() => setCount(prev => prev + 1)}>
      Clicked {count} times
    </button>
  );
}

Каждый клик по кнопке вызывает setCount, и компонент заново отрисовывается.

На экране меняется только цифра, но в браузере происходит целая цепочка событий.

Первый шаг — React впервые вызывает Counter() и получает JSX.  Создается  Fiber Node, внутри которого хранится:

  • memoizedState — связанный список хуков, где каждый useState создает элемент списка { memoizedState: value, queue: updateQueue }.

  • updateQueue — очередь обновлений (связанный список из объектов { action, next }), куда React будет складывать все вызовы setState.

Пример внутреннего состояния Fiber:

Fiber = {
  type: Counter,
  memoizedState: {
    memoizedState: 0,
    queue: null
  }
};

Следующим шаг — Commit Phase. React превращает JSX в Virtual DOM:

React.createElement("button", { onClick }, "Clicked ", count, " times")

Затем создается реальный DOM-элемент через document.createElement('button'), добавляется слушатель click и вставляется текст. Результат в DOM:

<button>Clicked 0 times</button>

Как происходит обновление состояния? Когда пользователь кликает по кнопке, вызывается setCount(prev => prev + 1). React создает объект обновления:

const update = { action: prev => prev + 1, next: null };

И добавляет его в очередь updateQueue текущего Fiber.

Планировщик (Scheduler) помечает компонент как «dirty» и запускает новый рендер.

React снова вызывает Counter(). При этом он:

  • достает из updateQueue все апдейты;

  • применяет их к старому memoizedState (0 → 1);

  • сохраняет результат как новое состояние;

  • пересоздает Virtual DOM.

React сравнивает новое дерево Virtual DOM с предыдущим (Reconciliation), видит, что изменился только текст внутри кнопки. В Commit Phase обновляет его через node.textContent = "Clicked 1 times".

Результат в DOM после обновления:

<button>Clicked 1 times</button>

Angular

В Angular такой же компонент будет выглядеть так:

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <button (click)="update()">
      Clicked {{ count() }} times
    </button>
  `
})
export class CounterComponent {
  count = signal(0);

  update() {
    this.count.update(prev => prev + 1);
  }
}

Что происходит в Angular?

Компиляция. Шаблон превратится в набор низкоуровневых инструкций Ivy:

function CounterComponent_Template(rf, ctx) {
  if (rf & 1) {
    ɵɵelementStart(0, "button");
    ɵɵlistener("click", function() { return ctx.update(); });
    ɵɵtext(1);
    ɵɵelementEnd();
  }
  if (rf & 2) {
    ɵɵadvance(1);
    ɵɵtextInterpolate1("Clicked ", ctx.count(), " times");
  }
}

После происходит первое создание TView, в котором содержатся метаданные о шаблоне — ссылки на DOM-ноды, индексы для биндингов, инструкции которые нужно выполнить при рендеринге. В момент создания в DOM появится:

<button>Clicked 0 times</button>

Сигнал count — это объект:

{
  value: 0,
  version: 0,
  dependents: Set<Computation>
}

При чтении в шаблоне count() Angular регистрирует зависимость: вычисляемая функция ɵɵtextInterpolate1 добавляется в dependents.

Когда вызывается count.update(prev => prev + 1):

  • значение сигнала меняется на 1;

  • увеличивается версия;

  • все зависимые Computation помечаются как «dirty»;

  • Angular вызывает Change Detection для нужного поддерева View.

В отличие от старого Zone.js, Signals не требуют прохода по всему дереву —
обновляется только конкретный биндинг. При следующем проходе Change Detection Angular вызывает вторую часть шаблонной функции(rf & 2). Это приводит к прямому изменению текста, без diff и без пересоздания элементов.

Результат в DOM после обновления:

<button>Clicked 1 times</button>

Выводы

React

  • Состояние хранится в Fiber как список хуков.

  • setState добавляет обновление и инициирует новый рендер.

  • Создается новый Virtual DOM и сверяется с предыдущей версией.

  • Изменения вносятся в реальный DOM через нативные методы браузера.

Angular

  • Шаблон компилируется в инструкции Ivy.

  • Сигналы отслеживают зависимости и помечает их как «dirty» и вызывает только нужные инструкции.

  • Обновление DOM выполняется напрямую, без Virtual DOM и diff.

Почему это важно

Понимание того, как происходят обновления, помогает осознанно подходить к оптимизации рендеринга. 

В React знание о работе очереди обновлений помогает предотвращать лишние ререндеры через мемоизацию и правильные зависимости. В Angular понимание работы сигналов и их связи с TView дает возможность точечно управлять обновлениями интерфейса, минимизируя нагрузку на механизм обнаружения изменений.

Правильная работа с обновлением состояния приложения поможет оптимизировать сложные интерфейсы с множеством зависимостей, тянущих за собой обновления UI.

Задача 5: как организовать архитектуру приложения 

Когда ваше приложение становится чем-то бóльшим, чем пара компонентов, невольно приходится задумываться об архитектуре. Правильно построенная архитектура позволит удобно вести разработку и не потеряться во всех фичах, компонентах и состояниях приложения. Можно очень долго и подробно рассматривать множество архитектурных паттернов, но сейчас мы будем рассматривать только типовые подходы, которые я чаще всего встречала в React и Angular.

React

Сама библиотека React не диктует, где хранить бизнес-логику, компоненты или состояния. Архитектура React-приложения чаще складывается на основе соглашений внутри команды разработки.

Самый «базовый» вариант архитектуры, которому меня когда-то учили на курсах, и который я чаще встречаю у начинающих фронтенд-разработчиков, выглядит так:

«Наивная» архитектура.
«Наивная» архитектура.

Такое разбиение в начале пути кажется логичным, ведь компоненты, хуки, страницы — это разные сущности. Но по мере роста проекта такое проектирование быстро становится неудобным — чтобы изменить одну фичу, приходится переходить между несколькими папками, искать связанные файлы и помнить, какие компоненты зависят друг от друга. Получается, эта структура формируется по типу сущности, а не по функциональности. Логика одной фичи разбросана по всему проекту — вот из-за чего я страдала и плевалась на React, когда училась писать на нем и когда мне приходилось работать над чьим-то чужим проектом с этой библиотекой.

Чтобы избежать подобного, существуют различные подходы, которые более распространены в реальных проектах — feature-based, domain-based и feature-sliced архитектуры. 

Feature-based архитектура. Код группируется вокруг функциональных областей приложения — auth, cart, profile и т. д. Внутри каждой фичи лежат ее собственные компоненты, стор, сервисы и хуки.

Feature-based архитектура.
Feature-based архитектура.

Такая структура хорошо масштабируется и позволяет командам работать параллельно: каждая фича — изолированный модуль со своими зависимостями.

Domain-based архитектура. Похожа на feature-based, но делится вокруг сущностей (бизнес-доменов), над которыми ведется работа. 

Domain-based архитектура.
Domain-based архитектура.

Domain-based подходит для команд, где разные разработчики отвечают за разные бизнес-области. Если ваше приложение работает с одними и теми же сущностями в разных фичах — domain-based поможет избежать дублирования логики.

Feature-Sliced Design архитектура. Более строгая и продуманная версия feature-based подхода. Главное отличие в том, что FSD вводит четкие слои с правилами взаимодействия между ними, тогда как feature-based — это просто идея группировать код по фичам без жесткой структуры.

FSD архитектура.
FSD архитектура.

FSD используют, когда проект сложный, в команде несколько разработчиков и нужно поддерживать порядок на долгосрочной основе. 

В архитектуре важно не только, где лежат файлы, но и как организованы связи, находящиеся в компонентах и обвязывающие все приложение — роутинг, данные о состоянии, различные формы.

Роутинг. В React нет встроенного роутера. Для поддержки роутинга используют библиотеки, например, react-router.  Она выполняет условный рендеринг компонентов в зависимости от URL. При навигации React размонтирует старый компонент и смонтирует новый.

Пример простого роутинга в React-приложении:

import { createBrowserRouter, RouterProvider } from "react-router-dom";
import Home from "./pages/Home";
import User from "./pages/User";

const router = createBrowserRouter([
  { path: "/", element: <Home /> },
  { path: "/user/:id", element: <User /> },
]);

export function App() {
  return <RouterProvider router={router} />;
}

В последних версиях React Router поддерживает загрузку данных в роутах и отложенную загрузку компонентов. Благодаря этому можно обрабатывать ошибки на уровне роутинга, а не писать обработку в useEffect во вложенных компонентах.   

Формы. В React контролируемые формы делают с помощью связи контрола с useState

function LoginForm() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  return (
    <form>
      <input value={email} onChange={e => setEmail(e.target.value)} />
      <input type="password" value={password} onChange={e => setPassword(e.target.value)} />
    </form>
  );
}

В случае с громоздкими формами использование useState может выглядеть неудобным, но для этого в сообществе React тоже есть решение — например, react-hook-form, которая работает через ref и не вызывает лишних ререндеров. 

Angular

Angular с самого начала строился вокруг архитектуры. Разработчик не решает, где объявить компонент, а следует заданной схеме. Компоненты описываются через декораторы (@Component, @Directive, @Pipe), шаблон и метаданные объявляют, как элемент должен вести себя, какие зависимости подключены и что он может использовать внутри.

В современных версиях Angular (17+) большая часть проектов строится на standalone-подходе: компоненты объявляются как самостоятельные единицы (standalone: true), импорты объявляются явно. Приложение выглядит примерно так:

Пример базовой архитектуры Angular.
Пример базовой архитектуры Angular.

Такой подход накладывает реальные ограничения на разработчиков — но в хорошем смысле: когда зависимости явные и декларативные, легче понять, какие UI-блоки и утилиты нужны компоненту, легче рефакторить код. Это держит проект в рамках и делает архитектуру самодокументированной — код сам рассказывает, какие части системы он использует.

Роутинг. Angular Router встроен в сам фреймворк, являясь его частью, а не отдельной библиотекой. Он управляется через RouterOutlet. При переходах Angular сам следит за состоянием приложения, создавая и удаляя компоненты. 

app.routes.ts

import { Routes } from '@angular/router';
import { HomeComponent } from './pages/home/home.component';
import { UserComponent } from './pages/user/user.component';

export const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'user/:id', component: UserComponent },
];

app.component.ts

import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet],
  template: `<router-outlet></router-outlet>`
})
export class AppComponent {}

С версии 17 появилась возможность ленивой загрузки компонентов через loadComponent и директивы в шаблоне @defer, @placeholder. Это значит, что тяжелые части страницы можно загрузить только тогда, когда они действительно нужны, без ручных оберток.

Формы. Angular предоставляет два подхода — формы Template-driven (через ngModel)  и Reactive Forms (FormGroup, FormControl). Reactive Forms поддерживают сигналы.

Пример реактивной формы:

form = new FormGroup({
  email: new FormControl(''),
  password: new FormControl(''),
});

При изменении формы поля обновятся через свой контрол, валидация и статус (dirty, touched) синхронизируются автоматически.

Благодаря структуре в Angular даже огромные проекты выглядят предсказуемо. Когда все собирается через декларации и импорты, нет сюрпризов — и это одно из главных достоинств фреймворка.

Выводы

React оставляет выбор архитектуры и организацию кода за разработчиками, поэтому можно видеть и проекты, где царит полный хаос, так и проекты, где все выглядит понятно и структура легко читаема. Решения по выбору механизма роутинга, обработке состояний, форм остаются на совести разработчиков.

Angular дает системный подход «из коробки», и правила, которым нужно следовать при организации зависимостей в проекте. Все необходимые инструменты уже продуманы и предоставлены для использования разработчиками, не нужно искать решений.

Почему это важно

Архитектура — это то, чего не видно в интерфейсе, но то, что определяет, насколько комфортно жить в проекте. От архитектуры зависит, как быстро новый человек разберется, как легко исправить баг или выделить часть системы в отдельную библиотеку. Понимание архитектурных принципов помогает не теряться в чужом проекте и не плодить бессмысленные структуры в своем.

От того, насколько продумана навигация, зависит, как быстро пользователь сможет перемещаться между разделами, как приложение обрабатывает глубокие ссылки и насколько просто добавить новую страницу. Умение выбрать правильный подход для работы с формами напрямую влияет на надежность и отзывчивость интерфейса.

Управление данными — будь то состояние приложения, кэширование или подписки на данные — должно быть предсказуемым. В React вы сами решаете, как хранить и обновлять состояние, что позволяет выбирать различные решения, но это требует понимания, нужна ли определенная библиотека в проекте. Angular предлагает больше встроенных паттернов (Services, Signals), что ускоряет разработку, но требует следовать правилам и рамкам фреймворка.

Задача 6: как повысить производительность 

Производительность — это не вопрос «кто быстрее», а вопрос того, как именно тратятся ресурсы. React и Angular решают одну задачу — обновлять интерфейс с минимальными издержками, но путь у них разный.

React

React использует Virtual DOM — промежуточное представление интерфейса. При изменении состояния строится новое виртуальное дерево, которое сравнивается с предыдущим, и лишь затем вносятся минимальные изменения в реальный DOM. 

Это сравнение и последующие операции с DOM — главный источник нагрузки. Поэтому любое лишнее обновление состояния или крупное поддерево компонентов напрямую бьют по скорости. Этот процесс идет синхронно в React 17 и частично асинхронно в React 18+, где появился Concurrent Mode и Scheduler.

Теперь React может приостанавливать рендеринг, если в это время браузер занят обработкой пользовательского ввода, и продолжать позже — интерфейс продолжает работать даже при тяжелых пересчетах. Но вместе с этим растет цена памяти и вычислений: чем глубже компонентное дерево и чем больше состояний меняется одновременно, тем больше операций diff придется выполнить. Поэтому в больших проектах React оптимизируют через React.memo, useMemo, useCallback, мемоизацию селекторов и вынос тяжелой логики из рендера в эффекты или воркеры.

Angular

Angular работает без Virtual DOM. Он компилирует шаблон в инструкции, которые напрямую обновляют DOM. Эти инструкции описывают, какие узлы создать, какие атрибуты обновить, какие биндинги связаны с данными. Когда состояние меняется, Angular запускает цикл Change Detection — проход по дереву представлений (View Tree), где каждый компонент проверяет свои привязки.

Механизм обнаружения изменений по умолчанию проверяет все компоненты при любом асинхронном событии. Это может быть избыточно, поэтому для производительности используется стратегия OnPush и сигналы , которые позволяют точно определять зависимости и обновлять только затронутые компоненты. Это снижает нагрузку и делает рендер более предсказуемым в сложных приложениях.

React тратит ресурсы на diff, Angular — на обход дерева и синхронизацию с зоной. Оба решают одну задачу, но разные этапы становятся узким местом: у React — reconciliation, у Angular — Change Detection. 

Почему это важно

Понимание внутренней модели рендера помогает видеть причину тормозов, а не их следствие. Если React подвисает — ищите лишние пересоздания дерева, если Angular — проверьте границы стратегии и изоляцию сигналов. Это позволяет не «оптимизировать наугад», а управлять производительностью осознанно, проектируя интерфейс под особенности движка.

Незадача: почему React собирается быстрее Angular

Да, это не задача, а моя личная боль, и боль еще многих разработчиков на Angular. Когда запускаешь npm start, React оживает почти мгновенно, а Angular «разогревается» заметно дольше. Кажется, будто Angular просто медленнее. Но разница не в скорости, а в том, что происходит внутри.

React — это библиотека, а не фреймворк. Его дев-сборка — это компиляция JSX и TypeScript в JS без анализа шаблонов, метаданных или зависимостей. Все, что делает vite или esbuild, — это транспиляция и бандлинг. При hot reload vite пересобирает только измененный модуль и отправляет обновленный JS через WebSocket, браузер просто перерисовывает компонент — никакого общего пересчета. Это и создает ощущение мгновенной реакции.

Angular — полноценный фреймворк со стадией компиляции. Каждый шаблон проходит через Ivy-компилятор, который превращает HTML-разметку и биндинги в JavaScript-инструкции. Кроме этого, Angular собирает метаданные, проверяет типы в шаблонах и выстраивает граф зависимостей между компонентами. Это все происходит еще до старта приложения и занимает время. Даже при dev-сборке, где часть шагов упрощена, анализ шаблонов остается.

Почему же Angular делает это заранее? Потому что цель — AOT-компиляция (ahead-of-time): шаблоны проверяются и превращаются в JS до выполнения. Это делает рантайм быстрее и безопаснее — не нужно хранить компилятор в бандле, не может упасть из-за синтаксической ошибки в шаблоне. За это приходится платить временем сборки.

Начиная с Angular 16–17 многое улучшилось. Webpack заменили на esbuild, HMR включается прямо из CLI, а standalone-компоненты убрали громоздкие модули и ускорили пересборку. Теперь Angular при hot reload не пересобирает все приложение, а обновляет только конкретный компонент, если он standalone. Но из-за анализа типов и шаблонов Angular все равно делает больше работы, чем React, и потому чуть дольше.

Вывод

Вне зависимости от выбора технологии на выходе мы получаем один и тот же современный, быстрый и качественный UI. А значит, главное — это не фреймворк, а разработчик, который его использует.

Спасибо за чтение! Надеюсь, было интересно и полезно заглянуть под капот React и Angular вместе со мной.

В завершение могу оставить только эту картинку.
В завершение могу оставить только эту картинку.

Комментарии (2)


  1. ZudaR
    13.11.2025 08:59

    Очень классный обзор подкапотного устройства! Именно этого и не хватало в вузе. Я вообще сам низкоуровневый разработчик, поэтому у меня всегда возникали вопросы: а как это работает-то? Ведь мне нужно понимать, что происходит на низком уровне, чтобы писать оптимизированный код. И вместо простых дерганий функций реакта, нужно именно вот так залезать в его устройство и разбираться. Спасибо Вам!


  1. isumix
    13.11.2025 08:59

    Фреймворк управляет вашим кодом, ваш код управляет библиотекой - вот разница между фреймворком и боблиотекой. Реакт - тоже фреймворк, так как вы не управляете тем когда и как вызываются ваши компоненты и хуки, этим управляет движок Реакта когда вы передаете управление в createRoot.

    С другой стороны Фьюзор - это библиотека. В ней функция-компонент сразу создает DOM и запоминает в каком месте есть динамические данные (в виде фунций-обновителей c указателем DOM ноды), и далее только обновляет эти участки DOM без их поиска и DIFF-инга. Управление никуда не передается, все делается явно и в ручном режиме - async/await, try/catch, filter/map, только JavaScript и ничего лишнего.