Svelte 5 e SvelteKit: storefront reativo e leve com runes
Compilador sem virtual DOM, bundle pequeno e Core Web Vitals que melhoram sua descoberta em buscadores e LLMs.
Alexandre Caramaschi
CEO da Brasil GEO, ex-CMO da Semantix (Nasdaq), cofundador da AI Brasil
O frontend de uma loja brasileira mid-market vive uma tensão diária: precisa de muita interatividade (filtros, variações de produto, carrinho, cupom, frete) e, ao mesmo tempo, precisa carregar rápido no celular de uma rede 4G instável. Quanto mais JavaScript você envia para resolver a interatividade, mais lento fica o primeiro carregamento — e mais você perde em conversão e em descoberta. Svelte 5 ataca exatamente esse ponto: ele move o trabalho de reatividade para o tempo de compilação.
Este guia mostra como construir um storefront reativo com Svelte 5 e SvelteKit, usando as runes novas, SSR de catálogo e form actions, e por que essa combinação ajuda diretamente nos Core Web Vitals e, por consequência, no GEO.
Por que um compilador sem virtual DOM importa para uma loja?
Resposta direta: porque o navegador do seu cliente recebe menos código e faz menos trabalho em tempo de execução. Frameworks baseados em virtual DOM embarcam um runtime que diffa árvores de componentes a cada atualização. Svelte faz esse cálculo durante o build: o compilador analisa onde o estado muda e gera instruções diretas para tocar só os nós do DOM afetados.
Na prática, isso significa um bundle inicial menor e uma thread principal menos ocupada. Em um storefront com grade de produtos, isso aparece no Largest Contentful Paint e no Interaction to Next Paint — duas das métricas que o Google usa como sinal de qualidade e que, indiretamente, influenciam quanto seu conteúdo é exposto.
Um catálogo que pinta rápido e responde ao toque sem travar vai além de boa experiência: vira um sinal de qualidade que motores de busca e modelos generativos usam para decidir se vale a pena indexar e citar a sua loja.
Como modelar estado reativo com as runes do Svelte 5?
Resposta direta: você declara estado com $state, deriva valores com $derived e recebe props com $props. As runes são palavras-chave do compilador prefixadas com $ — você não as importa de lugar nenhum.
Carrinho com quantidade e subtotal
O exemplo abaixo é um componente de item de carrinho. $state torna a quantidade reativa; $derived recalcula o subtotal sempre que a quantidade muda; $props desestrutura o que o componente recebe de fora.
<script lang="ts">
let { nome, precoUnitario } = $props();
let quantidade = $state(1);
let subtotal = $derived(quantidade * precoUnitario);
</script>
<div class="item">
<span>{nome}</span>
<button onclick={() => quantidade = Math.max(1, quantidade - 1)}>-</button>
<strong>{quantidade}</strong>
<button onclick={() => quantidade++}>+</button>
<span>Subtotal: R$ {subtotal.toFixed(2)}</span>
</div>
Repare em dois detalhes idiomáticos do Svelte 5: o evento é onclick={...} (atributo simples, sem dois-pontos), e o subtotal não precisa de nenhuma declaração manual de “isto depende daquilo” — $derived cria a dependência ao ler quantidade e precoUnitario. Quando a quantidade muda, só o texto do subtotal é repintado.
Filtro de produtos com busca
Catálogo sem busca rápida é catálogo que esconde estoque. Aqui, $state guarda o termo digitado, bind:value faz o two-way binding com o input, e $derived produz a lista filtrada. Para lógica que não cabe em uma expressão, use $derived.by.
<script lang="ts">
let { produtos } = $props();
let termo = $state('');
let filtrados = $derived.by(() => {
const q = termo.trim().toLowerCase();
if (!q) return produtos;
return produtos.filter((p) => p.nome.toLowerCase().includes(q));
});
</script>
<input type="search" placeholder="Buscar produto" bind:value={termo} />
<ul>
{#each filtrados as produto}
<li>{produto.nome} — R$ {produto.preco.toFixed(2)}</li>
{/each}
</ul>
A cada tecla, o bind:value atualiza termo, $derived.by recomputa filtrados e a lista repinta. Nenhum addEventListener, nenhuma store, nenhum useMemo. A reatividade fina é mérito do compilador, que sabe exatamente quais nós dependem de filtrados.
Como sincronizar com o navegador sem vazar lógica?
Resposta direta: use $effect para efeitos colaterais — coisas que tocam o mundo fora do componente, como localStorage, analytics ou uma API do navegador. $effect roda depois que o DOM atualiza e reexecuta quando suas dependências mudam.
O caso clássico em e-commerce é persistir o carrinho para que o cliente não perca os itens ao recarregar a página.
<script lang="ts">
let carrinho = $state<Array<{ id: string; qtd: number }>>([]);
// Hidrata uma vez, no cliente.
$effect(() => {
const salvo = localStorage.getItem('carrinho');
if (salvo) carrinho = JSON.parse(salvo);
});
// Persiste sempre que o carrinho muda.
$effect(() => {
localStorage.setItem('carrinho', JSON.stringify(carrinho));
});
</script>
O segundo $effect lê carrinho, então o compilador o reexecuta a cada alteração da lista. Use $effect com parcimônia: ele é a ponte para o lado de fora, não o lugar para calcular valores derivados — para isso existe o $derived. Confundir os dois é o erro mais comum de quem chega ao Svelte 5.
Como o SvelteKit entrega catálogo já renderizado para crawlers e LLMs?
Resposta direta: com a função load() em +page.server.ts, que roda no servidor e alimenta o data da página. O HTML chega ao navegador (e ao crawler) já preenchido — esse é o SSR por padrão do SvelteKit.
O SvelteKit usa roteamento por arquivos em src/routes: +page.svelte é a UI, +page.server.ts carrega dados e trata formulários, +layout.svelte envolve as rotas e +server.ts define endpoints. Veja uma página de categoria que busca produtos no servidor:
// src/routes/categoria/[slug]/+page.server.ts
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params, fetch }) => {
const resposta = await fetch(`https://api.minhaloja.com.br/categorias/${params.slug}/produtos`);
const produtos = await resposta.json();
return { produtos, slug: params.slug };
};
A UI consome data sem se preocupar de onde os dados vieram. Como tudo foi resolvido no servidor, o primeiro HTML já contém os produtos:
<!-- src/routes/categoria/[slug]/+page.svelte -->
<script lang="ts">
let { data } = $props();
</script>
<h1>Categoria: {data.slug}</h1>
<ul>
{#each data.produtos as produto}
<li>{produto.nome} — R$ {produto.preco.toFixed(2)}</li>
{/each}
</ul>
Para o GEO isso é decisivo. Crawlers tradicionais e a maioria dos coletores que alimentam modelos generativos leem o HTML servido sem executar JavaScript pesado. Se o catálogo só existisse depois que o JS rodasse no cliente, boa parte desses agentes veria uma página vazia. Com SSR, eles veem nome, preço e descrição — o material que um motor generativo cita quando alguém pergunta “qual loja vende X no Brasil”.
SSR vai além do Google: trata-se de ser legível para qualquer agente que lê HTML, incluindo os modelos que respondem às perguntas dos seus clientes antes de eles chegarem ao seu domínio.
Como tratar formulários sem quebrar quem está sem JavaScript?
Resposta direta: com form actions em +page.server.ts e a diretiva use:enhance no formulário. A action processa o POST no servidor; o use:enhance adiciona o comportamento progressivo no cliente quando há JS.
Veja uma captura de newsletter — pão com manteiga de qualquer loja que constrói base própria de contatos:
// src/routes/+page.server.ts
import type { Actions } from './$types';
import { fail } from '@sveltejs/kit';
export const actions = {
default: async ({ request }) => {
const form = await request.formData();
const email = String(form.get('email') ?? '');
if (!email.includes('@')) {
return fail(400, { email, erro: 'E-mail inválido.' });
}
// Aqui você grava o contato na sua base.
return { sucesso: true };
}
} satisfies Actions;
No componente, o formulário usa method="POST". Sem JavaScript, o navegador envia o POST e recarrega a página com o resultado. Com use:enhance, o envio é interceptado e a UI atualiza sem recarregar:
<!-- src/routes/+page.svelte -->
<script lang="ts">
import { enhance } from '$app/forms';
let { form } = $props();
</script>
<form method="POST" use:enhance>
<input type="email" name="email" placeholder="Seu e-mail" required />
<button type="submit">Quero novidades</button>
{#if form?.erro}<p class="erro">{form.erro}</p>{/if}
{#if form?.sucesso}<p class="ok">Inscrição confirmada.</p>{/if}
</form>
Esse é o coração do progressive enhancement: a funcionalidade base não depende de JavaScript, e a experiência melhora quando o JS está disponível. Para um checkout, isso significa robustez — o cliente com conexão ruim ou bloqueador agressivo ainda consegue comprar.
Como reaproveitar um card de produto com snippets?
Resposta direta: com {#snippet} para definir um trecho reutilizável e {@render} para renderizá-lo. No Svelte 5, snippets substituem os antigos slots para reúso de marcação dentro de um mesmo componente.
<script lang="ts">
let { produtos } = $props();
</script>
{#snippet cardProduto(p)}
<article class="card">
<h3>{p.nome}</h3>
<p>R$ {p.preco.toFixed(2)}</p>
<button onclick={() => console.log('adicionar', p.id)}>Adicionar</button>
</article>
{/snippet}
<section class="grade">
{#each produtos as produto}
{@render cardProduto(produto)}
{/each}
</section>
O snippet cardProduto recebe argumentos como uma função e é renderizado quantas vezes for preciso. Você evita criar um arquivo de componente separado para um pedaço pequeno de marcação repetida, mantendo a árvore enxuta — o que, de novo, ajuda o tamanho final do bundle.
Quando Svelte é a escolha certa para o seu storefront?
Resposta direta: quando a rota tem muita interação por unidade de conteúdo e o peso do JavaScript no mobile é uma restrição real do seu público. É o caso típico de páginas de listagem com filtros, página de produto com variações e carrinho, e fluxos de checkout.
A decisão honesta passa por comparar com os irmãos de stack. Se a página é majoritariamente conteúdo editorial e marketing com pouca interação, Astro com islands entrega ainda menos JavaScript. Se o seu time já vive em React e o produto é um dashboard pesado de back-office, Next 16 com React 19 traz o ecossistema mais amplo. O comparativo lado a lado está em como escolher a stack de frontend para e-commerce.
Svelte ocupa o meio-termo virtuoso: leve como conteúdo estático, mas confortável com muita interatividade. Para um storefront mid-market que precisa de Core Web Vitals fortes sem renunciar a filtros, carrinho e busca ricos, essa combinação é difícil de bater.
Próximo passo
Comece pequeno: reescreva a sua página de listagem de categoria em SvelteKit com load() no servidor e um filtro client-side com $derived. Meça LCP e INP antes e depois num Android intermediário. Em seguida, aprofunde a arquitetura nestes guias:
- Storefront como app e PWA mobile — como levar essa base reativa para uma experiência instalável.
- Composable, MACH e headless API-first — como o
load()do SvelteKit conversa com um back-end desacoplado. - Busca, navegação e discovery com IA — como evoluir o filtro simples deste guia para descoberta inteligente.
Um storefront que pinta rápido, responde ao toque sem travar e entrega HTML legível no servidor não está só agradando o usuário. Está se tornando a fonte que os motores generativos preferem citar — e essa é a essência do GEO.
Perguntas frequentes
Svelte serve para loja grande ou só para protótipo?
Resposta direta: serve para produção. O diferencial do Svelte aparece justamente em catálogos com muitos componentes interativos, porque o código compilado não carrega o peso de um runtime de virtual DOM. Lojas mid-market brasileiras com milhares de SKUs ganham em tempo de carregamento e em consumo de banda no mobile.
Preciso reaprender tudo por causa das runes do Svelte 5?
Resposta direta: não. As runes são poucos símbolos ($state, $derived, $props, $effect, $bindable) e substituem padrões que antes exigiam stores e declarações reativas implícitas. Quem já escrevia Svelte 4 migra em dias; quem chega agora aprende um modelo mais explícito desde o início.
SvelteKit renderiza no servidor para SEO e GEO?
Resposta direta: sim, por padrão. A função load() em +page.server.ts roda no servidor e entrega o HTML já preenchido com o catálogo. Crawlers tradicionais e motores generativos leem esse conteúdo sem precisar executar JavaScript, o que melhora indexação e citação.
Como o Svelte se compara a Astro e Next no e-commerce?
Resposta direta: Astro brilha em páginas de conteúdo com pouca interação; Next domina dashboards React pesados e ecossistema amplo; Svelte entrega o storefront mais leve quando a página tem muita interatividade (filtros, carrinho, busca). A escolha depende da proporção entre conteúdo e interação na rota.
Form actions funcionam sem JavaScript no cliente?
Resposta direta: sim. Um formulário com method POST e action no +page.server.ts já funciona sem JS. O diretivo use:enhance adiciona a camada progressiva: intercepta o envio, evita o recarregamento e atualiza a UI quando há JS disponível.
Para levar deste guia
-
Svelte 5 compila a reatividade em tempo de build: sem virtual DOM, o bundle entregue ao navegador é menor e o tempo de interação cai.
-
Runes ($state, $derived, $props, $effect) tornam o estado do carrinho e dos filtros explícito e previsível, sem boilerplate de stores.
-
SvelteKit faz SSR por padrão: load() no servidor entrega catálogo já renderizado, o que crawlers e LLMs leem sem executar JavaScript.
-
Form actions com use:enhance dão progressive enhancement: o checkout funciona sem JS e melhora quando há JS.
-
Core Web Vitals fortes não são vaidade técnica: são sinal de qualidade que alimenta descoberta orgânica e citação por motores generativos (GEO).