Estágio 02 · 02-03
LockedA "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).
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.Destrava
02-03 é prereq dos seguintes módulos: