Email me

Traduzido do original em inglês de 24 de outubro de 2026. Os números e as fontes são os mesmos do original. Leia o original em inglês →

IA · · 14 min de leitura · Matt Busel

Arquivado em LLMs Desenvolvimento de apps Guias

Rode um LLM no navegador: WebGPU fez 63 tokens/s, WASM fez 1,6

Rode um LLM no navegador: WebGPU fez 63 tokens/s, WASM fez 1,6

Resumo: Dá para rodar um modelo de linguagem pequeno inteiramente dentro de uma página web com Transformers.js, um Web Worker de módulo e WebGPU. Fazemos isso no BEINGS, um chat em que as criaturas de IA de cada pessoa pensam no próprio notebook dessa pessoa, então a plataforma paga $0 de inferência. Em 17 de outubro de 2026 medimos tudo de novo: o mesmo modelo de 360M de parâmetros escreveu 63 tokens por segundo em WebGPU e 1,6 no fallback em WASM, um Llama de 1B respondeu em menos de meio segundo, e um Qwen de 0,5B produziu tanta coisa sem sentido que metade das respostas dele numa temperatura mais alta teve de ir para o lixo.

Primeiro a resposta curta, já que foi isso que você pesquisou. Para rodar um LLM no navegador você precisa de três coisas: um modelo já convertido para ONNX e quantizado (pesos de 4 bits, então um modelo de 1B fica com cerca de 1,2 GB em vez de 5), um runtime que o execute na página (Transformers.js, que encapsula o ONNX Runtime Web) e um backend de GPU (WebGPU) com WebAssembly na CPU como fallback. Coloque o modelo num Web Worker para a página nunca congelar. São umas 90 linhas de JavaScript, e abaixo está o arquivo que usamos em produção, levemente resumido.

A parte difícil é tudo o que vem depois de "está rodando": qual modelo, quais configurações de amostragem, o que fazer quando um modelo minúsculo começa a digitar "u.u.u.u.u.", e uma armadilha do bundler que quebrou tudo silenciosamente. Código real, números reais e as partes que deram errado.

Diagrama animado de uma resposta de criatura no BEINGS: a página enfileira o pedido no LocalBrain, um worker de módulo em /public roda o Transformers.js 4.2.0, os pesos ONNX q4f16 carregam do Cache Storage, o WebGPU roda o modelo com WASM como fallback, e isGarbage() decide se a mensagem é postada. Llama 3.2 1B: 1.238 MB, de 55 a 64 tokens/s, 394 ms para uma resposta.
Uma linha de chat, do começo ao fim, com números do nosso benchmark de 17 de out. de 2026 (Chrome 155, RTX 4070). Nenhum servidor toca no modelo.

Por que colocar o modelo no navegador do usuário?

O BEINGS é um chat à moda antiga em que cada pessoa traz as próprias criaturas de IA: um finance bro que acha que pizza é sinal de mercado, um cavaleiro medieval que acha que o seu celular é feitiçaria, um detetive de teorias da conspiração. As criaturas conversam entre si e com os humanos, o dia todo, sem ninguém pedir. Isso é muita chamada de modelo para um brinquedo grátis.

Se essas chamadas passassem pelo nosso servidor até uma API paga, cada sala animada viraria uma conta. Então a aposta da arquitetura foi a oposta: cada criatura roda no aparelho da pessoa que a trouxe. O seu notebook está literalmente rodando os seus bots. A página inicial diz isso com todas as letras: sem chave de API, sem cadastro, nada para pagar. Cérebros na nuvem (OpenAI, Claude, Gemini, OpenRouter) são um upgrade opcional em que você cola a sua própria chave, e essa chave fica no localStorage do seu navegador e vai direto para o provedor, nunca passa por nós.

As contrapartidas são honestas. O usuário baixa o modelo uma vez (de centenas de MB a mais de um GB), a GPU dele faz o trabalho, e modelos pequenos são bem mais burros do que os grandes hospedados. Para um chat caótico, burro-mas-grátis é o negócio certo. Para um bot de atendimento ao cliente, provavelmente não.

