APRENDIZADO & COGNIÇÃO · ESTUDO BASEADO EM EVIDÊNCIA
Pitch: TianTian, flashcards de chinês (HSK) com áudio offline num PWA: Service Worker, Range requests do Safari e outras pegadinhas
Fizemos o TianTian (天天, “todo dia”), um app de flashcards para o vocabulário do HSK, a prova oficial de proficiência…

Fizemos o TianTian (天天, “todo dia”), um app de flashcards para o vocabulário do HSK, a prova oficial de proficiência em chinês. Cada palavra tem foto, caractere, pinyin e áudio nativo, e tudo se responde só com toques. Ele roda como PWA: abre no navegador do celular e pode ser instalado na tela inicial.
Este post é um pitch, mas a ideia é contar o que deu trabalho. Áudio offline em PWA, principalmente no iPhone, tem umas pegadinhas que quase não aparecem em tutorial.
Por que PWA, e não loja de apps
O público inicial é brasileiro estudando chinês. Pesou na decisão:
- Celular com pouco espaço. Muita gente não instala “mais um app” de 150 MB. O PWA baixa o shell (HTML, CSS, JS e o JSON das palavras grátis) e o resto vem sob demanda.
- Um link resolve. Dá para mandar o link num fórum ou grupo de estudo e a pessoa já está usando em 5 segundos, sem cadastro no plano grátis.
- Atualização na hora. Corrigiu um tom errado num áudio? Sobe o arquivo e pronto, sem esperar revisão de loja.
- Pagamento direto. Sem loja no meio, a assinatura vai pelo Stripe Checkout. O lado ruim: por enquanto só cartão (sem PIX), o que no Brasil é uma limitação real que ainda estamos avaliando.
O preço: PWA no iOS ainda tem limitações (armazenamento separado do Safari, sem notificação confiável em versões antigas), e “instalar” não é óbvio para quem nunca fez. Esse é um ponto que ainda queremos melhorar no próprio app (hoje ele não tem um tutorial de instalação).
A estratégia de cache: três caches, três regras
O Service Worker separa três tipos de conteúdo:
const SHELL = `tiantian-shell-${VERSION}`; // HTML/CSS/JS + JSON grátis: precache no installconst MEDIA = "tt-media-v2"; // imagens e áudios grátis: cache preguiçosoconst PREMIUM = "tt-premium"; // /premium/*: NUNCA precache, só em runtime
- Shell e dados grátis: precache no
install+ stale-while-revalidate. O app abre instantâneo, mesmo offline, e se atualiza em segundo plano. Para forçar atualização nas cópias instaladas, basta mudar oVERSION, porque oactivateapaga os caches com nome antigo. - Áudio grátis: cache-first, salvo no primeiro play. Quem ouviu uma palavra uma vez já tem ela offline.
- Imagens: stale-while-revalidate, para que uma foto trocada no servidor apareça sem quebrar o offline.
- Conteúdo pago: só entra no cache se a resposta for um 200 completo, sem redirect. Se o servidor responder 401/403 (sessão expirada ou assinatura cancelada), o cache premium inteiro é apagado. Não existe “desbloqueio” no cliente: o app só espelha o que o servidor diz.
const cacheable = res => !!res && res.status === 200 && res.type === "basic" && !res.redirected;
Pegadinha 1: o Safari pede áudio com Range, e o Cache API não guarda 206
No iPhone, o elemento <audio> não pede o arquivo inteiro. Ele manda Range: bytes=0-1 e depois vai pedindo pedaços. A resposta é 206 Partial Content, e o Cache API recusa respostas parciais. Resultado: o áudio “funcionava” online e sumia offline.
A solução tem duas partes:
- No SW, buscar sempre sem o header Range, para receber o 200 completo e poder guardar.
- Quando o navegador pedir um Range, montar o 206 a partir do arquivo inteiro que está no cache:
async function rangeResponse(req, res){ const buf = await res.arrayBuffer(), size = buf.byteLength; const m = /bytes=(d*)-(d*)/.exec(req.headers.get("range") || ""); let start = m && m[1] ? +m[1] : 0, end = m && m[2] ? +m[2] : size - 1; if (m && !m[1] && m[2]) { start = size - +m[2]; end = size - 1; } // sufixo: "bytes=-500" end = Math.min(end, size - 1); return new Response(buf.slice(start, end + 1), { status: 206, headers: { "Content-Type": res.headers.get("Content-Type") || "audio/mpeg", "Content-Range": `bytes ${start}-${end}/${size}`, "Content-Length": String(end - start + 1), "Accept-Ranges": "bytes" } });}
Os MP3 das palavras são pequenos, então carregar o arquivo inteiro em memória para fatiar não pesa.
Pegadinha 2: o áudio precisa tocar dentro do gesto do usuário
O iOS bloqueia play() fora de um toque. Num flashcard, o natural é tocar o áudio depois de conferir a resposta, e foi aí que travou. Duas medidas resolveram:
- Chamar
play()dentro do handler do toque, antes de qualquersetTimeoutou animação. - Pré-carregar os áudios da rodada como Blob URLs (
fetch→blob()→URL.createObjectURL). Na hora do toque, osrcjá está em memória e toca instantâneo, inclusive offline.
Se o arquivo falhar mesmo assim, há um fallback para a voz do sistema (speechSynthesis com lang = "zh-CN" e rate = .8). Não é tão boa quanto o áudio nativo, mas é melhor que silêncio.
Repetição espaçada simples de propósito
Nada de SM-2 completo. São 6 níveis com intervalos fixos:
const INTERVALS = [0, 10*60e3, 1*DAY, 3*DAY, 7*DAY, 16*DAY, 35*DAY]; // 10 min ... ~5 semanas
- Acertou: sobe um nível. Errou: desce um nível, e a palavra volta 3 cartões depois na mesma rodada.
- Para quem está no HSK 1, isso basta, e o comportamento fica previsível (“vi ontem, volta em 3 dias”).
- O progresso fica no
localStoragedo aparelho, sem conta. - O ponto fraco é conhecido: trocar de celular perde o progresso do plano grátis.
Pegadinha 3: medir de onde vêm os usuários num PWA
Duas coisas que quase nos pegaram:
- O
start_urldo manifest não pode ter UTM. Se tiver, toda abertura do app instalado conta como visita da campanha. - No iPhone, o PWA instalado tem armazenamento separado do Safari. O que ficou gravado no Safari (inclusive a origem da visita) não existe dentro do app instalado. Então a atribuição precisa ir ao servidor logo na primeira visita, e não ficar só no cliente.
O que existe hoje (sendo honestos)
- HSK 1 completo, com a lista clássica de 150 palavras (HSK 2.0).
- O HSK 3.0 tem prova oficial mundial marcada para 13/12/2026 e lista maior (300 palavras no nível 1). Ainda não cobrimos a lista nova inteira. O HSK 2 em diante está em construção.
- O app também tem 18 lições curtas de gramática do HSK 1, montador de frases (tocar nas palavras na ordem certa), exercícios de escuta e textos curtos com toque para ouvir.
- Plano grátis: 20 palavras por nível, as primeiras lições e alguns textos, com anúncio, sem cadastro.
- Premium: desbloqueia tudo e tira o anúncio. Tem também o “Baixar tudo” para estudar 100% offline (no metrô, no avião). R$14,90/mês ou R$79,90/ano, e um plano de fundador de R$59,90/ano para os 100 primeiros.
- Não é dicionário. O Pleco continua sendo o melhor para isso.
Perguntas para a comunidade
- Alguém já resolveu de forma mais elegante o
Range+ Cache API no Safari? (Pensamos em guardar em IndexedDB, mas o Cache API ficou mais simples.) - Para PWA no Brasil: vocês já mediram quanta gente instala de fato na tela inicial ou só usa no navegador?
- Vale a pena oferecer PIX numa assinatura recorrente, ou o cartão resolve para esse público?
Quem quiser testar (roda no navegador do celular, sem instalar nada):
https://tiantianhsk.com/?utm_source=tabnews&utm_medium=post&utm_campaign=launch&utm_content=pitch
Respondemos a todos os comentários por aqui.
Perguntas frequentes
Respostas institucionais sobre esta cobertura, fontes e limites editoriais.
- Sobre o que trata “Pitch: TianTian, flashcards de chinês (HSK) com áudio offline num…”?
- Fizemos o TianTian (天天, "todo dia"), um app de flashcards para o vocabulário do HSK, a prova oficial de proficiência em chinês. Cada palavra tem foto, caractere, pinyin e áudio nativo, e tudo se responde só…
- Por que Aprendizado & Cognição importa agora?
- O tema impacta decisões e debates cotidianos ligados a Aprendizado & Cognição. A redação destaca fatos, datas e implicações práticas sem substituir orientação profissional personalizada.
- Quem edita o conteúdo do Estrato Mente?
- A cobertura é produzida e revisada pela equipe editorial do Estrato Mente, com editor-chefe responsável pela linha editorial. Conheça a redação em https://mente.estrato.cc/equipe/.
- Quais fontes o Estrato prioriza?
- Priorizamos fontes primárias (órgãos oficiais, balanços, estudos revisados, documentos públicos) e cruzamos informações antes da publicação, conforme a política editorial.
- Como solicitar correção de uma matéria?
- Envie o pedido pela página de Correções (https://mente.estrato.cc/correcoes/) ou Contato (https://mente.estrato.cc/contato/). Erros materiais são corrigidos com registro da atualização.
- O conteúdo constitui aconselhamento profissional?
- Não. As matérias têm caráter informativo e jornalístico. Decisões financeiras, de saúde, jurídicas ou educacionais devem considerar profissionais habilitados e a sua situação particular.
- Onde acompanhar a política editorial do Estrato Mente?
- Metodologia, ética e transparência estão em https://mente.estrato.cc/metodologia/, https://mente.estrato.cc/etica-editorial/ e https://mente.estrato.cc/politica-editorial/.
- Como o Estrato trata temas sensíveis (YMYL)?
- Temas que afetam dinheiro, saúde e bem-estar recebem revisão editorial reforçada, disclaimer visível e links para páginas institucionais de metodologia e correções. Portal: https://mente.estrato.cc/