Vize

Desempenho

⚠️ Trabalho em andamento: O Vize está em desenvolvimento ativo e ainda não está pronto para uso em produção. Os números de benchmark vêm de builds de desenvolvimento e podem mudar.

O Vize alcança melhorias significativas de desempenho em relação ao compilador padrão baseado em JavaScript do Vue ao aproveitar as abstrações de custo zero e o multithreading nativo da Rust. Velocidade não é algo agradável — é um pré-requisito para a experiência do desenvolvedor.

Ambiente de medição

Um único artefato versionado alimenta o README e todos os idiomas. A quantidade de arquivos é indicada em cada linha.

Ambiente Data Medições Aquecimentos
blacksmith-32vcpu-ubuntu-2404 (32 CPU) 2026-09-16T08:19:25.262Z 3 1

Actions · f9cc50ee2e2f · JSON

As medições locais de linter, formatador e os perfis abaixo são históricos e não pertencem a este snapshot. Ambiente registrado: MacBook Pro M2 Max (12 núcleos, 96 GB RAM), macOS 15.3.2, Node v24.14.0, Vite v8.0.0 e Vue v3.6.0-beta.10.

Benchmark: compilação SFC

Arquivos: 3,000 (12,302,100 bytes).

@vue/compiler-sfc Vize Razão
1T 3.57s 835.3ms 4.3x
workers / max 1.61s 65.0ms 24.7x
1T / max 3.57s 65.0ms 54.8x

Metodologia e resultados completos

Versões das ferramentas e checksums
Ferramenta Versão SHA-256 do binário
Vize vize 0.424.11 dc5e840133beec86106ca27c496ca01fbff4299a8aef6c3a0f2fca3131f88fe3
tsgo Version 7.0.2 4f2de678286401759b3fb4475bafe35b8f32b4b3a07d92642bbf37eadc9b34a4
vue-tsc Version 6.0.3 811cc8124f36b6c770964d486da62af1b9db6949f62bc47205982d17e8e7dc8d
verter-tsc verter-tsc 0.0.1-beta.3 81bec5c957487bd2caf15312bf4f180afadd074f11c1d34cb4bb2c66bd7ac224
Golar Golar version 0.1.10 a100c0e559b6cf85fc3e67a65f0b8eedcee0ccf624022fc4882bea2a3d6316da
TypeScript (vue-tsc) n/a n/a
Vue 3.6.0-beta.10 n/a
ESLint v10.4.1 41dd060d45aca4a2144b94862e21e285bbb726b19e61db7a03c5b2e0b67973cc
Prettier 3.8.3 a7a78ad15344be4f14f24832a8c5fa5918bec4339b8d1d0903a5167c34ecb428
Node.js v24.14.0 n/a

Backend: native TypeScript engine ready at /home/runner/_work/vize/vize/node_modules/@typescript/typescript-linux-x64/lib/tsc. Planted-diagnostic gating for the type-check rows lives in tools/benchmarks/scripts/check-gate.mjs (.github/workflows/check-bench.yml).

Por que Rust?

Abstrações de custo zero

O modelo de propriedade do Rust elimina pausas de coleta de lixo. Os nós da AST do template vivem em uma arena por compilação (vize_carton) e tomam emprestado seu texto do código-fonte do template, de modo que um nó é dado puro, sem alocações de heap próprias (crates/vize_relief/src/relief/elements.rs). Isso significa:

  • Sem pausas de GC — Em compiladores baseados em V8, a coleta de lixo pode causar picos imprevisíveis de latência. O Vize não tem overhead de GC.
  • Sem aquecimento JIT — O compilador JIT do V8 precisa de tempo para otimizar os caminhos quentes. O Vize roda em velocidade máxima desde a primeira instrução.
  • Desempenho previsível — A compilação antecipada do Rust significa que o desempenho é consistente entre execuções, sem depender das heurísticas de otimização do V8.

Multithreading nativo

O Vize usa Rayon para compilação paralela de dados. Cada arquivo SFC é compilado de forma independente, tornando a carga de trabalho trivialmente paralela, e o driver de lote em crates/vize/src/commands/build/runner.rs distribui as entradas planejadas pelo pool:

// crates/vize/src/commands/build/runner.rs — o driver de lote
planned_inputs
    .par_iter()
    .map(|input| compile_file_with_profile(&input.source, compile_settings, &stats))
    .collect()

A arena não é criada aqui. Ela é adquirida onde nasce — nos pontos de entrada de template, script e estilo dentro de vize_atelier_sfc — a partir de um pool por worker:

// por exemplo, crates/vize_atelier_sfc/src/compile.rs
let allocator = vize_carton::pool::acquire();

A abordagem de roubo de trabalho significa que, se um arquivo for significativamente maior que os outros, threads ociosas vão roubar trabalho da fila da thread ocupada, mantendo um balanceamento de carga quase perfeito.

Layout de memória eficiente

O layout de structs e os discriminantes de enum do Rust são compactos. A representação da AST em vize_relief é amigável ao cache, reduzindo gargalos de largura de banda de memória:

  • Discriminantes de um byte — NodeType é um #[repr(u8)] com 27 variantes (crates/vize_relief/src/relief/core.rs), então o tipo de um nó custa um byte, não uma string alocada no heap.
  • Tamanhos de nó fixados — cada nó de template carrega uma asserção const de tamanho, de modo que um campo que faz o nó crescer quebra a build em vez do orçamento. ElementNode tem 104 bytes, SimpleExpressionNode 88, AttributeNode 56, TextNode 24 e SourceLocation 8 (crates/vize_relief/src/relief/{elements,expressions,control_flow,nodes}.rs).
  • Sem cabeçalhos de objeto — Ao contrário dos objetos JavaScript (que carregam cadeias de protótipos, mapas de propriedades e ponteiros de classe oculta), structs Rust são dados puros com overhead zero.

Sem overhead de tempo de execução

Diferente dos compiladores baseados em JavaScript que rodam na V8, o Vize compila diretamente para código nativo. Não há aquecimento JIT, nem coletor de lixo, nem contenção de loop de eventos. A CLI é distribuída como um executável nativo autocontido por plataforma — totalmente estático nos alvos Linux musl, o que a CI verifica (tools/commands/ci/github/verify-musl-cli-binary.rs), e ligado dinamicamente à biblioteca C do sistema nos alvos glibc, macOS e Windows. O plugin do Vite carrega o mesmo compilador como um addon nativo do Node (@vizejs/native), e não como um processo separado.

Escolhas de Arquitetura para Desempenho

Alocação em Arena

vize_carton::Allocator é um alocador de bump para nós da AST que encapsula o oxc_allocator, de modo que os nós de template e as expressões JavaScript retidas compartilham uma arena e um tempo de vida (davinci/vize_l0/src/allocator.rs). Isso significa:

  • A alocação é O(1) — Basta avançar um ponteiro. Sem percorrer listas livres, sem gerenciamento de fragmentação.
  • A recuperação é O(1) e reutilizada — Ao fim de uma compilação a arena sofre reset(), não é descartada: o ponteiro de bump volta ao início do bloco e a arena retorna a uma lista livre por worker (davinci/vize_l0/src/pool.rs, limitada a 4 arenas ociosas por worker). O arquivo seguinte reutiliza a mesma memória em vez de pedir mais ao sistema operacional.
  • A localidade de memória é excelente — Os nós são empacotados de forma contígua na memória, maximizando os acertos de cache L1/L2 durante a travessia da árvore.

Valores apoiados na arena não podem sobreviver à sua compilação. Esse contrato é imposto pelo compilador (reset recebe &mut self, e o guard do pool é dono da sua arena) e, em builds de depuração, por um carimbo de geração que causa panic se um valor for lido depois que sua arena foi reciclada (davinci/vize_l0/src/allocator/generation.rs).

Nada na AST implementa Drop — os tipos de contêiner da arena rejeitam payloads que precisem ser destruídos, então isso é um erro de compilação, e não uma convenção.

Tokenizador de passagem única