Captura da página inicial do BEINGS: uma janela no estilo Windows 95 chamada BEINGS.EXE com o título humans and their weird AIs, same room, a frase Your creatures run on your own device, not on a server. No API key, no signup, nothing to pay, e um formulário hatch your creature com os presets Braxton, Sir Aldric, REDACTED, Chad Vantage, Dr. Lumen e Malvora.
A página inicial do BEINGS no ar, capturada em 17 de out. de 2026, 2h37 da manhã (ET). O rodapé: as criaturas pensam na sua máquina usando WebGPU, com fallback para a CPU.

Passo 1: o worker que guarda o modelo

Gerar texto é um cálculo numérico pesado e síncrono. Rode isso na thread principal e a página congela enquanto o modelo pensa. Então o modelo mora num Web Worker de módulo: uma thread separada que carrega o Transformers.js, guarda um modelo e conversa com a página por mensagens. Aqui está o arquivo, sem apenas o ramo de erro (public/llm-worker.js):

import {
  pipeline,
  env,
} from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@4.2.0/+esm";

env.allowLocalModels = false;

let pipe = null;
let loadedRepo = "";
let busy = false;

async function load(repo, device) {
  if (pipe && loadedRepo === repo) return;
  pipe = await pipeline("text-generation", repo, {
    device,
    dtype: device === "webgpu" ? "q4f16" : "q4",
    progress_callback: (p) => {
      if (p && p.status === "progress" && typeof p.progress === "number") {
        self.postMessage({ type: "progress", pct: Math.round(p.progress), file: p.file });
      }
    },
  });
  loadedRepo = repo;
}

self.onmessage = async (e) => {
  const msg = e.data;
  if (msg.type === "load") {
    self.postMessage({ type: "status", state: "loading" });
    try {
      await load(msg.repo, msg.device);
    } catch (err) {
      // WebGPU can be present but fail to initialise on some drivers.
      if (msg.device === "webgpu") {
        self.postMessage({ type: "status", state: "falling-back" });
        pipe = null;
        loadedRepo = "";
        await load(msg.repo, "wasm");
      } else {
        throw err;
      }
    }
    self.postMessage({ type: "status", state: "ready" });
    return;
  }
  if (msg.type === "gen") {
    // ... one generation at a time ("busy" guard), then:
    const out = await pipe(msg.messages, {
      max_new_tokens: msg.maxTokens,
      do_sample: true,
      temperature: msg.temperature,
      top_p: 0.9,
      top_k: 40,
      repetition_penalty: 1.12,
      no_repeat_ngram_size: 3,
      return_full_text: false,
    });
    // ... pull the assistant text out and post it back as "gen-done"
  }
};

Quatro detalhes ali dentro justificam a presença:

  • O dtype muda conforme o dispositivo. No WebGPU ele carrega q4f16 (pesos de 4 bits, contas em 16 bits); no WASM carrega q4. São arquivos diferentes no Hugging Face, e o guia de dtypes lista o que cada modelo oferece. Para o SmolLM2, o arquivo de WebGPU tem 272,7 MB e o de WASM tem 387,9 MB.
  • O WebGPU pode existir e mesmo assim falhar. "gpu" in navigator ser true não significa que o driver vai colaborar, então um carregamento de WebGPU que falha tenta de novo em WASM em vez de mostrar um erro.
  • O progresso volta para a página. Um primeiro download de 1,2 GB sem barra de progresso parece exatamente um site quebrado.
  • O Transformers.js vem de uma URL de CDN com versão fixada, a 4.2.0 no jsDelivr, não do npm. Isso é de propósito, e é o assunto da próxima seção.

A armadilha do bundler: por que o worker não passa pelo bundle

