---
url: https://nsh.dev/ru/docs/architecture
description: "Как работает приложение на NSH: конвейер сборки, потоки движка и главный поток, путь от изменения сигнала до нативных представлений, нативный реактивный граф, раскладка текста и крейты."
---

# Архитектура

Эта страница для разработчиков приложений, которые хотят понять, что происходит между `count.set(1)` и изменением надписи на экране
в приложении, написанном на Rust или TSX.
Она не нужна для написания приложений, но объясняет, почему NSH ведёт себя именно так: почему компоненты выполняются один раз, почему
анимации остаются плавными, пока JS занят, и почему основной поток никогда не ждёт ваш код.

<a id="the-big-picture"></a>

## Общая картина

![How an nsh app runs: the nsh CLI builds App.tsx into bytecode; on the engine thread, QuickJS runs a handler, the reactive graph in nsh-js queues ops, and nsh-core commits one MountBatch; it crosses to the main thread in one hop, which applies it to UIKit or Android views; touches go back as events. Native animations run on the main thread without JavaScript.](https://nsh.dev/diagrams/architecture-light.svg)

<a id="building-only-your-code-becomes-js"></a>

## Сборка: в JS превращается только ваш код

`nsh build`, `nsh dev` и другие команды собирают ваше приложение с помощью инструментария, написанного на Rust (без Node.js):

1. **Oxc** удаляет типы TypeScript.
2. **JSX-компилятор NSH** превращает JSX в прямые вызовы нативного рендерера за тот же проход. Статические свойства и
   дочерние элементы задаются один раз; выражения, которые могут измениться (они вызывают функцию или читают свойство), становятся привязками, за которыми
   следит рендерер. Компоненты превращаются в обычные вызовы функций с объектом props, в котором изменяющиеся свойства являются геттерами.
3. **rolldown** собирает ваши модули в один ES-модуль. Импорты модулей NSH (`nsh`, `nsh/mobile`,
   `nsh/base`, ...) остаются импортами: эти модули нативные и предоставляются средой выполнения. **Бандл содержит только код вашего
   приложения**; пример со счётчиком занимает менее 1 КБ JS.
4. Бандл компилируется в **байт-код** (`bundle.nshc`), который загружается быстрее исходного кода, а **source map**
   собирается по всем шагам, так что трассировки стека указывают на строки вашего `.tsx`.

Импортируемые изображения и шрифты собираются как ресурсы, а объявления типов для вашего редактора записываются в
`.nsh/types`.

У приложения на Rust бандла нет: Cargo собирает запускающий модуль для каждой целевой платформы с вашим крейтом, а корневой
компонент, названный в `nsh.json`, нативно выполняется движком. Всё описанное ниже справедливо и для такого приложения, только код ваших компонентов
выполняется в Rust там, где в TSX-приложении он выполняется в QuickJS.

<a id="running-two-threads-per-app"></a>

## Выполнение: два потока на приложение

Каждое запущенное приложение (`NshAppView` на iOS, `NshView` на Android, окно упакованного приложения) — это один **движок** со
своим **потоком движка** и, для TSX, своей виртуальной машиной QuickJS. Глобальных синглтонов нет, поэтому несколько приложений могут работать рядом
в одном приложении-хосте.

<a id="the-engine-thread"></a>

### Поток движка

Здесь происходит всё, что относится к логике вашего приложения:

- **Ваш код** выполняется здесь: компоненты, обработчики событий, таймеры, эффекты. Компоненты на Rust выполняются нативно, TSX — в
  **QuickJS**.
- **Реактивный граф** (сигналы, вычисляемые значения, эффекты, владельцы, контекст) нативный. Его узлы — это объекты QuickJS
  нативных классов, поэтому сборщик мусора видит ссылки через них, но отслеживание зависимостей и
  планирование обновлений реализованы на Rust (по алгоритму Solid 1.9).
- **Рендерер**, который вызывает ваш скомпилированный JSX, нативный. Он создаёт узлы и превращает каждое изменение в **операцию**, помещаемую
  прямо в нативную очередь: без JSON и без отдельных сообщений на каждое свойство.
- **Ядро** хранит теневое дерево всех узлов. Когда ход JS заканчивается, оно забирает все операции из очереди сразу и выполняет **коммит**:
  рендерит компоненты на Rust (`nsh/base` и собственные компоненты приложения), разрешает отступы `env()` в padding и margin, пересобирает изменившиеся тексты, выполняет раскладку с помощью **Taffy** (flexbox и grid, с
  колонкой по умолчанию и без сжатия), сравнивает фреймы и стили и формирует один **MountBatch**: список операций, нужных нативным
  представлениям.

<a id="the-main-thread"></a>

### Основной поток

Основной (UI) поток платформы применяет каждый MountBatch за один переход: создаёт представления, вставляет их, задаёт стили и фреймы,
передаёт уже разложенный текст. Кроме этого он делает только то, что должно происходить именно в нём:

- **отрисовка текста**, уже разложенного в потоке движка;
- **нативные анимации**: анимированные значения из `nsh` управляются покадрово (`CADisplayLink` на iOS, аниматор
  UI-потока на Android) и записываются прямо в представления;
- **жесты и прокрутка**: касания превращаются в события, отправляемые в поток движка; `scrollValue` и `pan` передают анимированные
  значения в том же кадре.

**Основной поток никогда не ждёт JS.** Когда системе нужен ответ немедленно (можно ли изменить этот текст, может ли
кнопка «назад» закрыть этот экран, можно ли закрыть этот лист), ответ заранее смонтирован как свойство, а JS узнаёт о нём
позже через событие. Занятый поток движка может задержать обновления, но не может заморозить прокрутку, анимации или
системные жесты.

<a id="from-a-tap-to-the-screen"></a>

## От касания до экрана

```text
 main thread               engine thread                                              main thread
 ───────────               ─────────────────────────────────────────────────────      ───────────
 tap on a view ─ event ─▶  handler runs: count.set(count() + 1)
                           reactive graph: the bindings that read count() run again
                           renderer: one op per changed binding → native queue
                           JS turn ends → core commit: render, text, layout, diff
                           → MountBatch (e.g. 1 Text item)                 ─ hop ─▶   apply: draw the new text
```

В примере со счётчиком касание обходится примерно в 0,12 мс в релизной сборке на Mac, включая JS и коммит вместе, и порождает
небольшое число элементов монтирования (см. [Тестирование](https://nsh.dev/translations/ru/docs/testing-and-trace.md)).

В приложении на Rust обработчик — это замыкание Rust (`count.set(count.get() + 1)`), а далее следуют те же граф, коммит и пакет,
без интерпретатора на пути.

Из этого устройства следуют три свойства:

- **Один ход — один коммит.** Сколько бы сигналов ни установил обработчик, представления изменятся один раз, вместе.
- **Компоненты выполняются один раз.** Повторно выполняются только привязки; нет повторного рендера компонента и сравнения виртуального DOM. Остающееся
  сравнение выполняет ядро, над фреймами и стилями, на Rust.
- **Передаются только изменения.** Неизменившиеся стили и фреймы не отправляются; новые представления не получают значений по умолчанию.

<a id="text-laid-out-once-off-the-main-thread"></a>

## Текст: раскладывается один раз, вне основного потока

Текст часто оказывается самой дорогой частью UI. NSH раскладывает его в потоке движка штатным текстовым
движком платформы, поэтому он выглядит в точности как нативный текст:

1. **подготовка**: `<Text>` и его вложенные спаны превращаются в одну строку с атрибутами (CoreText на платформах Apple,
   `PrecomputedText` / `StaticLayout` на Android);
2. **измерение** при ширинах, которые запрашивает раскладка; измерения кешируются по содержимому и ширине, поэтому изменение только цвета
   не приводит к повторному измерению;
3. **раскладка** в итоговом размере.

Основной поток получает неизменяемый результат и только рисует его. Текст, вложенный в `<Text>`, — это спан, а не представление;
строки вне `<Text>` ничего не отображают.

<a id="lists"></a>

## Списки

`List` виртуализируется нативно. Ядро решает, какие строки находятся рядом с областью просмотра, вычисляет позиции, колонки и
закреплённые заголовки и переиспользует ячейки. Когда ячейке нужен другой элемент, среда выполнения привязывает его (меняются
акцессоры `item()` и `index()` вашей функции строки); кадры, не открывающие новых строк, вообще не вызывают JS. См. [Списки](https://nsh.dev/translations/ru/docs/lists.md).

<a id="everything-else-the-runtime-does"></a>

## Всё остальное, что делает среда выполнения

- **Сеть**: `fetch` и другие глобальные API — нативные классы. Запросы идут в HTTP-стек платформы
  (`NSURLSession`, `HttpURLConnection`) с потоковой передачей в обоих направлениях.
- **Изображения** загружаются и декодируются вне основного потока, в размере представления, с кешем в памяти.
- **Ограничения и разрешения** обеспечиваются средой выполнения на Rust: см. [Песочница и разрешения](https://nsh.dev/translations/ru/docs/sandbox-and-capabilities.md).
- **Горячая замена модулей** тоже нативная: патчи применяются средой выполнения значительно быстрее миллисекунды (см.
  [Сервер разработки и отладка](https://nsh.dev/translations/ru/docs/dev-server-and-debugging.md)).

<a id="three-layers"></a>

## Три слоя

UI в NSH построен из трёх слоёв, как и в GPUI Kit:

| Слой | NSH | GPUI Kit | Что это |
|---|---|---|---|
| Примитивы | `nsh-core` и среда выполнения (`nsh`, `nsh/mobile`, `nsh/desktop`) | `gpui` | Хост-элементы, стили, раскладка, рендеринг, реактивность, окружение |
| Базовые компоненты | `nsh-base` (`nsh/base`) | `gpui-base` | Компоненты без стилей, которые отвечают за поведение, состояние, фокус, клавиатуру и доступность, а внешний вид берут из приложения |
| Библиотека компонентов | ваша собственная или сообщества | `gpui-component` | Стилизованные компоненты: кнопки, карточки, строки с внешним видом |

Первые два слоя — это Native Shell. Третий — ваш: приложение строит собственную библиотеку компонентов на `nsh/base` и
примитивах, в TSX или как компоненты Rust в [собственном крейте](https://nsh.dev/translations/ru/docs/rust.md), использует библиотеку, опубликованную сообществом, или
стилизует свой UI прямо в приложении.

<a id="the-crates"></a>

## Крейты

NSH — это рабочее пространство Rust. Для написания приложений собирать его не нужно (дистрибутив содержит CLI и готовые среды выполнения), но
эти имена встречаются в логах и документации:

| Крейт | Что это |
|---|---|
| `nsh-core` | Платформонезависимое ядро: теневое дерево, стили, раскладка Taffy, слияние текста, виртуализация списков, элементы монтирования; значения CSS и Tailwind; нативный граф анимации (значения, интерполяция, timing / spring / decay); фреймворк и тема компонентов на Rust; модули среды выполнения на Rust (`BackHandler` из `nsh/mobile`, ...). |
| `nsh-base` | `nsh/base`, базовые компоненты без стилей (`gpui-base` из NSH): навигация, диалоги, элементы управления и строка состояния; приложения на Rust используют его как `nsh_base`. |
| `nsh-js` | Хост QuickJS и среда выполнения NSH: реактивный граф, рендерер, все нативные модули, глобальные API, горячая замена модулей, ограничения; а также объявления TypeScript для всего этого. |
| `nsh-engine` | Поток движка: управляет `nsh-js` и `nsh-core` и передаёт платформе один пакет за кадр. |
| `nsh-package` | Подписанные пакеты: ключи, сертификаты, отзыв, хранилище доверия. |
| `nsh-bundler` | Сборщик: rolldown, Oxc, JSX-компилятор NSH, source map, проверки классов Tailwind. |
| `nsh-dev` | Сервер разработки и его клиент. |
| `nsh-platform-ios` | Бэкенд UIKit (iOS, симулятор, Mac Catalyst): монтирование представлений, текстовый движок CoreText, ввод; SDK для встраивания. |
| `nsh-platform-android` | Бэкенд Android: `libnsh.so` и среда выполнения на Java (`NshView`, представления, изображения, сеть). |
| `nsh-cli` | Команда `nsh`. |

Направление зависимостей: `nsh-core` ← `nsh-js` ← `nsh-engine` ← платформы; `nsh-base` строится на `nsh-core`. Ядро не зависит ни от платформы, ни от JS-движка,
что позволяет `nsh test` запускать настоящую среду выполнения без интерфейса на любой машине.

<a id="see-also"></a>

## См. также

- [Производительность](https://nsh.dev/translations/ru/docs/performance.md)
- [Реактивность](https://nsh.dev/translations/ru/docs/reactivity.md)