O tokenizador do vize_armature é uma máquina de estados orientada a bytes sobre &[u8] (crates/vize_armature/src/tokenizer.rs). Ele nunca materializa um token: não existe tipo Token nem vetor de tokens em lugar algum do compilador. Em vez disso, tokenize() executa uma passagem até o fim da entrada e envia eventos a um receptor Callbacks, que o parser implementa — assim cada evento é tratado de forma síncrona conforme é produzido, e o array intermediário que um projeto de duas fases exigiria simplesmente nunca existe.

Note que isso é baseado em push, não em uma leitura preguiçosa: o parser não pede tokens e não pode interromper o laço no meio.

Internação de strings

Nomes que se repetem dentro de uma compilação — nomes de diretiva normalizados, nomes de assets, nomes de argumento em camelCase — são internados em átomos apoiados na arena por vize_carton::interner, com um conjunto phf de tempo de compilação com 181 nomes bem conhecidos (tags HTML/SVG/MathML, componentes embutidos do Vue, nomes de diretiva e os atributos que as transformações tratam de forma especial) que resolvem para literais 'static sem tocar a arena. Isso significa:

  • Nomes computados repetidos compartilham uma única alocação na arena
  • As buscas por nomes bem conhecidos são um hash perfeito de tempo de compilação, sem alocação

A internação é o caminho de fallback, não o caso comum. A maioria dos nomes nunca é copiada: um nome de tag, um nome de atributo e a maior parte do conteúdo de expressões são fatias &'a str emprestadas diretamente do código-fonte do template, então o caminho comum não aloca nada (davinci/vize_l0/src/interner.rs documenta a política campo a campo).

Átomos são &'a str comuns, então comparações de nome são comparações de conteúdo, não de identidade de ponteiro. A internação compra economia de alocação e localidade de cache — ela não é um atalho rápido para ==.

Compilação Incremental

O plugin do Vite (@vizejs/vite-plugin) faz cache em nível de arquivo, em duas camadas com chaves diferentes:

  • Em memória, para dev e HMR — indexado pelo caminho resolvido do arquivo (npm/builder/vite/src/plugin/compiled-module-cache.ts). As entradas são removidas explicitamente em hot update, em vez de reindexadas, de modo que um arquivo alterado é recompilado e seus vizinhos não.
  • Detecção de mudança na pré-compilação — indexada por mtime + tamanho, comparados em Rust (crates/vize_atelier_sfc/src/vite_plugin/precompile.rs). É esse portão que decide quais arquivos um lote recompila.
  • Em disco, entre processos — node_modules/.vize/vite-precompile, indexado por um hash SHA-256 do código-fonte mais uma chave de manifesto que cobre a identidade do binário do compilador e as opções resolvidas (npm/builder/vite/src/plugin/precompile-cache-key.ts). O hash de conteúdo é usado aqui justamente porque mtime não é confiável entre máquinas e checkouts.

Medido: trabalho de arena e de expressões

O trabalho interno do compilador descrito acima é medido por um harness de microbenchmarks por crate (cargo bench --bench davinci) sobre uma escada fixa de seis fixtures, tools/benchmarks/crates/davinci_harness/fixtures/{small,medium,large,stress-deep,stress-wide,stress-interp}.vue.

Como ler estes números. As contagens de alocação são determinísticas e independentes de máquina, portanto são fatos exatos e servem como catraca de regressão. Os tempos de parede foram tomados em uma máquina de desenvolvimento compartilhada com amostragem --quick e são apenas indicativos — as gravações no runner de referência (Blacksmith) ainda estão pendentes, e é por isso que cada entrada wall_p50_ns e allocs em docs/davinci/plan/budgets.toml continua em 0, significando "ainda não gravado, apenas informativo". Os arquivos de resultado de cada execução caem em tools/benchmarks/results/davinci/ e são artefatos locais, não baselines versionadas.

Chamadas de alocação por compilação, antes e depois do trabalho de strings e arena (exatas, mesmas fixtures):

Fixture Parse Compilação DOM Compilação SSR Compilação Vapor
small 21 → 9 52 → 39 73 → 60 90 → 73
medium 171 → 107 329 → 264 1.099 → 1.030 588 → 515
large 350 → 272 656 → 573 1.106 → 983 1.136 → 1.003
stress-deep 397 → 155 669 → 426 612 → 369 764 → 514
stress-wide 213 → 204 255 → 245 416 → 405 280 → 261
stress-interp 616 → 105 1.048 → 536 3.149 → 2.637 1.495 → 974