O jeito "correto" de escrever um worker num framework moderno é um arquivo TypeScript mais new Worker(new URL("./llm.worker.ts", import.meta.url)), deixando o bundler cuidar do resto. O BEINGS é um app Next.js 16, compilado com Turbopack. Quando fizemos desse jeito, o Turbopack emitiu o worker como um arquivo .ts cru em .next/static/media/. Nenhum navegador executa TypeScript, então o worker nunca iniciou, e toda a inferência local quebrou sem um único erro de build.

A correção é sem graça e definitiva: o worker é JavaScript escrito à mão em /public, servido como está, carregado por um caminho simples, e importa o Transformers.js da CDN para que o bundler nunca encoste nele. O comentário no cabeçalho do próprio arquivo diz isso, e as notas do projeto avisam para não "arrumar" o arquivo de volta para dentro do bundle. Este é o lado da página (lib/brains/client.ts):

// Served from /public as a real ES module; see the comment in that file.
const w = new Worker("/llm-worker.js", { type: "module" });
this.worker = w;

const hasGpu = typeof navigator !== "undefined" && "gpu" in navigator;
w.postMessage({ type: "load", repo, device: hasGpu ? "webgpu" : "wasm" });

Se você usa Vite ou webpack, o bundling de workers pode muito bem funcionar para você. A lição que vale em qualquer lugar é menor: depois de qualquer build, abra o site publicado e confira se o worker realmente inicia. Um worker morto em silêncio parece um modelo lento.

Passo 2: um modelo, uma geração por vez

Um usuário pode trazer seis criaturas para uma sala. Se as seis chamassem o modelo ao mesmo tempo, um notebook ficaria carregando seis cópias ou brigando por uma GPU. Então o lado da página é dono de exatamente um worker e enfileira cada geração atrás da anterior (lib/brains/client.ts):

/** Serialised: generations run one after another, never concurrently. */
generate(messages: ChatTurn[], maxTokens = 60, temperature = 0.75): Promise<string> {
  const run = () =>
    new Promise<string>((resolve, reject) => {
      if (!this.worker) return reject(new Error("no brain"));
      const id = Math.random().toString(36).slice(2);
      this.pending.set(id, { resolve, reject });
      this.worker.postMessage({ type: "gen", id, messages, maxTokens, temperature });
      setTimeout(() => {
        if (this.pending.has(id)) {
          this.pending.delete(id);
          reject(new Error("timed out"));
        }
      }, 90_000);
    });

  const next = this.queue.then(run, run);
  this.queue = next.catch(() => {});
  return next;
}

this.queue.then(run, run) é o truque inteiro: cada chamada espera a anterior terminar, com sucesso ou falha, então uma geração que quebrou não consegue travar a fila. O timeout de 90 segundos existe porque, no fallback em WASM, uma única resposta pode levar quase um minuto, como a próxima seção mostra.

Quão rápido é um LLM no navegador, de verdade?

As notas do projeto tinham uma linha incômoda: a inferência com WebGPU no navegador nunca tinha sido verificada em hardware real, porque a sessão de desenvolvimento não tinha navegador. Então hoje à noite medimos. Configuração: Chrome 155 no Windows 11, um desktop com RTX 4070 (o WebGPU informa o adaptador como nvidia / lovelace), 24 núcleos lógicos de CPU. Um pequeno script abriu a própria aba no site em produção, iniciou o /llm-worker.js real, carregou cada modelo e fez 24 turnos reais de chat: a persona de finance bro Braxton do app, três mensagens de sala diferentes, quatro tentativas cada, em duas temperaturas. O mesmo limite de 70 tokens que o app usa.

Gráfico de barras com a mediana de tokens por segundo. SmolLM2 360M em WASM: 1,6 com temperatura 0,75, 1,7 com 1,05, 26,9 s por resposta. SmolLM2 em WebGPU: 62,9 e 67,3, 0,54 s por resposta. Qwen 2.5 0.5B em WebGPU: 37,2 e 40,4, 1,82 s. Llama 3.2 1B em WebGPU: 55,1 e 64,0, 0,48 s.
Mediana de tokens por segundo em 12 respostas por barra, na nossa própria aba no site em produção, Chrome 155, RTX 4070, 17 de out. de 2026, das 2h19 às 2h34 da manhã (ET).

O destaque é o backend, não o modelo. O SmolLM2 360M escreveu 62,9 tokens por segundo em WebGPU e 1,6 em WASM (medianas, temperatura 0,75). A resposta mediana levou 0,54 segundo na GPU e 26,9 segundos no caminho da CPU. Num chat, 27 segundos é uma eternidade: a conversa já seguiu em frente antes de a sua criatura terminar a frase.

Parte dessa diferença provavelmente é culpa nossa, e vale conferir no seu próprio site. O ONNX Runtime Web só usa várias threads de CPU quando a página está em isolamento entre origens (cross-origin isolated); caso contrário, roda numa thread só (isso vem direto da documentação das env flags). A nossa página envia Cross-Origin-Opener-Policy: same-origin, mas nenhum Cross-Origin-Embedder-Policy, e a aba do benchmark informou crossOriginIsolated: false, sem SharedArrayBuffer. Ou seja, o nosso fallback em WASM rodou numa thread de uma máquina com 24. Ainda não medimos com o isolamento ligado, então não vou prometer um número, mas a minha aposta é que o fallback fica várias vezes mais rápido. A pegadinha é que o COEP bloqueia embeds de outras origens que não aderem a ele, e o BEINGS incorpora mídia do YouTube e da Wikimedia. O guia do web.dev sobre COOP e COEP cobre as opções.

A outra surpresa: o maior modelo foi o mais rápido para responder. O Llama 3.2 1B teve média de 55 a 64 tokens por segundo, perto do SmolLM2, mas a resposta mediana dele saiu em 0,4 a 0,5 segundo porque ele disse o que tinha a dizer em uns 24 a 30 tokens e parou. Os modelos menores enrolaram até o limite de 70 tokens. O Qwen 2.5 0.5B foi o mais lento dos três, com 37 a 40 tokens por segundo e cerca de 1,8 segundo por resposta.

Gráfico de barras do tempo entre o início do worker e o estado pronto em WebGPU. SmolLM2 360M, 273 MB: 7,5 s a frio com download, 1,0 s a quente. Qwen 2.5 0.5B, 483 MB: 13,4 s a frio, 2,2 s a quente. Llama 3.2 1B, 1.238 MB: 26,8 s a frio, 3,1 s a quente.
Carregamentos a frio baixaram os arquivos do modelo do zero (entradas do Cache Storage datadas de 17 de out. de 2026); carregamentos a quente os leram do cache do navegador poucos minutos depois.

O download é o custo real da primeira execução

Os três modelos foram baixados do zero durante esta execução (as entradas do Cache Storage estão datadas de 17 de outubro). Nesta conexão, a partida a frio, com download incluído, foi de 7,5 segundos para o SmolLM2 (273 MB), 13,4 para o Qwen (483 MB) e 26,8 para o Llama (1.238 MB). Já em cache, os mesmos carregamentos levaram 1,0, 2,2 e 3,1 segundos. O Transformers.js guarda os arquivos no Cache Storage do navegador, então a segunda visita é rápida sem você escrever nenhum código de cache.

Nem todos os seus usuários vão ter uma conexão rápida. Um download de 1,2 GB num celular em 4G é um momento de "volto depois", e é por isso que o app deixa você escolher um cérebro menor, e por isso que o callback de progresso do Passo 1 importa mais do que parece.

Configurações de amostragem importam mais que o tamanho do modelo

No começo, o app fazia a amostragem com temperatura 1,05 e top_p 0,95 para ter "mais caos". Até o Qwen entrava em loop com linhas como "u.u.u.u.u." e "The The FQ Q M". A correção que ficou está no worker acima: temperatura 0,75, top_p 0,9, top_k 40, uma penalidade de repetição de 1,12 e no_repeat_ngram_size: 3, que proíbe o modelo de repetir qualquer sequência de três tokens. (Se esses botões são novidade para você, a documentação de geração do Hugging Face explica cada um.)

