Browser, HTTP & networking

Web APIs worth knowing by name

The platform APIs a senior frontend engineer should be able to reach for without searching — URL and encoding, Web Crypto, files and blobs, history and the Navigation API, View Transitions, Page Lifecycle and bfcache, the Performance and observer APIs, Speculation Rules, clipboard and share, Web Components and WebAssembly — with the questions each one answers.

The senior signal is not knowing every API; it is knowing which one exists for a problem so you do not install a package for something the browser does. This page is a map of the ones that come up in interviews and code reviews, grouped by the question they answer, with the 2026 support status where it matters.

Know

  • Working with URLs and bytes: URL and URLSearchParams (parse, build, URL.canParse()/URL.parse() without try/catch), encodeURIComponent versus encodeURI, TextEncoder/TextDecoder, Uint8Array.toBase64()/fromHex() (ES2026), structuredClone for deep copies (Dates, Maps, cycles; not functions).
  • Crypto: crypto.randomUUID() for ids, crypto.getRandomValues() for tokens, crypto.subtle for hashing (digest('SHA-256')), HMAC signatures (webhooks), AES-GCM encryption, key derivation (PBKDF2, HKDF); available in browsers, Node and edge runtimes, which is why h3 and nuxt-auth-utils build on it. Never hand-roll crypto; never do password hashing on the client.
  • Files and blobs: Blob, File, FileReader (or blob.text()/arrayBuffer()/stream()), URL.createObjectURL() (revoke it), FormData for multipart uploads, <input type="file"> with accept/capture, drag-and-drop DataTransfer, the File System Access API (Chromium: showOpenFilePicker, write back to disk) versus the download fallback (<a download>), showSaveFilePicker.
  • Navigation: the history API (pushState, replaceState, popstate) that routers use; the Navigation API (navigation.navigate(), navigate event with intercept(), navigation.entries()) in Chromium and Safari, the modern replacement Vue Router does not yet use; View Transitions API (document.startViewTransition, ::view-transition-* pseudo-elements, view-transition-name) for same-document and cross-document transitions, which Nuxt wires through experimental.viewTransition; Speculation Rules (prefetch/prerender with eagerness) for instant navigations; scroll-behavior, scrollIntoView({ block }), scrollend event.
  • Page Lifecycle and bfcache: visibilitychange and document.visibilityState (pause work, send beacons), pageshow/pagehide with event.persisted for back/forward cache restores, freeze/resume (Chromium). bfcache restores a page instantly on Back and counts as a fast navigation for the vitals; it is blocked by unload listeners, Cache-Control: no-store on the HTML (relaxed in Chrome for some cases), open connections (WebSocket, IndexedDB transactions in flight in some browsers), and beforeunload in some cases; test it in DevTools → Application → Back/forward cache.
  • Performance measurement: performance.now(), mark/measure (visible in the Performance panel), PerformanceObserver for largest-contentful-paint, layout-shift, event (INP), longtask/long-animation-frame (LoAF, with script attribution), resource and navigation timing, paint; the web-vitals library wraps these; performance.memory is deprecated in favour of measureUserAgentSpecificMemory() (cross-origin isolated pages).
  • Scheduling: requestAnimationFrame (before paint), requestIdleCallback (when idle, with deadline), scheduler.postTask() with priorities and scheduler.yield() to break up long tasks, queueMicrotask, setTimeout; AbortSignal.timeout()/AbortSignal.any() to compose cancellation.
  • Observers: IntersectionObserver (visibility, lazy loading, infinite scroll, rootMargin, threshold), ResizeObserver (element size; contentBoxSize/borderBoxSize), MutationObserver (DOM changes, batched as microtasks), PerformanceObserver (above), ReportingObserver (deprecations, interventions, CSP reports).
  • User-facing capabilities: navigator.clipboard.writeText()/read() (permission-gated, user gesture), navigator.share() (Web Share, mobile-first), Notification + Web Push, Geolocation, Permissions API to check before asking, Screen Wake Lock, Vibration, Battery (limited), matchMedia() for preference and viewport queries in JS, Intl (see the JavaScript section).
  • Communication and workers: postMessage with origin checks (always verify event.origin), MessageChannel, BroadcastChannel, SharedWorker, Web Locks, EventSource, WebSocket, WebTransport (QUIC-based, Chromium/Firefox), WebRTC for peer media and data.
  • Components and compute: Custom Elements and Shadow DOM (defineCustomElement in Vue), <template>/<slot>, ElementInternals for form-associated elements; WebAssembly (WebAssembly.instantiateStreaming; how SQLite runs in the browser for Nuxt Content's client-side queries in this very app), WebGPU for compute and rendering, OffscreenCanvas.
  • Security-adjacent APIs: Trusted Types (CSP-enforced DOM sink typing; Vue 3.5 supports it), the Sanitizer API (setHTML(), still maturing; verify), Permissions-Policy header controls feature availability, Credential Management and WebAuthn/passkeys (navigator.credentials) for phishing-resistant login.
  • Feature detection: 'startViewTransition' in document, CSS.supports(), @supports, and Baseline as the vocabulary; polyfill only what is worth its bytes.

How it works

Instant back navigation: making a page bfcache-eligible and measuring it:

app/plugins/lifecycle.client.ts
export default defineNuxtPlugin(() => {
  // ✗ window.addEventListener('unload', save)  -> blocks bfcache in every browser
  // ✓ pagehide fires on both real unloads and bfcache freezes
  window.addEventListener('pagehide', () => navigator.sendBeacon('/api/telemetry/leave', JSON.stringify({ path: location.pathname })))

  window.addEventListener('pageshow', (event) => {
    if (event.persisted) refreshNuxtData() // restored from bfcache: data may be stale, refresh quietly
  })

  document.addEventListener('visibilitychange', () => {
    if (document.visibilityState === 'hidden') pauseLiveUpdates()
    else resumeLiveUpdates()
  })
})

The Performance APIs behind the vitals, without a library, for a custom dashboard:

app/utils/observe-perf.ts
export function observePerformance(report: (entry: { type: string, value: number, detail?: string }) => void) {
  new PerformanceObserver((list) => {
    for (const e of list.getEntries()) report({ type: 'lcp', value: e.startTime, detail: (e as LargestContentfulPaint).element?.tagName })
  }).observe({ type: 'largest-contentful-paint', buffered: true })

  new PerformanceObserver((list) => {
    for (const e of list.getEntries() as PerformanceEntry[] & { hadRecentInput?: boolean, value?: number }[]) {
      if (!e.hadRecentInput) report({ type: 'layout-shift', value: e.value ?? 0 })
    }
  }).observe({ type: 'layout-shift', buffered: true })

  new PerformanceObserver((list) => {
    for (const e of list.getEntries()) report({ type: 'long-animation-frame', value: e.duration, detail: (e as any).scripts?.[0]?.sourceURL })
  }).observe({ type: 'long-animation-frame', buffered: true })
}

Web Crypto for a webhook signature check in a Nitro route (same API in browser, Node and edge):

server/api/webhooks/payments.post.ts
export default defineEventHandler(async (event) => {
  const raw = await readRawBody(event, 'utf8') ?? ''
  const signature = getHeader(event, 'x-signature') ?? ''
  const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(useRuntimeConfig(event).webhookSecret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'])
  const mac = new Uint8Array(await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(raw)))
  const expected = mac.toHex()
  if (expected.length !== signature.length || !timingSafeEqual(expected, signature)) throw createError({ status: 401, statusText: 'Bad signature' })
  return handleEvent(JSON.parse(raw))
})

View transitions and speculation, the two navigation upgrades that need no framework:

app/composables/useSmoothSwap.ts
export async function swapContent(update: () => void) {
  if (!document.startViewTransition || matchMedia('(prefers-reduced-motion: reduce)').matches) return update()
  await document.startViewTransition(update).finished
}
app/app.vue
<script setup lang="ts">
useHead({ script: [{ type: 'speculationrules', innerHTML: JSON.stringify({ prefetch: [{ where: { href_matches: '/study/*' }, eagerness: 'moderate' }] }) }] })
</script>
Gotcha· postMessage without an origin check

window.addEventListener('message', e => apply(e.data)) accepts messages from any embedded or embedding page. Always check e.origin against an allow-list and validate the payload; the same rule applies to BroadcastChannel data from other tabs (trusted origin, untrusted content).

Gotcha· Object URLs that never die

URL.createObjectURL(blob) pins the blob in memory until URL.revokeObjectURL() or the document unloads. Image previews in a long-lived SPA leak megabytes per file; revoke after the image has loaded or when the preview is replaced.

Verify before the interview:

Support status of the Navigation API (Firefox), the Sanitizer API, scheduler.yield() and Speculation Rules prerendering as of the interview week.

docs ↗

Exercise

Exercise
  • Open DevTools → Application → Back/forward cache on a page of your app, click "Test back/forward cache", and fix every reason listed.
  • Implement observePerformance and log LCP, CLS and LoAF entries while interacting with a heavy page; identify the script behind the longest frame.
  • Verify a webhook signature with crypto.subtle in a Nitro route and confirm the same code runs in a Cloudflare Worker.

Be able to say

Be able to say· What is the back/forward cache and how do you keep a page eligible?

"When a user navigates away, the browser can freeze the whole page, JavaScript heap included, and restore it instantly on Back or Forward, which counts as an immediate navigation for the vitals. A page is ineligible if it registered an unload listener, if its HTML came with Cache-Control: no-store in most cases, or if it holds open connections such as a WebSocket at the time of navigation. So I use pagehide and visibilitychange instead of unload, send analytics with sendBeacon, close sockets on hide and reopen on pageshow, and when pageshow fires with persisted true I quietly refresh data that may have gone stale. The DevTools Application panel has a test that lists the blocking reasons."

Be able to say· Which observer API would you use for lazy loading, element resizing and DOM changes?

"IntersectionObserver for lazy loading and infinite scroll: it reports when an element crosses the viewport or a rootMargin around it, asynchronously, replacing scroll handlers and getBoundingClientRect polling; it is also what lazy hydration strategies use. ResizeObserver for element size changes, which is how you build container-query-like behaviour in JavaScript and equalise heights without thrashing. MutationObserver for DOM changes, batched as microtasks, useful for integrating third-party widgets. And PerformanceObserver for the browser's own measurements: LCP candidates, layout shifts, long animation frames and event timing, which is what the web-vitals library wraps."

Your notes

Open in Notes

Nothing yet. Notes filed here show up on this page and the section overview.