Os tamanhos dos nós encolheram junto, e os novos tamanhos estão fixados por asserções const: RootNode 296 → 224 bytes, DirectiveNode 208 → 176, ElementNode 128 → 104, SimpleExpressionNode 120 → 88, AttributeNode 80 → 56, TextNode 32 → 24.

Pico de memória residente. A reutilização da arena entre arquivos é o maior ganho isolado, e é um resultado de memória, não de velocidade. Compilando todos os 36.541 SFCs do corpus versionado (vize build "tests/_fixtures/_git/**/*.vue" --format stats, binários ci-opt, tamanho máximo do conjunto residente via /usr/bin/time -l, mesma máquina antes e depois):

Workers Antes Depois Variação Execuções cada
12 766,5 MB 171,1 MB −77,7% 5
1 717,0 MB 88,2 MB −87,7% 3

O número com um único worker é o sinal de acumulação: ele independe de escalonamento, mostrando que o pico antigo vinha de vazamento por arquivo, e não das arenas por worker. O tempo de parede ficou inalterado dentro do ruído, e todos os 36.541 arquivos emitidos foram idênticos byte a byte (manifestos SHA-256 comparados).

Reanálise de expressões. As expressões de template agora são analisadas uma única vez, durante a análise do template, e retidas no nó. Os consumidores leem a AST retida em vez de reanalisar o texto. Na trilha SSR, a fixture stress-interp passou de 500 reanálises redundantes de expressão por compilação para zero, e essa trilha fundida está −13,6% líquidos em tempo de parede em relação à árvore anterior à retenção (346,8µs → 299,8µs) — a análise agora custa mais e os consumidores custam muito menos. As trilhas DOM e Vapor não tinham reanálises a excluir nessa fixture, então ainda carregam o custo de análise adicionado; fechar essa lacuna é rastreado como trabalho restante da fase, não como um ganho já entregue.

Benchmark: Linter — patina vs eslint-plugin-vue

Analisando 15.000 arquivos Vue SFC, estação de trabalho local:

eslint-plugin-vue (ST) Pátina Vize (ST) Aceleração eslint-plugin-vue (MT) Pátina Vize (MT) Aceleração eslint ST vs Vize MT
Tempo 45,08s 4,02s 11,2x 16,38s 784ms 20,9x 57,5x

Correr vp run --workspace-root bench:lint para se reproduzir.

Perfil de fiapos consciente do tipo

O linting consciente de tipos é intencionalmente perfilado nas fases em que o custo tende a se agrupar: análise sintática SFC, análise Croquis, geração virtual de TypeScript, coleta de consultas de templates e sondas Corsa. Quando múltiplas regras de type-aware apoiadas por templates são ativadas, o Patina coleta a expressão do template e consultas Promise template em uma única caminhada AST antes da fase de sonda Corsa. A coleção de consultas também compartilha análise de expressões OXC para verificações unsafe-template e floating-Promise, de modo que uma expressão template não paga custo de análise duplicado quando ambas as regras estão ativadas.

Faça vize lint --profile --preset opinionated src para ver essas linhas em um projeto local. O relatório de perfil também inclui uma seção rigorosa de auditoria que verifica a cobertura de tempo de parede, o tempo acumulado trabalhador, os acertos de limiar lento e os períodos internos capturados antes de listar arquivos quentes e operações internas de . Linhas de arquivo quente mostram participação e throughput por estágio, e linhas de operação sinalizam spans dominantes de ou picos máximos/médios.

Benchmark: Formatter — glifo vs Pretier

Formatação de 15.000 arquivos Vue SFC, estação de trabalho local:

Mais bonita (CLI) Glifo Vize (ST) Aceleração Glifo Vize (MT) Cli mais bonito vs Vize MT
Tempo 101,20s 2,97s 34,1x 835ms 121,2x

Correr vp run --workspace-root bench:fmt para se reproduzir.

Benchmark: verificação de tipos

Um único artefato versionado alimenta o README e todos os idiomas. A quantidade de arquivos é indicada em cada linha.

