Teu progresso
0 / 83 módulos0%
Estágio 02 · 02-03
BloqueadoA "web platform" virou enorme. O browser hoje oferece umas 500 APIs, fetch, eventos, storage, workers, observers, streaming, crypto, audio, gamepad, push notifications, web bluetooth, file system access, etc. Entender DOM e Web APIs significa parar de tratar o browser como mistério e começar a usar de verdade o que ele já te dá de graça.
Frameworks (React, Vue, Svelte) abstraem boa parte do DOM, mas vazam constantemente. Quando o ref não funciona, quando o effect roda em ordem errada, quando um listener não dispara, quando IntersectionObserver dispara duplicado, você precisa do modelo abaixo do framework. E vários problemas têm solução muito mais limpa em Web API nativa do que no nível do framework.
O DOM (Document Object Model) é a representação em árvore do documento, exposta como objetos JS. Cada elemento HTML vira um Node do tipo Element, com propriedades, métodos, eventos.
Operações cruciais:
document.querySelector(sel), querySelectorAll(sel), selectors CSS retornando elemento(s).element.closest(sel), sobe ancestrais até match.element.matches(sel), testa selector.element.children, firstElementChild, nextElementSibling, navegação.element.append(child) (modern), appendChild(child) (legacy, pode adicionar 1 só), before/after/replaceWith/remove, manipulação.element.classList.add/remove/toggle/contains, classes.element.dataset, data-* attributes como objeto. <div data-user-id="42"> → el.dataset.userId === "42".DOM e CSSOM se cruzam em element.getBoundingClientRect() (posição/tamanho calculado), getComputedStyle(el) (todos os estilos resolvidos). Ambos forçam layout sync: se você ler bbox depois de escrever style, browser tem que recalcular layout no meio do JS, penalidade real. Padrão é separar leitura (read) de escrita (write) em frames diferentes.
Modelo de eventos do browser tem três fases:
addEventListener(type, handler, options):
options.capture (default false), registra na capture phase.options.once, auto-remove após primeiro fire.options.passive, promete que handler não vai chamar preventDefault. Crítico em scroll/touch, habilita scroll suave em mobile.options.signal: AbortSignal, cancelamento moderno (vinculável a AbortController).event.preventDefault(), cancela ação default (submit do form, link navegando, scroll).
event.stopPropagation(), para bubble. Use raríssimo, geralmente é cheiro de mal arquitetar.
event.stopImmediatePropagation(), para outros listeners no mesmo target também.
Event delegation é padrão valioso: em vez de N listeners (1 por linha de tabela), 1 listener no parent. Bubble traz o evento, você usa event.target.closest('.row') pra identificar:
table.addEventListener('click', (e) => {
const row = e.target.closest('tr[data-id]');
if (row) handleRowClick(row.dataset.id);
});
Tipos importantes pra dominar: click, pointerdown/move/up, keydown/keyup, input, change, submit, focus/blur (não bubble; tem versões focusin/focusout que sim), scroll, resize, wheel, touchstart/move/end, dragstart/over/drop, visibilitychange, beforeunload, online/offline.
pointer* events unificam mouse + touch + caneta, prefira sobre mouse*/touch* em código novo.
Você pode emitir e ouvir eventos próprios, útil pra desacoplar componentes vanilla.
const ev = new CustomEvent('order:created', { detail: { id: 42 }, bubbles: true });
element.dispatchEvent(ev);
EventTarget é a base, qualquer objeto pode estender e virar um event emitter no estilo browser. Útil pra design de SDKs JS.
fetch(url, init) é a API HTTP do browser (e Node moderno). Returns Promise<Response>.
const res = await fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
credentials: 'include', // envia cookies
signal: controller.signal, // cancellation
});
if (!res.ok) throw new HttpError(res.status);
const json = await res.json();
Detalhes importantes:
fetch não rejeita em status 4xx/5xx, só em erro de rede. Cheque res.ok ou res.status.credentials: 'omit' | 'same-origin' (default) | 'include', controla envio de cookies.mode: 'cors' | 'no-cors' | 'same-origin', controle de CORS.Response.body é um ReadableStream, você pode consumir incremental (importante pra streaming de LLMs).AbortController:
const c = new AbortController();
fetch(url, { signal: c.signal });
c.abort(); // cancela em qualquer momento
AbortSignal.timeout(5000) é shorthand pra cancelamento por timeout. AbortSignal.any([...]) combina sinais.
HttpOnly; Secure; SameSite=Lax), sem JS access. Detalhes em 01-03 e 02-13.idb.Limitações de quota: browsers podem evictar storage de origens não-utilizadas. navigator.storage.persist() pede persistência permanente (browser pode aceitar ou não baseado em uso).
JavaScript é single-threaded por origin. Pra rodar código pesado sem travar UI, você precisa de Web Worker: outra thread, sem acesso a DOM, comunicação por message passing.
// main.js
const w = new Worker('worker.js', { type: 'module' });
w.postMessage({ kind: 'compute', data });
w.onmessage = (e) => console.log('result:', e.data);
// worker.js
self.onmessage = (e) => {
const result = heavy(e.data);
self.postMessage(result);
};
postMessage faz structured clone dos dados (deep copy). Pra evitar copy de buffers grandes, use transferable objects:
w.postMessage(arrayBuffer, [arrayBuffer]); // transfer ownership; original fica detached
SharedArrayBuffer permite memória compartilhada entre threads, mas exige cabeçalhos de cross-origin isolation (COOP/COEP).
Service Workers são variante: ficam entre seu app e a rede. Implementam offline (Cache API), push notifications, background sync. Foundation de PWAs.
Worklets (Audio Worklet, Paint Worklet, Animation Worklet) são threads especializadas pra audio em tempo real, custom paint em CSS, animações declarativas.
Quatro observers pra dominar. Todos têm pattern semelhante: criar com callback, observar elementos, callback é chamado em batch.
IntersectionObserver: notifica quando elemento entra/sai do viewport (ou de outro elemento). Substitui hacks de getBoundingClientRect em scroll listener.
const io = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) loadMore();
});
}, { rootMargin: '200px', threshold: [0, 0.5, 1] });
io.observe(loadMoreSentinel);
Use cases: lazy load imagens (já é nativo via loading="lazy", mas IO dá mais controle), infinite scroll, reveal on scroll, viewport-aware tracking.
ResizeObserver: dispara quando tamanho do elemento muda (não só viewport). Substitui listeners de window resize pra elementos individuais. Usar pra responsivos baseados em container ou pra reagir a mudanças via flexbox/grid.
MutationObserver: dispara quando DOM muda (filhos adicionados, attrs alterados). Caro, use só quando precisa observar mudanças de terceiros. Em código próprio, geralmente você sabe quando muda.
PerformanceObserver: notifica eventos de performance (LCP, FID, CLS, long tasks, fetch entries). Pra observabilidade frontend. Veremos mais em 03-09.
Browsers (e Node moderno) suportam streams nativos no estilo Web Streams API. Response.body é um ReadableStream<Uint8Array>.
const res = await fetch('/large');
const reader = res.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
process(value);
}
TransformStream pra pipelines (parse SSE de stream de LLM, decode UTF-8, etc.). Esse é o foundation pra streaming de RSC, edge runtime, server-sent events.
URL constructor é ferramenta poderosa:
const u = new URL('/orders/42?view=detail', location.origin);
u.searchParams.set('debug', '1');
u.toString();
History API:
history.pushState(state, '', '/new-url'), muda URL sem reload.history.replaceState(...), substitui entry atual.popstate event, dispara em back/forward.Esse é o motor de SPAs vanilla. Frameworks usam por baixo.
location.assign(url) faz navegação (reload). location.replace(url) substitui sem entry no history.
navigator.clipboard.readText() / writeText(). Async, com permissions.<input type="file"> + File/Blob + FileReader. Drag-and-drop integra.crypto.subtle, hash, encrypt, sign. Use pra ETags client-side, key derivation.navigator.geolocation.getCurrentPosition, só com user gesture + permissions.document.visibilityState, visibilitychange event. Pause animation/poll em background.Sequência problemática:
el.style.width = '100px';
const w = el.offsetWidth; // força layout aqui
el.style.height = '100px';
Lendo offsetWidth força browser a re-calcular layout porque sabe que mudou. Loop de N elementos com escrita-leitura-escrita = N reflows. Solução: batch reads, depois batch writes.
DevTools Performance tab mostra forced reflow como warning. Pratique abrir Performance tab, gravar 5s de uso da app, e ler o flame graph.
requestAnimationFrame(cb) agenda cb pra próximo frame, ajuda batching. requestIdleCallback(cb) roda em idle entre frames, bom pra trabalho não-urgente (analytics, prefetch).
Service Worker é proxy programável entre app e network rodando em thread separada. Foundation pra Progressive Web Apps (PWA): offline, install, push, background sync.
register → installing → installed (waiting) → activating → activated → idle ↔ fetching/working
self.addEventListener('install', e => ...)): pre-cache assets críticos via caches.open('v1').then(c => c.addAll([...]))`.clients.claim() força controle de tabs já abertas.e.respondWith(...) substitui resposta.postMessage.| Strategy | Comportamento | Quando |
|---|---|---|
| Cache-first | Cache → fallback network | Assets estáticos versionados (CSS, JS hash) |
| Network-first | Network → fallback cache | HTML de página (sempre fresh quando online) |
| Stale-while-revalidate | Retorna cache imediato + revalida async | API de dados que toleram 1 versão stale |
| Network-only | Sempre network | POST, PUT, payments — nunca cache |
| Cache-only | Sempre cache | Asset offline-first conhecido |
Lib canônica: Workbox (Google). Abstração production-ready com strategies, cleanup, precaching.
import {registerRoute} from 'workbox-routing';
import {CacheFirst, NetworkFirst, StaleWhileRevalidate} from 'workbox-strategies';
registerRoute(({request}) => request.destination === 'image',
new CacheFirst({cacheName: 'images'}));
registerRoute(({url}) => url.pathname.startsWith('/api/'),
new StaleWhileRevalidate({cacheName: 'api'}));
Permite engagement quando page fechada (PWA-only em iOS 16.4+, sempre em Android/desktop):
Notification.requestPermission().pushManager.subscribe({applicationServerKey: VAPID_PUBLIC}).web-push (Node), pywebpush (Py).push event, mostra self.registration.showNotification(...).Privacy: usuários odeiam push spam. Peça permission só após value clearly demonstrated; deixe opt-out fácil.
registration.sync.register('tag')): quando user offline, registra; quando volta online, SW dispara sync event. Use pra POSTs que falharam.registration.periodicSync.register('news', {minInterval: ...})): browser agenda fetch periódico. Suporte limitado (Chrome só, com PWA installed). Valor questionável.beforeinstallprompt event fires quando navegador acha PWA instalável (Manifest válido + SW registered + critérios engagement). Capture, mostre UI custom, depois e.prompt(). Manifest mínimo:
{
"name": "Fathom Logística",
"short_name": "Logística",
"start_url": "/",
"display": "standalone",
"icons": [{"src":"/icon-512.png","sizes":"512x512","type":"image/png"}]
}
waiting até todas tabs fecharem. Force update via skipWaiting() + clients.claim() em activate, ou aviso UI "nova versão disponível, reload"./sw.js controla todo origin; SW em /app/sw.js controla só /app/*. Erro comum.Cruza com 02-05 (Next.js next-pwa plugin), 02-14 (push real-time), 03-09 (PWA acelera perceived perf).
Quatro APIs cruzaram pra Baseline 2024 e mudam código que antes exigia lib. Saiba usar nativo antes de instalar dependência.
Same-document (Chromium 111+, Safari 18, Firefox 129+ flagged): document.startViewTransition tira snapshot do DOM antes, executa callback, tira snapshot depois, faz cross-fade automático no compositor.
function navigateToOrder(id: string) {
if (!document.startViewTransition || matchMedia('(prefers-reduced-motion: reduce)').matches) {
router.push(`/orders/${id}`);
return;
}
const vt = document.startViewTransition(() => router.push(`/orders/${id}`));
vt.ready.then(() => analytics.mark('vt-ready'));
vt.finished.catch(() => {}); // skipped/aborted, não warne
}
Cross-document (MPA, Chromium 126+) habilita transição entre full reloads same-origin:
@view-transition { navigation: auto; }
/* Hero do pedido na lista vira hero do detalhe */
.order-card[data-id="42"] img { view-transition-name: order-42-hero; }
.order-detail img { view-transition-name: order-42-hero; }
::view-transition-old(order-42-hero),
::view-transition-new(order-42-hero) { animation-duration: 240ms; }
::view-transition-group(order-42-hero) { animation-timing-function: cubic-bezier(.2,.8,.2,1); }
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*) { animation: none; }
}
Pegadinhas: view-transition-name duplicado em página = browser pula transition silently; layout shifts dentro do callback animam suavemente (FLIP grátis); vt.updateCallbackDone resolve quando DOM update termina, vt.ready quando pseudo-elements montaram, vt.finished quando animation acabou. Cross-document só funciona same-origin.
Atributo nativo. Browser gerencia top-layer, light-dismiss, focus, ESC. Substitui ~90% de Floating UI / Radix Popover.
<!-- Tooltip: auto = ESC + click-outside fecham -->
<button popovertarget="tip-eta">ETA</button>
<div id="tip-eta" popover="auto">Estimativa baseada em Haversine + tráfego.</div>
<!-- Menu dropdown: manual = só fecha programaticamente -->
<button popovertarget="menu-courier">Courier</button>
<menu id="menu-courier" popover="manual">
<li><button onclick="this.closest('[popover]').hidePopover()">Atribuir</button></li>
</menu>
<!-- Modal substitute -->
<div id="confirm" popover="manual">Confirmar entrega?</div>
[popover]:popover-open { opacity: 1; transform: translateY(0); }
[popover] { opacity: 0; transform: translateY(-4px); transition: opacity .15s, transform .15s, display .15s allow-discrete; }
[popover]::backdrop { background: rgb(0 0 0 / .4); backdrop-filter: blur(2px); }
@starting-style {
[popover]:popover-open { opacity: 0; transform: translateY(-4px); }
}
Comparação: popover="auto" = tooltip/menu (light-dismiss); popover="manual" = control total (use pra modal crítico, nunca auto aí senão click-outside descarta confirm); <dialog> = modal com showModal() + form integration; Floating UI = só pra positioning complexo (collision, virtual elements) onde anchor positioning CSS ainda não cobre.
@starting-style (CSS Transitions Level 2, Baseline 2024)Define estado inicial de element entrando na DOM. Sem isso, transition de display: none → block pulava porque não havia "from state". Combine com transition-behavior: allow-discrete pra animar display e overlay (top-layer) discretely. Vide bloco acima.
Mutex cross-tab same-origin. Resolve "duas tabs abertas, qual sincroniza WebSocket de courier tracking".
async function leaderTab(): Promise<void> {
await navigator.locks.request('logistica:courier-ws', { mode: 'exclusive' }, async (lock) => {
if (!lock) return; // ifAvailable retornou null
const ws = openCourierSocket();
await new Promise<void>((resolve) => { ws.addEventListener('close', () => resolve()); });
});
}
leaderTab(); // tabs subsequentes esperam; quando líder fecha, próxima assume
// Inspect held/pending
const snapshot = await navigator.locks.query();
console.log(snapshot.held, snapshot.pending);
Modes: exclusive (default) bloqueia outras requests; shared permite múltiplas leituras concorrentes. steal: true força tomada (use só pra recovery de tab travada). Sempre passe signal: AbortSignal.timeout(30_000) em request crítico — sem timeout, tab freezada deadlock outras.
Filesystem sandboxed por origin, invisível em OS file picker, performant. Backend ideal pra SQLite WASM (sql.js-httpvfs / wa-sqlite) e blobs grandes.
// Worker context (sync API só roda em worker)
const root = await navigator.storage.getDirectory();
const fh = await root.getFileHandle('orders.sqlite', { create: true });
const sync = await fh.createSyncAccessHandle(); // SYNC, throws fora de worker
sync.write(buffer, { at: 0 });
sync.flush();
sync.close();
Vs IndexedDB: OPFS é byte-stream/file-handle (mmap-friendly, melhor pra SQLite e blobs >10 MB); IndexedDB é structured data com índices. OPFS sync API só dentro de Worker — chamar de main thread throws InvalidStateError.
prefers-reduced-motion check, viola WCAG 2.3.3.view-transition-name duplicado, browser cancela transition sem warning.@starting-style sem transition-behavior: allow-discrete, display pula sem animar.navigator.locks.request sem signal/timeout, tab travada deadlock cross-tab.createSyncAccessHandle fora de Worker, throws.popover="auto" em modal de confirm de pagamento, click-outside descarta sem feedback.IntersectionObserver em hero LCP com rootMargin: '0px' e threshold: 1, callback dispara depois do paint, perde a janela de pre-fetch e degrada LCP em vez de melhorar.MutationObserver em document.body com { childList: true, subtree: true, attributes: true } sem debounce em SPA grande, callback dispara milhares de vezes por segundo durante hydration; restrinja subtree: false ou observe nó específico + requestIdleCallback pra batchar.Cruza com 02-01 (CSS, view-transition-name + container queries), 02-02 (a11y, prefers-reduced-motion + popover focus management nativo), 02-04 (React 19 useTransition + View Transitions integration via flushSync), 02-05 (Next.js cross-document VT em App Router com unstable_ViewTransition), 03-09 (View Transitions roda no compositor, evitam relayout custom em JS).
Você precisa, sem consultar:
localStorage é ruim em hot path.Construa uma mini-PWA "Logística Offline-First" vanilla, sem framework.
A app permite registrar entregas localmente quando offline e sincronizar com servidor (mock) quando volta online.
UI vanilla com Web Components (use customElements.define):
<delivery-list>, <delivery-form>, <delivery-item>./, /new, /delivery/:id.Storage:
idb se quiser, é wrapper fino).deliveries store com id, address, status, createdAt, syncedAt.Sync:
/api/deliveries e tenta enviar.online event) pra retry quando voltar conectividade.UX:
<dialog> ou custom element + live region.navigator.onLine === false.Workers:
idb (opcional).requestAnimationFrame é macrotask especial. Microtasks (Promise) precedem rAF.next-pwa).fetch, URL, WebStreams, AbortController, mesma API. Reuso de conhecimento.Threshold de Maestria
Acerte todas as 5 pra marcar o módulo como concluído. Sem pressa, sem timer. Tudo fica salvo no teu navegador.
Q1Em uma tabela com 1000 linhas, qual padrão é mais eficiente para registrar handlers de click?
Q2Qual o problema crítico com `localStorage` em um hot path da aplicação?
Q3Sobre o comportamento de `fetch()` quando recebe status 404 ou 500, o que acontece?
Q4Qual a diferença fundamental entre Web Worker e Service Worker?
Q5Por que usar `view-transition-name` duplicado em diferentes elementos da página é problemático?
Destrava
02-03 é prereq dos seguintes módulos: