Cheat SheetsJavaScript & TypeScriptThe Browser

The Browser — Cheat Sheet

JavaScript & TypeScript · 8 topics. Download the PDF or the Instagram carousel and share it.

Cheat Sheet · AiCanCode.org
The Browser
JavaScript & TypeScript8 topicsQuick revision reference
1

The DOM — Selecting and Changing the Page

The DOM is a tree of objects representing the page. You will mostly let React manage it, but understanding it is what lets you debug React and handle the cases React does not cover.

  • querySelector/querySelectorAll accept any CSS selector and cover nearly every selection need
  • querySelectorAll returns a static snapshot; getElementsBy* return live collections that update as you mutate
  • textContent is safe; innerHTML with untrusted input is an XSS vulnerability
  • dataset reads and writes data-* attributes, and values are always strings
  • Reading offsetHeight or getBoundingClientRect forces layout — batch reads before writes to avoid thrashing
querySelector, and live vs static lists
document.querySelector('.card')            // first match, or null
document.querySelectorAll('.card')         // static NodeList of all matches
document.getElementById('root')            // fastest, id only

// querySelectorAll returns a static list — not live
const items = document.querySelectorAll('li')   // snapshot
// getElementsByClassName returns a LIVE collection that updates itself,
// which causes infinite loops if you append inside a loop over it

// NodeList has forEach but not map; convert when you need array methods
[...document.querySelectorAll('li')].map(li => li.textContent)

// Searching within an element, not the whole document
card.querySelector('.title')
2

Events — Bubbling, Delegation and Cleanup

Events travel down to the target and back up again. Bubbling is what makes delegation possible, and delegation is what makes one listener handle a thousand rows.

  • Events capture down to the target then bubble back up; listeners default to the bubble phase
  • e.target is where the event happened, e.currentTarget is where the listener is attached
  • Delegation puts one listener on a container and uses closest() — it also works for elements added later
  • preventDefault cancels the default action; stopPropagation stops travel — they are unrelated
  • removeEventListener needs the identical function reference; an AbortController signal removes many at once
Capture, target, bubble
// document -> ... -> parent -> TARGET -> parent -> ... -> document
//         capture phase            |         bubble phase

parent.addEventListener('click', h)              // bubble (default)
parent.addEventListener('click', h, true)        // capture
parent.addEventListener('click', h, { capture: true, once: true })

// e.target  — where it actually happened
// e.currentTarget — where this listener is attached

parent.addEventListener('click', e => {
  e.target        // the button that was clicked
  e.currentTarget // parent
})

e.stopPropagation()   // stop travelling further
e.preventDefault()    // cancel the default action; does NOT stop bubbling
3

Forms — Inputs, Validation and Submission

Forms are where most user data enters an application. The browser gives you validation, keyboard handling and accessibility free — if you use a real form element rather than a div with a button.

  • A real form element gives Enter-to-submit, validation, autofill and screen-reader support for free
  • preventDefault in the submit handler stops the page navigating away
  • FormData reads every named field at once — which is why inputs need name attributes, not just ids
  • Constraint attributes (required, minlength, pattern) provide validation without JavaScript; :user-invalid styles only after interaction
  • Disable the submit button while the request is in flight, or a double-click submits twice
FormData, and why name attributes matter
form.addEventListener('submit', async (e) => {
  e.preventDefault()                     // stop the page navigating

  const data = new FormData(form)
  data.get('email')                      // by name attribute
  Object.fromEntries(data)               // plain object, for JSON APIs

  await fetch('/api/signup', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(Object.fromEntries(data)),
  })
})

// Sending files: pass the FormData directly, no Content-Type header
await fetch('/api/upload', { method: 'POST', body: new FormData(form) })

// Checkboxes and multi-selects need getAll
data.getAll('topics')     // ['arrays', 'graphs']
4

Browser Storage — localStorage, Cookies and IndexedDB

Four options with different lifetimes, sizes and security properties. The choice that matters most is where an auth token goes, and the answer is usually not localStorage.

  • localStorage persists across restarts, sessionStorage clears with the tab, and both store strings only
  • localStorage access can throw in private mode or when the quota is full — always wrap it in try/catch
  • Anything in localStorage is readable by any script on the page, so one XSS exposes every stored token
  • An httpOnly, Secure, SameSite cookie cannot be read by JavaScript, which is the stronger default for auth
  • The storage event fires in other tabs, giving cross-tab sync for free; IndexedDB is the option for large structured data
Strings only, and it can throw
localStorage.setItem('theme', 'dark')
localStorage.getItem('theme')          // 'dark', or null
localStorage.removeItem('theme')

// Objects must be serialised
localStorage.setItem('user', JSON.stringify(user))
const user = JSON.parse(localStorage.getItem('user') ?? 'null')

// It can throw — private mode, quota full, storage blocked
function safeSet(key, value) {
  try { localStorage.setItem(key, JSON.stringify(value)) }
  catch { /* quota or blocked — carry on without persisting */ }
}

// sessionStorage: identical API, cleared when the tab closes,
// and not shared between tabs even on the same site.

