Esta página foi traduzida automaticamente e pode conter erros. Ler o original em inglês

Documentação

Desempenho

Performance é a prioridade número um do NSH, e a maior parte dela é decidida pelo framework, não pelo seu app: o runtime é nativo, cada atualização cruza threads uma única vez, animações e virtualização de listas rodam sem JS, e um app escrito em Rust não executa JS algum. Esta página explica as regras que o NSH segue, o que elas significam para o código que você escreve, em Rust e em TSX, e como medir.

#As regras

#1. O JS faz o mínimo possível

Um app escrito em Rust é código nativo do início ao fim: seus componentes, estado e handlers são compilados com o engine do NSH, e nenhum interpretador é executado (Desenvolvendo com Rust). Essa é a forma mais rápida de escrever um app, e a indicada para lógica de negócio exigente.

Em um app TSX, o QuickJS é um interpretador: pequeno, rápido para iniciar, e muito mais lento que código nativo em trabalho pesado. Por isso, no NSH, somente o código do seu app é JS. O grafo reativo (signals, valores computed, effects), o renderer que o seu JSX compilado chama, o motor de layout, a virtualização de listas, a animação e todo módulo que você importa são Rust. Parsing, diffing, layout e processamento de strings acontecem de forma nativa.

O efeito é mensurável. O mesmo app de contador no runtime JS anterior do NSH e no runtime nativo (builds de release):

Runtime JS anterior Runtime nativo
Tamanho do bundle 14 KB de JS / 19,6 KB de bytecode 886 B de JS / 1,7 KB de bytecode
Avaliação do bundle ~390 µs ~95 µs
Tempo de JS de um toque (mediana) ~14 µs ~5 µs
Commit de um toque ~22 µs ~15 µs
Rolagem de uma lista de 1000 linhas, JS por frame de rebinding ~150 µs ~77 µs

O mesmo vale para componentes: um botão escrito como componente Rust contra um botão TSX equivalente, 200 de cada:

Botão Rust: JS / core Botão TSX: JS / core
Primeiro frame (ops) 1,32 ms / 0,77 ms (1015) 2,32 ms / 0,70 ms (2415)
Atualização dos 200 títulos (ops) 0,42 ms / 0,42 ms (200) 0,77 ms / 0,36 ms (600)

(Medido em Apple silicon; a segunda tabela no runtime anterior. Ambas vêm das baselines de performance do NSH.)

Para o seu código: mantenha handlers de eventos e valores computed leves. Computação pesada (parsing de payloads grandes, ordenação de milhares de itens a cada tecla digitada) custa mais no QuickJS do que você talvez esteja acostumado no V8 ou no JavaScriptCore com JIT. Faça-a uma única vez, guarde o resultado em cache em um computed, ou tire-a do caminho da interação.

#2. Cruze threads em lotes

O código do seu app, Rust ou JS, roda em uma thread de engine, nunca na thread principal. Um turno, por exemplo um handler de evento com tudo o que ele altera, vira um commit: o runtime renderiza componentes, faz o layout, calcula o diff e produz um lote de operações de views nativas, que chega à thread principal em um único salto. Não há mensagem de bridge por view nem serialização por prop.

Para o seu código: você não precisa agrupar escritas por questões de performance; dez chamadas a set em um handler ainda produzem um único commit, em Rust e em TSX. Use batch quando quiser que effects e valores computed vejam todas as escritas de uma vez, em vez de rodarem após cada uma.

#3. Atualizações de granularidade fina

Os componentes rodam uma vez, tanto em Rust quanto em TSX. Depois disso, apenas os bindings que leem um signal rodam novamente quando ele muda: um texto, uma propriedade de estilo, um ramo de when (<If>), as linhas de each (<Each>). Nada re-renderiza uma subárvore inteira para descobrir o que mudou.

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

Para o seu código:

  • Leia signals onde eles são usados (dentro de bindings, closures de move || em Rust e expressões JSX em TSX, e em computed e effect), não no topo de um componente, para que o binding mais estreito seja atualizado.
  • Use each / <Each> (linhas mantidas por chave) ou uma lista virtual para coleções; linhas refeitas a partir da coleção inteira (dynamic em Rust, um .map() simples em JSX) são todas recriadas a cada mudança.
  • Derive valores com computed quando vários bindings os usam, para que sejam calculados uma vez por mudança.

Acrescentar um quarto item a uma lista de três itens, como o nsh test --trace mostra:

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

Veja Reatividade.

#4. Não envie o que não é necessário

