Эта страница переведена машинным способом и может содержать ошибки. Читать оригинал на английском

Документация

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

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

#Правила

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

Приложение на Rust — это нативный код от начала до конца: его компоненты, состояние и обработчики компилируются движком NSH, и никакой интерпретатор не запускается (Building with Rust). Это самый быстрый способ написать приложение, и именно его стоит выбирать для требовательной бизнес-логики.

В приложении на 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 или вынесите с пути взаимодействия.

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

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

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

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

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

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) }
    })
}

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

  • Читайте сигналы там, где они используются (внутри привязок, замыканий move || в Rust и JSX-выражений в TSX, а также в computed и effect), а не в начале компонента, чтобы обновлялась самая узкая привязка.
  • Используйте each / <Each> (строки сохраняются по ключу) или виртуальный список для коллекций; строки, заново создаваемые из всей коллекции (dynamic в Rust, обычный .map() в JSX), при каждом изменении создаются все заново.
  • Выводите значения через computed, когда их используют несколько привязок, чтобы они вычислялись один раз на изменение.

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

// 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())))

См. Reactivity.

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

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

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

Анимация, управляемая из потока движка (сигнал, записываемый в каждом кадре), стоит коммита на кадр и вызова JS в TSX и подтормаживает, когда поток движка занят. Анимированные значения (Animation: animated_value в Rust, API анимации nsh в TSX) вместо этого запускают анимации нативно:

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

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

List хранит только строки рядом с областью просмотра, переиспользует их ячейки и вычисляет позиции, колонки, закреплённые заголовки и видимость нативно. Кадры прокрутки, в которых не появляется новая строка, не выполняют ваш код и не создают пакет; кадр, привязывающий одну новую строку списка фотографий в галерее, — это 4 операции и 4 элемента монтирования.

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

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

#Измерения

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

В тесте на Rust EngineConfig::trace выводит те же строки perf, а Headless::observe видит каждый пакет (Testing). nsh test --trace выводит их для TSX:

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
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.

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

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

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

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

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

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

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

Измеренные числа, базовые показатели NSH и галерея страница за страницей на Linux приведены в отчёте о производительности.

#Чек-лист

  • Язык: пишите приложение или наиболее используемые его части на Rust (Building with Rust); TSX оставьте для того, что часто меняется или доставляется во время выполнения.
  • Взаимодействия: нажатие меняет несколько элементов монтирования, а не десятки.
  • Списки: виртуальный список (virtual_list, List) для всего длинного, с оценочным размером элемента; состояние каждого элемента — в данных.
  • Анимации: анимированные значения, а не сигналы, записываемые в каждом кадре.
  • Эффекты прокрутки: scroll_value (scrollValue), а не слушатель прокрутки.
  • Обработчики: никакой тяжёлой работы на каждое нажатие клавиши или событие прокрутки; кэшируйте в computed.

#См. также