Como otimizamos o Quick Tools: de um bundle de 1.4MB para carregamento sob demanda
Estava eu, totalmente imerso em código, subindo tool adoidado, para tentar bater uma meta pessoal de 100 tools no meu site Quick Easy, sem me dar conta que meu site poderia estar perdendo performance.
Eu tenho no meu navegador, a extenão do daily.dev, e abri uma nova aba para pesquisar algo, e um post me chamou a atenção How's Linear so fast? A technical breakdown, gastei um tempo lendo, e tive o insight, como será que tá meu site, até o momento não tinha parado para olhar, rodei o lighthouse do google

O número me assustou um pouco, era evidente que seria necessário algum ajuste. Estudei o post, pesquisei nas IAs, separei o que não era util pra mim no momento, e comecei.
Fase 1: Code splitting granular com manualChunks
O Vite já faz code splitting automático, mas para dependências de terceiros ele tende a agrupar tudo em um único chunk vendor. A solução foi configurar manualChunks no vite.config.ts para separar cada biblioteca pesada no seu próprio arquivo:
manualChunks(id) {
if (!id.includes('node_modules')) return;
if (id.includes('/highlight.js/')) return 'vendor-highlight';
if (id.includes('/marked/')) return 'vendor-markdown';
if (id.includes('/ajv/')) return 'vendor-ajv';
if (id.includes('/jsonrepair/')) return 'vendor-jsonrepair';
if (id.includes('/qr-code-styling/')) return 'vendor-qr';
if (id.includes('/js-yaml/')) return 'vendor-yaml';
if (id.includes('/lucide-svelte/') || id.includes('/@iconify/')) return 'vendor-icons';
if (id.includes('/bits-ui/') || id.includes('/svelte-sonner/')) return 'vendor-ui';
}
O resultado: cada ferramenta agora carrega apenas os chunks que realmente precisa. Um usuário que acessa o conversor de texto não baixa nada do highlight.js. Quem usa o JSON validator não toca no qr-code-styling.
Além do benefício de tamanho, chunks nomeados melhoram muito o cache invalidation: quando atualizamos a lógica do JSON validator, apenas o chunk vendor-ajv e o chunk da ferramenta são invalidados, o resto continua em cache.
Fase 2: Dynamic imports onde importa
Só separar os chunks não é suficiente se o import ainda é estático. A biblioteca marked estava sendo importada com import { marked } from 'marked' diretamente no +page.ts, o que forçava o browser a baixá-la antes de qualquer renderização.
A correção foi converter para dynamic import:
// antes
import { marked } from 'marked';
// depois
const { marked } = await import('marked');
Com isso, o vendor-markdown chunk só é requisitado quando o usuário abre uma ferramenta que precisa renderizar Markdown e mesmo assim, de forma assíncrona, sem bloquear o restante da página.
Nota importante para quem usa SvelteKit com adapter-static: o load em +page.ts só executa no build time com SSG. No cliente, o SvelteKit usa o __data.json pré-renderizado e nunca re-executa o load no browser. Isso significa que dynamic imports dentro do load function não têm custo no bundle client-side o output do load já está serializado no JSON.
Fase 3: Service Worker com cache-first para assets
O último passo foi adicionar um Service Worker para maximizar o uso de cache entre sessões:
// Cache-first para assets com hash (JS/CSS do build)
if (PRECACHE.includes(url.pathname)) {
const cached = await cache.match(event.request);
if (cached) return cached;
}
// Network-first para HTML — conteúdo pode atualizar a cada deploy
A estratégia é diferente por tipo de recurso:
- Assets com hash (os chunks do Vite): cache-first. Um arquivo
vendor-highlight-abc123.jsnunca muda — se está em cache, serve do cache direto, sem nem tocar na rede. - Páginas HTML e dados: network-first, com fallback para cache quando offline.
Isso significa que na segunda visita, praticamente todo o JS e CSS já está no cache do browser, o site carrega como se fosse local.
Fase 4: Animações sem forçar reflow
Uma causa comum de TBT alto é CSS que força o browser a recalcular layout durante animações. Encontramos alguns lugares usando transition-all, que inclui propriedades como height, width e top todas elas trigam layout reflow.
A correção foi substituir por propriedades que só afetam compositing:
/* antes */
transition: all 0.2s ease;
/* depois */
transition: opacity 0.2s ease, transform 0.2s ease;
opacity e transform são tratados diretamente pela GPU, sem passar pelo main thread de layout. No Sonner (biblioteca de toasts), removemos a animação de height que estava causando um reflow visível a cada notificação.
Enfim, depois de passar por vários bugs, e dor de cabeça, mais fases do que está aqui escrito, e vários rounds, consegui, com muita ajuda das IAs, ajustar o site enfim. Agora o site bate 90-95 pontos no lighthouse. Mas o que aprendi é que tem que ser uma monitoria constante.
Bom, um pouco de mercha para quem não conhece o Quick Easy (ninguem conhece 😢), a menos de 60 dias eu resolvi criar um site de tools, onde eu sou o principal usuário do site. Hoje, com certeza eu já subi mais tools do que eu consigo usar. Mas a maioria delas eu mesmo utilizo, para quem quer conhecer melhor, já temos várias tools úteis, e algumas inuteis como o gerador de código morse.
Mas algumas tools são:
Gerador e validador de CNPJ, já com o novo padrão que entra em junho desse ano
Gerador de CPF
Comprimir imagens <-- a que eu mais utilizo hoje
Não tem tool só pra dev não, tem pra designer, financeiro, etc. Uma outra que utilizo bastante é a calculadora de juros compostos