O core envia apenas o que mudou: views novas não recebem estilo padrão nem máscara de eventos vazia, estilos e frames inalterados não são enviados, e uma mudança na cor do texto não refaz o layout do texto. Um toque que altera um número envia um único item de texto. Você pode ver isso na saída de nsh test --trace (abaixo).

#5. Anime de forma nativa

Uma animação conduzida pela thread de engine (um signal escrito a cada frame) custa um commit por frame, além de uma chamada de JS em TSX, e engasga quando a thread de engine está ocupada. Valores animados (Animação: animated_value em Rust, a API de animação do nsh em TSX) executam as animações de forma nativa:

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 e decay rodam na thread de UI da plataforma, frame a frame, sem trabalho na thread de engine por frame.
  • scroll_value (scrollValue) escreve um offset de rolagem em um valor animado no mesmo frame; um pan (pan={{ x, y }}) permite que arrastes movam uma view diretamente. Seu código é avisado do fim (a animação termina, o fim do pan recebe a velocidade).
  • Animated.sequence / parallel / stagger / loop rodam como uma única árvore nativa: cada etapa começa no instante em que a anterior termina, sem intervalo esperando o JS.
  • LayoutAnimation.configureNext anima de forma nativa os frames, inserções e remoções do próximo commit.

#6. Virtualize listas longas

List mantém apenas as linhas próximas da viewport, recicla suas células e calcula posições, colunas, cabeçalhos fixos e viewability de forma nativa. Frames de rolagem que não revelam nenhuma linha nova não executam código seu e não produzem lote; um frame que faz o binding de uma nova linha da lista de fotos da galeria tem 4 operações e 4 mount items.

Para o seu código:

  • Dê a estimated_item_size (estimatedItemSize) um valor próximo da altura real da linha.
  • Mantenha o estado por item nos seus dados (indexado por id), não em signals criados dentro da linha: as células são reutilizadas para outros itens, e a posição da linha (at.index(); item() / index() em TSX) muda quando isso acontece.
  • Mantenha as linhas baratas: uma linha faz binding uma vez por reciclagem, então seu código roda enquanto o usuário rola.
  • Use scroll_value (scrollValue) em vez de on_scroll (onScroll) para effects que acompanham a posição de rolagem. Um listener de rolagem recebe no máximo um evento por turno do engine, mas cada um ainda é uma chamada ao seu código (ao JS, em TSX).

#Medindo

#Headless: --trace

Em um teste Rust, EngineConfig::trace registra as mesmas linhas de perf e Headless::observe enxerga cada lote (Testes). nsh test --trace as imprime para 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

O que observar:

  • ops (operações que o seu JS enfileirou) e mount items (operações de views nativas): são determinísticos. Se um toque que deveria alterar um único label produz 40 itens, algo está recriando views que deveria atualizar.
  • event js: o custo de JS de um handler e do que ele disparou.
  • commit: o custo do core para renderizar componentes, fazer layout e calcular o diff.
  • list js: JS gasto fazendo o binding das linhas da lista durante aquele commit.

Guia completo: Testes.

#Em um dispositivo: --trace

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

registra as mesmas linhas de commit a partir do host real de um app TSX, com o motor de texto próprio da plataforma. NshAppView(trace: true) e new NshView(context, true) fazem o mesmo em um app embarcador.

#Na tela: --fps

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

(AppOptions { fps: true, .. } no run_app de um app Rust) mostra a taxa de frames e o custo de frames e commits sobre o app em execução, com CPU, GPU e memória (macOS: monitor de desempenho). No Linux e no Windows, --target linux --fps e --target windows --fps registram as mesmas leituras uma vez por segundo, com o tempo de cada fase de um frame; nsh test --target linux --fps -v (ou windows) as captura enquanto um teste conduz o app.

#Números de referência

Números medidos, as baselines do NSH e a galeria página por página no Linux estão no relatório de desempenho.

#Checklist

  • Linguagem: escreva o app, ou as partes mais usadas dele, em Rust (Desenvolvendo com Rust); deixe o TSX para o que muda com frequência ou é entregue em tempo de execução.
  • Interações: um toque altera um punhado de mount items, não dezenas.
  • Listas: uma lista virtual (virtual_list, List) para qualquer coisa longa, com um tamanho de item estimado; estado por item nos dados.
  • Animações: valores animados, não signals escritos a cada frame.
  • Efeitos de rolagem: scroll_value (scrollValue), não um listener de rolagem.
  • Handlers: nenhum trabalho pesado por tecla digitada ou por evento de rolagem; use cache em computed.

#Veja também