A execução de hoje testou só a temperatura, 0,75 vs 1,05, com todas as outras configurações nos valores mais seguros. Mesmo esse único botão apareceu com clareza:

  • Qwen 0.5B: 2 de 12 respostas foram descartadas como lixo em 0,75, e 6 de 12 em 1,05. Exemplos reais da temperatura mais alta: "CODESUPONSEGRAPHTEGOINSTRUMENTPRODUSONGRAPH..." e "What sorceryistransformedintoasolidreturningSorcery."
  • SmolLM2 360M: a resposta mediana cresceu de 15 palavras para 40,5 em 1,05, e 7 de 12 respostas bateram no limite de 70 tokens. O prompt pede menos de 20 palavras; na temperatura mais alta, o modelo parou de obedecer.
  • Llama 3.2 1B: 24 de 24 respostas foram aproveitáveis nas duas temperaturas, e ficaram no personagem: "Game-changer alert! WiFi outages when market indices hit bottom is proof!" (Alerta de virada de jogo! Queda do Wi-Fi quando os índices batem no fundo é a prova!)

Se for mudar uma única coisa na sua configuração no navegador, que seja esta: mantenha a temperatura abaixo de 0,8 para qualquer modelo com menos de 1B de parâmetros e ative o no_repeat_ngram_size.

Passo 3: um filtro de lixo, porque silêncio é melhor que besteira

Mesmo com boas configurações, um modelo de 360M às vezes vai produzir algo que nenhum humano deveria ver. A regra no BEINGS é "melhor uma criatura calada do que uma quebrada", então cada resposta passa por um filtro barato e determinístico antes de ser postada. Sem segundo modelo, sem chamada de API. São 64 linhas (lib/garbage.ts, resumido):

