---
url: https://nsh.dev/ru/docs/performance
description: "Как NSH обеспечивает высокую скорость приложений (приложения на Rust, нативная среда выполнения, пакетная передача вызовов через границу, нативные анимации, виртуализированные списки), что это значит для вашего кода и как измерять производительность без интерфейса и на экране."
---

# Производительность

Производительность — первый приоритет NSH, и большая её часть определяется самим фреймворком, а не вашим приложением: среда выполнения нативная, каждое обновление пересекает границу потоков один раз, анимации и виртуализация списков работают без JS, а приложение на Rust вообще не запускает JS. На этой странице описаны правила, которым следует NSH, что они означают для кода, который вы пишете, на Rust и на TSX, и как проводить измерения.

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

## Правила

<a id="1-js-does-as-little-as-possible"></a>

### 1. JS делает как можно меньше

**Приложение на Rust — это нативный код от начала до конца**: его компоненты, состояние и обработчики компилируются движком NSH, и никакой интерпретатор не запускается ([Building with Rust](https://nsh.dev/translations/ru/docs/rust.md)). Это самый быстрый способ написать приложение, и именно его стоит выбирать для требовательной бизнес-логики.

В приложении на TSX QuickJS — это интерпретатор: небольшой, быстро запускающийся и намного более медленный, чем нативный код, при тяжёлой работе. Поэтому в NSH **JS — только код вашего приложения**. Реактивный граф (сигналы, вычисляемые значения, эффекты), рендерер, который вызывает ваш скомпилированный JSX, движок раскладки, виртуализация списков, анимация и каждый импортируемый вами модуль написаны на Rust. Разбор, сравнение (diff), раскладка и обработка строк происходят нативно.

Эффект поддаётся измерению. Одно и то же приложение-счётчик на прежней JS-среде выполнения NSH и на нативной среде (релизные сборки):

| | Прежняя JS-среда | Нативная среда |
|---|---|---|
| Размер бандла | 14 KB JS / 19.6 KB байткода | 886 B JS / 1.7 KB байткода |
| Выполнение бандла | ~390 µs | ~95 µs |
| Время JS на одно нажатие (медиана) | ~14 µs | ~5 µs |
| Коммит одного нажатия | ~22 µs | ~15 µs |
| Прокрутка списка из 1000 строк, JS на кадр с перепривязкой | ~150 µs | ~77 µs |

То же верно и для компонентов: кнопка, написанная как компонент на Rust, в сравнении с эквивалентной кнопкой на TSX, по 200 штук:

| | Кнопка на Rust: JS / ядро | Кнопка на TSX: JS / ядро |
|---|---|---|
| Первый кадр (операции) | 1.32 ms / 0.77 ms (1015) | 2.32 ms / 0.70 ms (2415) |
| Обновление всех 200 заголовков (операции) | 0.42 ms / 0.42 ms (200) | 0.77 ms / 0.36 ms (600) |

(Измерено на Apple silicon; вторая таблица — на прежней среде выполнения. Обе взяты из базовых показателей производительности NSH.)

**Для вашего кода:** держите обработчики событий и вычисляемые значения лёгкими. Тяжёлые вычисления (разбор больших объёмов данных, сортировка тысяч элементов при каждом нажатии клавиши) в QuickJS обходятся дороже, чем вы, возможно, привыкли в V8 или JavaScriptCore с JIT. Выполните их один раз, закэшируйте в `computed` или вынесите с пути взаимодействия.

<a id="2-cross-threads-in-batches"></a>

### 2. Пересекайте границу потоков пакетами

Код вашего приложения, на Rust или JS, выполняется в потоке движка и никогда в главном потоке. Один такт, например один обработчик события со всеми его изменениями, превращается в **один коммит**: среда выполнения рендерит компоненты, выполняет раскладку, сравнивает и формирует один пакет нативных операций над представлениями, который попадает в главный поток за **один переход**. Нет ни сообщения моста на каждое представление, ни сериализации на каждое свойство.

**Для вашего кода:** вам не нужно объединять записи ради производительности; десять вызовов `set` в одном обработчике всё равно дают один коммит, на Rust и на TSX. Используйте `batch`, когда хотите, чтобы эффекты и вычисляемые значения увидели все записи сразу, а не выполнялись после каждой из них.

<a id="3-fine-grained-updates"></a>

### 3. Мелкозернистые обновления

Компоненты выполняются **один раз**, и на Rust, и на TSX. После этого заново выполняются только привязки, которые читают сигнал, когда он меняется: текст, свойство стиля, ветка `when` (`<If>`), строки `each` (`<Each>`). Ничто не перерисовывает целое поддерево, чтобы выяснить, что изменилось.

```rust
use nsh_core::prelude::*;

#[derive(Clone, PartialEq)]
struct Quote {
    price: f64,
    change: f64,
}

fn price(quote: Signal<Quote>) -> impl IntoElement {
    // Runs once. When the quote changes, only the text and the color update.
    text(move || format!("{:.2}", quote.with(|q| q.price))).text_color(move || {
        if quote.with(|q| q.change < 0.0) { rgb(0xcf222e) } else { rgb(0x1a7f37) }
    })
}
```

```tsx
import type { Accessor } from "nsh";
import { Text } from "nsh";

type Quote = { price: number; change: number };

function Price(props: { quote: Accessor<Quote> }) {
  // Runs once. When quote() changes, only the text and the color update.
  return (
    <Text style={{ color: props.quote().change < 0 ? "#cf222e" : "#1a7f37" }}>
      {props.quote().price.toFixed(2)}
    </Text>
  );
}
```

**Для вашего кода:**

- Читайте сигналы там, где они используются (внутри привязок, замыканий `move ||` в Rust и JSX-выражений в TSX, а также в `computed` и `effect`), а не в начале компонента, чтобы обновлялась самая узкая привязка.
- Используйте `each` / `<Each>` (строки сохраняются по ключу) или [виртуальный список](https://nsh.dev/translations/ru/docs/lists.md) для коллекций; строки, заново создаваемые из всей коллекции (`dynamic` в Rust, обычный `.map()` в JSX), при каждом изменении создаются все заново.
- Выводите значения через `computed`, когда их используют несколько привязок, чтобы они вычислялись один раз на изменение.

Добавление четвёртого элемента в список из трёх, как это показывает `nsh test --trace`:

```rust
// 22 mount items: 3 views removed, 4 created
dynamic(move || items.get().into_iter().map(|n| text(format!("row {n}"))).collect::<Vec<_>>())
// 4 mount items: 1 view created
each(move || items.get(), |n: &u32| *n, |n, _| text(move || format!("row {}", n.get())))
```

```tsx
{items().map((n) => <Text>{`row ${n}`}</Text>)}                   // 22 mount items: 3 views removed, 4 created
<Each of={items()}>{(n) => <Text>{`row ${n()}`}</Text>}</Each>      // 4 mount items: 1 view created
```

См. [Reactivity](https://nsh.dev/translations/ru/docs/reactivity.md).

<a id="4-do-not-send-what-is-not-needed"></a>

### 4. Не отправляйте лишнего

Ядро отправляет только то, что изменилось: новые представления не получают стиль по умолчанию и пустую маску событий, неизменённые стили и фреймы не отправляются, смена цвета текста не приводит к повторной раскладке текста. Нажатие, меняющее одно число, отправляет один текстовый элемент. Это видно в выводе `nsh test --trace` (ниже).

<a id="5-animate-natively"></a>

### 5. Анимируйте нативно

Анимация, управляемая из потока движка (сигнал, записываемый в каждом кадре), стоит коммита на кадр и вызова JS в TSX и подтормаживает, когда поток движка занят. Анимированные значения ([Animation](https://nsh.dev/translations/ru/docs/animation.md): `animated_value` в Rust, [API анимации `nsh`](https://nsh.dev/translations/ru/modules/nsh.md#animation) в TSX) вместо этого запускают анимации нативно:

```rust
use nsh_core::prelude::*;

let scroll_y = animated_value(0.0);

view()
    .bind_opacity(&scroll_y.interpolate(&[0.0, 120.0], &[1.0, 0.35]))
    .bind_translate_y(&scroll_y.interpolate(&[0.0, 120.0], &[0.0, -6.0]));
virtual_list(move || photos.with(Vec::len), move |at| photo_row(photos, at))
    .scroll_value(&scroll_y)
    .estimated_item_size(px(64.0))
    .flex_1();
```

```tsx
import { List, View } from "nsh";
import { createAnimatedValue } from "nsh";

const scrollY = createAnimatedValue(0);

<View
  style={{
    opacity: scrollY.interpolate({ inputRange: [0, 120], outputRange: [1, 0.35] }),
    translate: [0, scrollY.interpolate({ inputRange: [0, 120], outputRange: [0, -6] })],
  }}
/>;
<List each={photos()} scrollValue={scrollY} estimatedItemSize={64} style={{ flex: 1 }}>
  {(photo) => <Row photo={photo()} />}
</List>;
```

- `timing`, `spring` и `decay` выполняются в UI-потоке платформы, кадр за кадром, без работы в потоке движка на каждый кадр.
- `scroll_value` (`scrollValue`) записывает смещение прокрутки в анимированное значение в том же кадре; жест панорамирования (`pan={{ x, y }}`) позволяет перетаскиванию двигать представление напрямую. Ваш код узнаёт о завершении (анимация завершена, по окончании панорамирования передаётся скорость).
- `Animated.sequence` / `parallel` / `stagger` / `loop` выполняются как одно нативное дерево: каждый шаг начинается в момент окончания предыдущего, без паузы в ожидании JS.
- `LayoutAnimation.configureNext` нативно анимирует фреймы, вставки и удаления следующего коммита.

<a id="6-virtualize-long-lists"></a>

### 6. Виртуализируйте длинные списки

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

**Для вашего кода:**

- Задавайте `estimated_item_size` (`estimatedItemSize`) близким к реальной высоте строки.
- Храните состояние каждого элемента в ваших данных (по id), а не в сигналах, созданных внутри строки: ячейки переиспользуются для других элементов, и позиция строки (`at.index()`; `item()` / `index()` в TSX) меняется вместе с этим.
- Делайте строки дешёвыми: строка привязывается один раз на каждое переиспользование, поэтому её код выполняется, пока пользователь прокручивает.
- Используйте `scroll_value` (`scrollValue`) вместо `on_scroll` (`onScroll`) для эффектов, следующих за положением прокрутки. Слушатель прокрутки получает не более одного события за такт движка, но каждое из них всё равно является вызовом вашего кода (в JS, в случае TSX).

<a id="measuring"></a>

## Измерения

<a id="headless-50"></a>

### Без интерфейса: `--trace`

В тесте на Rust `EngineConfig::trace` выводит те же строки `perf`, а `Headless::observe` видит каждый пакет ([Testing](https://nsh.dev/translations/ru/docs/testing-and-trace.md#tracing-with-trace)). `nsh test --trace` выводит их для TSX:

```sh
echo 'await app.press("Add")' | nsh test App.tsx --trace                 # first frame + a press
echo 'await app.scroll("id:feed", 3000)' | nsh test App.tsx --trace       # scrolling, 10 pt per frame
```

```text
perf commit: 56 ops, 395µs (list js 6µs), 49 mount items, 21 nodes
perf first frame: eval 271µs, total 1235µs
perf event js: 88µs
perf commit: 10 ops, 151µs (list js 2µs), 8 mount items, 27 nodes
```

На что смотреть:

- **ops** (операции, поставленные в очередь вашим JS) и **mount items** (нативные операции над представлениями): они детерминированы. Если нажатие, которое должно менять одну метку, порождает 40 элементов, значит, что-то пересоздаёт представления, которые следовало обновить.
- **event js**: стоимость JS обработчика и того, что он вызвал.
- **commit**: стоимость работы ядра по рендерингу компонентов, раскладке и сравнению.
- **list js**: время JS, потраченное на привязку строк списка во время этого коммита.

Полное руководство: [Testing](https://nsh.dev/translations/ru/docs/testing-and-trace.md).

<a id="on-a-device-55"></a>

### На устройстве: `--trace`

```sh
nsh run App.tsx --trace
nsh run App.tsx --target android --trace --release
```

выводит те же строки коммитов из реального хоста приложения на TSX с собственным текстовым движком платформы. `NshAppView(trace: true)` и `new NshView(context, true)` делают то же самое во встраивающем приложении.

<a id="on-screen-58"></a>

### На экране: `--fps`

```sh
nsh run App.tsx --target macos --fps --release
```

(`AppOptions { fps: true, .. }` в `run_app` приложения на Rust) показывает частоту кадров и стоимость кадров и коммитов поверх работающего приложения, а также CPU, GPU и память ([macOS: performance monitor](https://nsh.dev/translations/ru/docs/macos.md#performance-monitor)). На Linux и Windows `--target linux --fps` и `--target windows --fps` выводят те же показания раз в секунду со временем каждой фазы кадра; `nsh test --target linux --fps -v` (или `windows`) снимает их, пока тест управляет приложением.

> [!TIP]
> Единичные измерения сильно зависят от состояния частоты процессора: поток, который только что спал, какое-то время работает медленнее. Сравнивайте минимум из нескольких запусков на одной и той же машине и для замеров времени предпочитайте релизные сборки.

<a id="reference-numbers"></a>

## Справочные числа

Измеренные числа, базовые показатели NSH и галерея страница за страницей на Linux приведены в [отчёте о производительности](https://nsh.dev/translations/ru/docs/performance-report.md).

<a id="checklist"></a>

## Чек-лист

- Язык: пишите приложение или наиболее используемые его части на Rust ([Building with Rust](https://nsh.dev/translations/ru/docs/rust.md)); TSX оставьте для того, что часто меняется или доставляется во время выполнения.
- Взаимодействия: нажатие меняет несколько элементов монтирования, а не десятки.
- Списки: виртуальный список (`virtual_list`, `List`) для всего длинного, с оценочным размером элемента; состояние каждого элемента — в данных.
- Анимации: анимированные значения, а не сигналы, записываемые в каждом кадре.
- Эффекты прокрутки: `scroll_value` (`scrollValue`), а не слушатель прокрутки.
- Обработчики: никакой тяжёлой работы на каждое нажатие клавиши или событие прокрутки; кэшируйте в `computed`.

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

## См. также

- [Отчёт о производительности](https://nsh.dev/translations/ru/docs/performance-report.md)
- [Architecture](https://nsh.dev/translations/ru/docs/architecture.md)
- [Testing](https://nsh.dev/translations/ru/docs/testing-and-trace.md)
- [Lists](https://nsh.dev/translations/ru/docs/lists.md), [Animation](https://nsh.dev/translations/ru/docs/animation.md), [Reactivity](https://nsh.dev/translations/ru/docs/reactivity.md)