// Cross-tab sync comes free with the storage event:
window.addEventListener('storage', e => {
  if (e.key === 'theme') applyTheme(e.newValue)   // fires in OTHER tabs
})
5

History & URLs — Client-Side Routing Underneath

The History API changes the URL without a page load, and URLSearchParams parses query strings. Together they are what every client-side router is built on.

  • pushState changes the URL without a request; it does not fire popstate, so you render yourself
  • popstate fires when the user presses back or forward, carrying the state you pushed
  • Client-side routes 404 on refresh unless the server returns index.html for unknown paths
  • URLSearchParams handles encoding, repeated keys and ordering — never parse a query string manually
  • Filters and pagination belong in the URL so a view can be shared, bookmarked and survive a refresh
pushState, popstate, and the refresh 404
// Change the URL without a request
history.pushState({ page: 2 }, '', '/problems?page=2')
history.replaceState({}, '', '/problems')     // no new history entry

// The user pressed back or forward
window.addEventListener('popstate', (e) => {
  render(e.state)        // whatever you passed to pushState
})

// pushState does NOT fire popstate — render yourself after calling it
function navigate(url, state) {
  history.pushState(state, '', url)
  render(state)
}

// The refresh problem: /problems/two-sum has no file on the server.
// The server must return index.html for unknown paths, or a refresh 404s.
// (Vercel and Next handle this; a bare nginx config does not.)
6

Observers — Intersection, Resize and Mutation

Observers tell you when something changes without polling. IntersectionObserver handles lazy loading and infinite scroll far better than scroll listeners.

  • IntersectionObserver replaces scroll listeners for visibility — no per-frame layout reads
  • rootMargin lets you trigger before an element is actually visible, which makes infinite scroll feel instant
  • Native loading="lazy" on images needs no JavaScript at all
  • ResizeObserver watches element size, which the window resize event cannot; beware feedback loops
  • MutationObserver is a last resort for reacting to DOM changes made by code you do not control
Infinite scroll, without a scroll listener
const observer = new IntersectionObserver(
  (entries) => {
    for (const entry of entries) {
      if (entry.isIntersecting) loadNextPage()
    }
  },
  { rootMargin: '200px' },      // fire 200px early, so it feels instant
)

observer.observe(sentinelElement)     // an empty div after the last row
observer.disconnect()                 // cleanup

// Lazy images need no JavaScript at all:
<img src="thumb.jpg" loading="lazy" />

// The old way, for comparison — runs on every scroll event
// and forces layout each time:
window.addEventListener('scroll', () => {
  if (el.getBoundingClientRect().top < innerHeight) load()
})
7

Useful Browser APIs

Clipboard, geolocation, notifications, media queries, crypto and Web Workers — the built-ins worth knowing before reaching for a library.

  • crypto.randomUUID() and navigator.clipboard.writeText() replace common dependencies with one call each
  • matchMedia reads media queries from JavaScript and fires on change — the correct way to react to dark mode
  • Permission prompts need a user gesture; requesting notifications on page load gets you permanently blocked
  • Always handle permission refusal — a denied prompt cannot be re-asked
  • Web Workers give genuine parallelism but have no DOM access, and data is copied across the boundary unless transferred
clipboard, crypto, matchMedia
// Clipboard — requires a user gesture and a secure context
await navigator.clipboard.writeText(code)
const text = await navigator.clipboard.readText()   // needs permission

// Crypto — proper random, no library needed
crypto.randomUUID()                    // 'a3f8...' RFC 4122 v4
crypto.getRandomValues(new Uint8Array(16))

// Media queries from JavaScript, including live changes
const dark = window.matchMedia('(prefers-color-scheme: dark)')
dark.matches                           // boolean now
dark.addEventListener('change', e => applyTheme(e.matches))

const reduced = window.matchMedia('(prefers-reduced-motion: reduce)')
if (!reduced.matches) animate()
8

How the Browser Renders — and Why Your Page Janks

Style, layout, paint, composite. Knowing which CSS properties skip layout is the difference between a smooth animation and a stuttering one.

  • The pipeline is style, layout, paint, composite — what you change decides how many stages run
  • transform and opacity composite on the GPU; top, left, width and height force a full layout
  • A frame is 16.7ms at 60fps, shared with the browser — anything over 50ms on the main thread blocks input
  • requestAnimationFrame syncs to paint; setInterval for animation drops frames
  • CLS is usually fixed by giving images explicit dimensions so space is reserved before they load
transform and opacity are free; top and left are not
// Style -> Layout -> Paint -> Composite

// Changes LAYOUT (all four stages) — expensive
width, height, top, left, margin, padding, font-size, display

// Changes PAINT only (skips layout)
color, background-color, box-shadow, border-radius, visibility

// COMPOSITE only (GPU, skips layout and paint) — cheapest
transform, opacity

// So this janks:
el.style.left = x + 'px'

// And this does not:
el.style.transform = `translateX(${x}px)`

// Hint to the browser before an animation starts:
.card { will-change: transform }   // use sparingly; it costs memory
Learn this free with Aria, your AI tutor → AiCanCode.org/learn/javascript