export function isGarbage(raw: string): boolean {
  const t = (raw || "").trim();
  if (t.length < 2) return true;

  // markup or template fragments
  if (/[<>]|&[a-z]+;|\{\{|\}\}/i.test(t)) return true;

  // symbol soup: not enough actual letters
  const letters = (t.match(/[a-zA-Z]/g) || []).length;
  if (letters / t.length < 0.55) return true;

  // a short chunk repeated over and over: "u.u.u.u.", "hahahahaha"
  if (/(.{1,4}?)\1{4,}/.test(t)) return true;

  const words = t.toLowerCase().match(/[a-z']+/g) || [];
  if (words.length >= 5) {
    // the same few words cycling
    const uniq = new Set(words);
    if (uniq.size / words.length < 0.5) return true;
    // ... and the same word three times in a row
  }

  // ... scattered one and two letter fragments: "The The FQ Q M"
  // ... invented words: no vowels, or a syllable cycling ("unununu")
  return false;
}

O filtro tem o próprio arquivo de testes, com as linhas ruins reais do relatório de bug como fixtures, mais linhas boas reais que precisam passar, inclusive as estranhas-mas-ok como "Penar wingle berry" e "THIS IS BULLISH ACTUALLY, LOAD UP" (scripts/garbage-test.mjs). Hoje à noite ele continuou passando: 14 aprovados, 0 com falha. No app, ele fica logo antes da postagem, ao lado de uma segunda checagem para quando o modelo papagueia as próprias instruções (lib/engine.ts):

const raw = await think(bot.brain, buildTurns(bot, this.ctx, trigger), 70);
const text = clean(raw, bot.name);
// Better a silent creature than one pasting its briefing or looping "u.u.u.".
if (!text || looksLikeLeak(text) || isGarbage(text)) return;
Grade de 96 respostas por modelo e temperatura, verde para postadas e vermelho ou pêssego para descartadas. SmolLM2 WASM 11 e 12 de 12 postadas, SmolLM2 WebGPU 9 e 11, Qwen 0.5B 10 em 0,75 e 6 em 1,05, Llama 1B 12 e 12. Abaixo, linhas reais: duas saladas de palavras do Qwen descartadas, uma linha do SmolLM2 terminando em Speak in character now descartada como vazamento, o Toestrafing GRating do Qwen que passou, e uma linha do Llama postada.
Todas as respostas do benchmark de 17 de out. de 2026, passadas pelos mesmos clean(), checagem de vazamento e isGarbage() que o app usa: 13 de 96 descartadas, 0 delas do Llama.

O que o filtro pegou, e o que passou

Rodando sobre as 96 respostas do benchmark de hoje, o filtro e a checagem de vazamento descartaram 13: 11 como lixo e 2 como instruções vazadas (uma resposta do SmolLM2 terminava com "Speak in character now!"). Ele pegou as falhas óbvias: as palavras grudadas, o markup --> perdido, as saladas de palavras do Qwen.

Ele também deixou passar coisas que um humano jogaria fora. O "Toestrafing GRating" do Qwen passou, porque as duas palavras têm vogais e nenhuma se repete. O mesmo aconteceu com uma linha do SmolLM2, toda em maiúsculas, sobre coisas "MULTILATERAL ORTHODOZIC". Um filtro de regras pega texto quebrado, não texto sem sentido. A minha opinião: isso serve para um chat caótico, em que uma linha esquisita é metade da graça, e não serve nem de longe para qualquer coisa que um cliente vá ler. Se o sentido importa, você precisa de um modelo maior, não de uma regex mais esperta.

Captura em estilo terminal dos resultados do benchmark: o env informa hasGpu true, 24 núcleos, crossOriginIsolated false, SharedArrayBuffer undefined, adaptador nvidia lovelace; tempos de carregamento dos modelos de 7491, 8023, 13423 e 26847 ms; respostas do SmolLM2 em WASM levando 27424 e 24335 ms; respostas do Qwen com palavras grudadas em cerca de 1,7 s; respostas do Llama em 1836 e 394 ms.
Linhas do arquivo de resultados do benchmark (bench.json), 17 de out. de 2026. Repare no coi:false: sem isolamento entre origens, então o fallback em WASM rodou numa thread só.

O que quebrou pelo caminho

  • O worker no bundle (acima): o Turbopack publicou um arquivo .ts como asset estático. Zero erros, zero inferência.
  • Configurações de caos: temperatura 1,05 e top_p 0,95 faziam os modelos pequenos entrarem em loop. Baixar esses valores fez mais pela qualidade do que qualquer troca de modelo entre os pequenos.
  • Eco das instruções: modelos pequenos repetem o briefing ("reply in one short line, in character"). A correção foi um filtro de vazamento e também uma mudança no prompt: turnos de chat reais em vez de uma transcrição colada numa única instrução enorme, porque, como diz um comentário no código, "small models echo meta-instructions but handle the chat format they were tuned on" (modelos pequenos ecoam meta-instruções, mas lidam bem com o formato de chat em que foram ajustados).
  • Seis criaturas, um notebook: a geração concorrente foi resolvida pela fila do Passo 2.
  • Nunca testado em hardware real até hoje à noite. Funciona, e o WebGPU é rápido. O fallback em WASM é o ponto fraco.

Os números

MedidaResultado
SmolLM2 360M, WebGPU vs WASM62,9 vs 1,6 tokens/s; 0,54 s vs 26,9 s por resposta
Llama 3.2 1B (q4f16), WebGPUde 55 a 64 tokens/s, de 0,4 a 0,5 s por resposta, 24 de 24 respostas aproveitáveis
Qwen 2.5 0.5B, WebGPUde 37 a 40 tokens/s, cerca de 1,8 s por resposta, 8 de 24 descartadas
Download (arquivo do modelo)273 MB / 483 MB / 1.238 MB
Carregamento, a frio vs em cache7,5 / 13,4 / 26,8 s vs 1,0 / 2,2 / 3,1 s
Conta de inferência da plataforma$0

Fonte: a nossa própria aba de benchmark no site do BEINGS em produção, Chrome 155, RTX 4070, 17 de outubro de 2026, das 2h19 às 2h35 da manhã (ET). 12 respostas por linha e temperatura: uma amostra pequena, boa para ordens de grandeza, não para a segunda casa decimal.

Faça isto no seu projeto

  1. Use Transformers.js num Web Worker de módulo. Um worker, um modelo, uma geração por vez, com uma fila na frente.
  2. Escolha um modelo ONNX que tenha um arquivo q4f16 para WebGPU e um arquivo q4 para o fallback. A organização onnx-community no Hugging Face é o lugar para começar.
  3. Use por padrão algo perto de 1B de parâmetros, não a menor coisa que carrega. No nosso teste, o Llama 3.2 1B foi tão rápido por resposta quanto o SmolLM2 360M e muito mais coerente que o Qwen 2.5 0.5B. Ofereça o pequeno como opção para "conexão lenta".
  4. Faça o fallback de WebGPU para WASM dentro do worker e, se o fallback importa para você, deixe a página em isolamento entre origens para que ela possa usar threads. Depois teste se os seus embeds continuam funcionando.
  5. Mantenha a temperatura em 0,75 ou menos para modelos pequenos, com no_repeat_ngram_size: 3 e uma penalidade de repetição leve.
  6. Filtre antes de mostrar. Uma checagem por regras de 60 linhas, com linhas de falha reais como fixtures de teste, é melhor do que postar "u.u.u.u.".
  7. Mostre o progresso do download e diga às pessoas o tamanho antes de elas clicarem.
  8. Verifique no site publicado, num navegador de verdade, depois de cada build. O nosso worker quebrou em silêncio de um jeito que nenhum teste teria pegado.

Para onde isso vai

Acho que a mudança interessante não é que os modelos pequenos sejam bons. Ainda não são. É que "o aparelho do usuário paga" muda o que vale a pena construir. Uma função que custaria um centavo por mensagem numa API é grátis para sempre no navegador, então categorias inteiras de coisas bobas, falantes e sempre ligadas ficam possíveis para um desenvolvedor solo. O hardware também continua empurrando essa linha: o modelo de 1B de hoje numa aba é mais ou menos o que precisava de um servidor não muito tempo atrás. Compare com a outra ponta da escala, onde um modelo "aberto" de 816B precisa de cerca de um terabyte de RAM só para rodar.

Se o seu app chama um modelo hospedado, o token mais barato é o que você não envia duas vezes: veja como paramos de pagar duas vezes pela mesma resposta de LLM. E antes de escolher entre navegador e API, calcule direito o custo do lado da API. A nossa LLM API Cost Calculator de $7 calcula o custo por requisição, por mês e por ano em 26 modelos, no Google Sheets ou no Excel.

Você já publicou um LLM que roda no navegador do usuário? O que o hardware dos seus usuários fez com os seus planos: o WebGPU simplesmente funcionou, ou foi o fallback que segurou a onda?

Publicado originalmente aqui no tensorust.com em 17 de out. de 2026. Também no dev.to. Relacionados: BEINGS, o app no ar, Funções de IA com preço fechado, Pare de pagar duas vezes pela mesma resposta de LLM.

Leia também

Todos os artigos →

Mais em português

Português (Brasil): Tensorust →

Up next

Most read on the site this weekcrates.io download count: 80% of ours arrived with a new versionWhat the crates.io download count really measures: 69 Rust crates, 90 days of daily data, 80% within a day of a publish, 0 outside dependents.Read it next →

Get the receipts by email.

What my AI agents tried this week, with the real numbers: what worked, what flopped, what made money. One email when there is something worth reading.

No spam, never sold. Every email has a one-click unsubscribe.

Popular in the shop

All 124 tools →

Browse: Bundles and kitsHome, family and schoolMoney and businessAI toolsSports and fitnessMaking and hobbiesFood and drinkIndie dev and launchTrading and quant

Free: Landing page teardown iPhone app cost calculator LLM API cost calculator Paid online: $49 Launch Check App builds Articles FAQ