Ambiente Data Medições Aquecimentos
blacksmith-32vcpu-ubuntu-2404 (32 CPU) 2026-09-16T08:19:25.262Z 3 1

Actions · f9cc50ee2e2f · JSON

Medição Arquivos Ferramenta comparada Mediana comparada Vize 1T Vize max Razão
Verificação de tipos 500 vue-tsc 6.75s 2.03s 1.37s 4.9x
Tipos em SFC grande 1 vue-tsc 1.34s 186.4ms 190.1ms 7.0x

As razões da verificação de tipos comparam Vize e vue-tsc, o verificador que os projetos Vue usam hoje. O vue-tsc executa o compilador TypeScript em JavaScript enquanto o Vize executa o tsgo nativo, portanto a razão cobre toda a cadeia de ferramentas e não apenas a camada Vue; as tabelas por classe de motor do instantâneo completo classificam ferramentas do mesmo motor entre si para isolar essa camada. Cada execução cronometrada valida o trabalho de diagnóstico, mas a cobertura varia entre ferramentas. Essas razões não demonstram precisão equivalente nem uma melhoria sobre uma versão anterior do Vize. Comparadores rejeitados não recebem tempo publicado nem classificação.

Metodologia e resultados completos

Os perfis abaixo são medições locais históricas e não fazem parte desta comparação.

Perfil do verificador de tipos

O dispositivo de perfil 500-SFC mantém a maior parte do tempo de parede dentro do comando CLI Corsa, enquanto o caminho rápido de importação remove o custo anterior de análise OXC para arquivos sem especificadores Vue:

Métrica Antes Atualidade
canon.import.rewrite.vue 26,77ms 2,45ms
Maior TS Virtual gerado 15.401B 14.414B
Tempo total na parede do perfil 1,88s 668ms
Fase de diagnóstico da Corsa 1,67s 482ms
Análise do CLI Corsa N/A 10,41ms

A fase de virtual project do lado Rust — análise SFC por arquivo, análise Croquis geração de Virtual TS e reescrita de importação — é distribuída pelo thread pool do rayon dentro de VirtualProject::register_paths. Cada arquivo .vue é independente uma vez que as opções do workspace são resolvidas, então um único lote paraleliza de forma limpa. Em um dispositivo de 1.000 SFC, a fase cai de ~71 ms para ~25 ms antes mesmo de Corsa ser ativada.

Luminária e2e com muitos diagnósticos

tools/benchmarks/scripts/check.ts também mede o aplicativo tests/_fixtures/_git/npmx.dev quando o aparelho está presente. Isso captura o caminho de mapeamento de diagnóstico em um dispositivo real de aplicação:

Jogos Arquivos SFC fonte Arquivos virtuais Diagnósticos Cânone Vize
npmx.dev aplicativo 134 226 1,053 1,94s

O perfil atual desse aparelho mantém a análise diagnóstica do CLI em ~7ms. A maior parte do tempo agora está no comando CLI da Corsa. A autoimportação de stubs do framework em um único arquivo ambiente também reduziu o maior arquivo Virtual TS gerado de cerca de 275KB para 144KB.

Benchmark: plugin Vite

Um único artefato versionado alimenta o README e todos os idiomas. A quantidade de arquivos é indicada em cada linha.

Ambiente Data Medições Aquecimentos
blacksmith-32vcpu-ubuntu-2404 (32 CPU) 2026-09-16T08:19:25.262Z 3 1

Actions · f9cc50ee2e2f · JSON

Arquivos: 1,000.

@vitejs/plugin-vue @vizejs/vite-plugin Razão
Build Vite 1.66s 889.2ms 1.9x

Metodologia e resultados completos

O número publicado aqui até agora — 957ms / 479ms / 2.0x — veio de tools/benchmarks/scripts/vite.ts antes de #3392, que media o Vize com um cache de pré-compilação persistente deixado quente pelo próprio warmup enquanto o @vitejs/plugin-vue compilava do zero. Esse harness agora reporta linhas separadas de cold e warm na máquina em que roda, então produz um diagnóstico local, não um speedup publicável. Use vp run --workspace-root bench:vite para comparar uma mudança consigo mesma.