A browser is not merely a window around React. It is a document parser, style engine, layout system, graphics compositor, JavaScript host, network client, storage manager and security boundary.
Before we begin: let us open the browser together
When I first learned frontend development, I could make pages work without truly understanding who was doing the work. I wrote HTML, CSS and JavaScript; something inside the browser turned them into a screen. When the screen behaved strangely, I guessed.
These are my self-notes for replacing that guesswork with a calm mental model. Imagine you are sitting beside me with DevTools open. You can ask the basic question before the advanced one. I will answer the question first, then show the mechanism, then give us a small experiment. The code is not the lesson by itself; it is evidence for the explanation.
Our repeated question will be: which browser system owns this result? If a resource never arrives, we inspect networking. If a CSS declaration loses, we inspect the cascade. If the winning style is correct but geometry is wrong, we inspect layout. If a click reaches an unexpected listener, we inspect propagation. If a removed screen stays in memory, we follow references.
You do not need to memorize a browser engine. Engines evolve and differ. You need a durable map that helps you predict, observe and correct your understanding.
1. “What actually happens after I press Enter on a URL?”
When a user enters a URL, the browser resolves the host, establishes a connection, negotiates security for HTTPS, sends an HTTP request and receives bytes. The response headers describe caching and content; the HTML bytes begin the document pipeline.
URL → DNS → connection/TLS → HTTP → HTML bytes
→ DOM + CSSOM → layout → paint → composite
This is a mental model, not a claim that every stage waits for the previous one completely. Browsers stream, preload and perform work concurrently.
2. “Is the DOM simply my HTML file kept in memory?”
Not quite. Your HTML response is a stream of characters. The DOM is the browser's repaired, live object model of the document. JavaScript can change it, custom elements can add behavior, and the parser may correct markup that was not nested legally. That is why View Source and the Elements panel can show different things.
Hold onto that distinction. Source is what arrived; DOM is what the document has become.
The HTML parser tokenises markup and constructs the Document Object Model.
<main>
<h1>StudyDesk</h1>
<button id="start">Start lesson</button>
</main>
The DOM is a tree of objects, not the original text file. Invalid markup may be repaired according to parsing rules, sometimes producing a tree different from what a developer imagined.
const button = document.querySelector("#start");
button.textContent = "Continue lesson";
DOM changes can trigger style, layout and paint work depending on what changed.
Classic scripts can block parsing while fetched and executed. defer downloads in parallel and runs after parsing in document order. Module scripts are deferred by default.
<script defer src="app.js"></script>
<script type="module" src="main.js"></script>
3. “How does the browser turn CSS text into usable styles?”
CSS is parsed into a model used to compute styles.
:root { --accent: #60a5fa; }
.lesson { color: var(--accent); }
.lesson[data-complete="true"] { opacity: .7; }
The cascade resolves competing declarations using origin, importance, layers, specificity and source order. Inheritance passes selected computed values from parent to child. A browser combines relevant DOM nodes and computed styles into structures used for rendering.
4. “How do styled elements finally become pixels?”
There is no single “render” button. The browser resolves styles, calculates geometry, records drawing work and combines prepared surfaces. We use the words style, layout, paint and composite because each answers a different question. When performance is poor, naming the repeated stage is much more useful than saying “rendering is slow.”
Layout calculates geometry: sizes and positions. Paint records visual instructions such as text, borders and shadows. Compositing assembles painted layers, often using the GPU.
Changing width may require layout and paint. Changing transform can often be handled during compositing after appropriate layer creation, but “GPU accelerated” is not automatically faster; extra layers consume memory.
// Avoid repeated layout reads and writes in one loop.
const width = panel.getBoundingClientRect().width;
panel.style.width = `${width + 20}px`;
Batch DOM reads, then writes. Animate transform and opacity when visually appropriate. Profile rather than guessing.
5. “Why can a parent handle a click on its child?”
Events have capture, target and bubble phases.
const list = document.querySelector("#lessons");
list.addEventListener("click", event => {
const button = event.target.closest("button[data-lesson-id]");
if (!button || !list.contains(button)) return;
openLesson(button.dataset.lessonId);
});
This is event delegation: one ancestor handler serves present and future children. event.target is the originating element; event.currentTarget is the element whose listener is running.
preventDefault() prevents a cancelable default action. stopPropagation() changes event travel and should not be a reflex.
6. “Why does a DOM change sometimes appear later?”
Because updating an object and presenting a frame are separate moments. JavaScript runs as work on the main thread. The browser normally waits until that work and its microtasks yield before performing the next rendering opportunity. A four-second loop can change textContent immediately in memory and still prevent the user from seeing it for four seconds.
JavaScript jobs run to completion. Promise microtasks run before the browser proceeds to the next ordinary task; rendering opportunities occur between work according to host scheduling.
requestAnimationFrame(() => {
card.style.transform = "translateX(20px)";
});
requestAnimationFrame schedules work before a rendering opportunity. It is useful for visual updates, not background polling. Long tasks delay input and paint.
7. “Which browser storage should I use, and why are they different?”
- cookies travel with matching HTTP requests and support server-managed sessions;
localStorageis synchronous string storage;sessionStorageis scoped to a tab/session;- IndexedDB provides asynchronous structured storage;
- Cache Storage stores request/response pairs, often with service workers.
HttpOnly, Secure and an appropriate SameSite policy.
8. “How does the browser keep one site away from another?”
An origin combines scheme, host and port. The same-origin policy limits how documents interact across origins. CORS is a server-controlled protocol that can permit selected cross-origin reads; it is not authentication.
Access-Control-Allow-Origin: https://app.example.com
Vary: Origin
Content Security Policy limits permitted sources and can reduce XSS impact.
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'
Deploy CSP carefully, begin with reporting when necessary, and avoid weakening it with broad wildcards.
9. “How can code respond to requests while the page is offline?”
A service worker can intercept requests within its scope and respond from a cache or network.
self.addEventListener("fetch", event => {
event.respondWith(caches.match(event.request).then(cached => cached ?? fetch(event.request)));
});
Real strategies must consider freshness, versioning, failed writes, authentication and cache storage limits. Never cache private responses under a shared key.
10. “What does browser performance feel like to a real user?”
Measure loading, responsiveness and visual stability. Reserve image dimensions, reduce render-blocking work, split genuinely optional JavaScript, stream useful content and avoid request waterfalls.
<img src="lesson-cover.webp" width="800" height="450" alt="JavaScript lesson diagram">
Use DevTools Network, Performance, Memory, Rendering and Accessibility panels. Diagnose the first expensive layer rather than optimising random code.
11. “Can we practise the entire journey on one small page?”
Build a no-framework lesson list. Load JSON, render with a document fragment, delegate click events, persist a harmless preference, cancel an obsolete search, add a CSP and inspect layout/paint. Test keyboard navigation and a throttled network.
Mastery statement: The browser turns network responses into an interactive, rendered and security-isolated document. Framework code participates in that pipeline; it does not replace it.
13. “Can you trace one navigation without skipping steps?”
Suppose I visit https://studydesk.example/courses/javascript.
- The browser separates scheme, host, port, path, query and fragment.
- It checks policies, caches, a service worker and connection reuse opportunities.
- DNS resolves the host to network addresses.
- A transport connection is established. HTTPS adds TLS negotiation and certificate verification.
- The browser sends an HTTP request with headers such as accepted formats, cookies and cache validators.
- Intermediaries may serve, forward or transform the response according to protocol rules.
- The response status, headers and body arrive, often as a stream.
- The browser determines how to process the content type.
- HTML parsing discovers more resources and builds the document.
#closures, is normally handled by the browser and not sent in the HTTP request. Query parameters are sent because they are part of the request target.
const url = new URL("https://studydesk.example/search?q=closure#results");
console.log(url.protocol); // https:
console.log(url.hostname); // studydesk.example
console.log(url.pathname); // /search
console.log(url.searchParams.get("q")); // closure
console.log(url.hash); // #results
14. “Must the browser download all HTML before parsing?”
The browser does not always wait for the complete HTML file. It can parse arriving chunks, construct nodes and discover resources progressively. This is why server streaming can improve perceived performance.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JavaScript course</title>
<link rel="stylesheet" href="/course.css">
</head>
<body>
<header>StudyDesk</header>
<main id="course">...</main>
</body>
</html>
Place character encoding information early. Use valid structure and let the parser do predictable work. When a table, paragraph or form is nested incorrectly, the browser's error recovery may rearrange nodes.
console.log(document.querySelector("table")?.innerHTML);
Inspect the DOM tree rather than assuming it matches source indentation.
15. “When should I use a normal, async, defer or module script?”
<script src="classic.js"></script>
<script defer src="deferred.js"></script>
<script async src="analytics.js"></script>
<script type="module" src="app.js"></script>
- Classic without attributes normally pauses HTML parsing while the script is fetched and executed.
deferdownloads without blocking parsing and executes after parsing, preserving document order among deferred classic scripts.asyncexecutes when ready; order relative to other async scripts is not guaranteed.- Modules are deferred by default and support explicit imports.
async for independent scripts. Use modules or defer when code depends on parsed document structure or order.
16. “Why did this CSS rule lose even though it appears later?”
Source order is only the final tie-breaker. Before it, the browser considers origin, importance, cascade layer and specificity. When we forget those earlier decisions, we keep moving a rule downward or adding !important, and the stylesheet becomes an argument nobody understands.
Instead, inspect the losing declaration in DevTools. The browser will usually show which declaration won. Then ask *why* it had priority.
Think of CSS as several people making requests about the same element. The cascade is the meeting procedure that decides which request wins. Specificity is only one vote-counting rule.
@layer reset, theme, components, utilities;
@layer theme {
:root { --course-accent: #60a5fa; }
}
@layer components {
.course-card { border-color: var(--course-accent); }
}
Computed values are resolved per element. Some properties inherit; many do not. Custom properties normally inherit and are resolved where used.
.course { color: #e5e7eb; }
.course button { color: inherit; }
Use DevTools' Computed panel to identify the winning declaration and inherited source rather than adding another selector blindly.
17. “What is layout calculating in everyday language?”
Layout resembles arranging furniture in rooms. Normal flow places block content in its writing direction. Flexbox arranges items along a main axis. Grid divides an area into rows and columns. Positioned elements use containing blocks.
.course-shell {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(16rem, 22rem);
gap: 2rem;
}
@media (max-width: 52rem) {
.course-shell { grid-template-columns: 1fr; }
}
The minmax(0, 1fr) detail allows long content to shrink rather than overflow because grid items otherwise have intrinsic minimum sizing behavior.
Layout bugs are often constraint bugs: an item has an intrinsic width, a parent has insufficient space, or an absolutely positioned element uses a different containing block than expected.
18. “How can innocent DOM reads and writes force repeated layout?”
The browser likes to batch geometry work. A style write can make old geometry stale. If our next line immediately asks for an exact width, the browser may have to stop and calculate layout before JavaScript can continue. Repeating write–read–write–read turns one potential batch into many forced calculations.
Some DOM reads require up-to-date layout. Alternating a write and layout read repeatedly can force the browser to recalculate many times.
// Poor pattern
for (const card of cards) {
card.style.width = `${container.offsetWidth / 3}px`;
}
Read once and write in a group:
const containerWidth = container.getBoundingClientRect().width;
const cardWidth = containerWidth / 3;
for (const card of cards) {
card.style.width = `${cardWidth}px`;
}
CSS Grid may eliminate the JavaScript entirely, which is even better.
19. “Can one listener safely manage many dynamic controls?”
<ul id="lesson-list">
<li><button data-action="open" data-id="scope">Scope</button></li>
<li><button data-action="open" data-id="closure">Closure</button></li>
</ul>
const list = document.querySelector("#lesson-list");
list.addEventListener("click", event => {
if (!(event.target instanceof Element)) return;
const control = event.target.closest("button[data-action='open']");
if (!control || !list.contains(control)) return;
openLesson(control.dataset.id);
});
One listener handles many controls. Newly appended matching controls work automatically. We still use real buttons so keyboard activation and semantics are provided by the platform.
Capture is useful when an ancestor needs to observe an event before its target handlers. Bubbling is the usual delegation tool. Some events have special propagation behavior; consult documentation rather than assuming.
20. “When does the event loop give the browser time to render?”
button.addEventListener("click", () => {
status.textContent = "Working";
queueMicrotask(() => {
status.textContent = "Microtask complete";
});
setTimeout(() => {
status.textContent = "Timer complete";
}, 0);
});
The event callback is one task. The queued microtask runs after that callback before a later timer task. The browser gets rendering opportunities around task processing, but scheduling details should not be exploited as animation folklore.
To let the browser paint progress before a large calculation, move the calculation to a worker or redesign it. Adding a zero-delay timer is not a general performance solution.
21. “Can the browser notify me instead of my code constantly checking?”
Browsers provide observer APIs for specific questions.
const observer = new IntersectionObserver(entries => {
for (const entry of entries) {
if (entry.isIntersecting) preloadLesson(entry.target.dataset.lessonId);
}
}, { rootMargin: "200px" });
document.querySelectorAll("[data-lesson-id]").forEach(element => observer.observe(element));
IntersectionObserver reports visibility relationships without continuous scroll calculations. ResizeObserver reports element size changes. MutationObserver reports DOM mutations. Disconnect observers during cleanup.
22. “Can we choose storage for three real product needs?”
Choose storage by lifetime and access requirements:
const preferences = { theme: "dark", density: "comfortable" };
localStorage.setItem("preferences", JSON.stringify(preferences));
const loaded = JSON.parse(localStorage.getItem("preferences") ?? "{}");
This is acceptable for non-sensitive preferences. It is not a good home for an unrestricted long-lived authentication token. Storage can be cleared, unavailable or modified. Validate stored data like other external input.
IndexedDB suits larger structured offline data. Cookies suit server-involved state. Cache Storage suits fetchable resources. None replaces a durable server database.
23. “Why does my new service worker sometimes wait?”
A service worker is installed, activated and then controls matching pages after lifecycle conditions are satisfied. Updates do not necessarily replace the active worker immediately because existing tabs may still use it.
self.addEventListener("install", event => {
event.waitUntil(caches.open("static-v1").then(cache => cache.addAll([
"/offline.html",
"/styles.css",
])));
});
self.addEventListener("activate", event => {
event.waitUntil(caches.keys().then(keys => Promise.all(
keys.filter(key => key !== "static-v1").map(key => caches.delete(key)),
)));
});
Version caches and design update behavior. An old application shell combined with a new API contract can break users.
24. “What does assistive technology receive from the browser?”
The browser derives an accessibility representation from HTML semantics, attributes, CSS visibility and dynamic state. Assistive technologies consume platform accessibility APIs.
<button aria-expanded="false" aria-controls="lesson-notes">Show notes</button>
<section id="lesson-notes" hidden>...</section>
When the state changes, update both visual state and accessible state. CSS-generated appearance cannot compensate for missing semantics.
25. “Which DevTools panel should I open first?”
When a page is slow or broken:
- Network: which request is late, duplicated, large or uncached?
- Console: which error occurs first?
- Elements: is the DOM what I think it is?
- Computed CSS: which rule wins?
- Performance: is time spent scripting, styling, layout, paint or idle waiting?
- Memory: what remains reachable after navigation?
- Accessibility: do names, roles, states and focus make sense?
27. “Can we now revisit the model more slowly?”
Let us now walk through the deeper details as if we were sitting together with a browser open between us. The browser continuously turns bytes into models, calculates pixels, reacts to input, protects origins and stores selected data.
Here is the gentle rule I want you to remember:
The browser is a collection of cooperating systems. When something goes wrong, identify which system owns the symptom.If the server never sends a stylesheet, investigate networking. If the stylesheet arrives but a rule loses, investigate the cascade. If the winning style is correct but the element is in the wrong place, investigate layout. If a click reaches the wrong handler, investigate event propagation. If an object remains after a screen closes, investigate references and memory. This way of thinking replaces guesswork with diagnosis.
28. “Is a modern browser really one program and one thread?”
Modern browsers commonly isolate work across multiple processes and threads. Exact architecture differs among engines and changes over time, so do not memorize a particular process diagram as universal law. Learn the reasons for separation.
A browser must coordinate at least these responsibilities:
- the browser interface, tabs and navigation;
- network requests and caches;
- parsing, style, layout and much page JavaScript;
- graphics and compositing;
- storage and permissions;
- sandboxing and isolation between sites;
- utility work such as audio, decoding or extensions.
Think of a hospital. Reception, laboratories, wards and pharmacy cooperate, but they do not all share one unlocked room. Information crosses defined boundaries. A browser is similarly more complicated than “one JavaScript engine.”
What does “main thread” mean?
In frontend conversations, the main thread usually means the renderer thread on which page JavaScript, DOM work, style and much layout activity occur. Long synchronous JavaScript can therefore delay input handling and rendering.
button.addEventListener("click", () => {
const started = performance.now();
while (performance.now() - started < 4_000) {
// Intentionally block for four seconds.
}
status.textContent = "Finished";
});
During that loop, the browser may be unable to run the normal page tasks needed to respond and paint. Setting textContent after the loop does not mean the new text is immediately visible; the browser must regain an opportunity to render.
This is our first important distinction:
Changing the DOM and displaying the changed pixels are related, but they are not the same action.
29. “What does one complete navigation story look like?”
Imagine entering https://studydesk.example/courses/javascript and pressing Enter.
Step 1: interpret the address
The browser parses the URL into a scheme, host, port, path, query and fragment. The scheme is https; the host is studydesk.example; the path is /courses/javascript.
A fragment such as #closures is normally used inside the document and is not sent as part of the HTTP request. A query such as ?page=2 is sent.
const url = new URL("https://studydesk.example/courses/javascript?page=2#closures");
console.log(url.protocol); // "https:"
console.log(url.hostname); // "studydesk.example"
console.log(url.pathname); // "/courses/javascript"
console.log(url.search); // "?page=2"
console.log(url.hash); // "#closures"
Step 2: apply policy and locate the server
The browser may apply an HSTS rule that requires HTTPS. It consults caches and resolves the host to a network address, often using information cached at several layers. A simplified explanation says “DNS turns a name into an IP address.” That is enough initially, but remember that resolvers, caches and modern privacy/security mechanisms make the real route richer.
Step 3: establish a protected connection
For HTTPS, client and server negotiate a secure connection. The browser verifies that the certificate is valid for the host and chains to a trusted authority. TLS provides confidentiality and integrity in transit. It does not prove that the application itself is honest or bug-free.
Depending on protocol support and prior connections, transport may involve TCP and TLS or QUIC for HTTP/3. Do not optimize by counting handshakes from memory; inspect the actual Network panel.
Step 4: create an HTTP request
A conceptual request looks like this:
GET /courses/javascript HTTP/1.1
Host: studydesk.example
Accept: text/html
Accept-Encoding: gzip, br
Cookie: session=opaque-value
The browser controls some headers. Matching cookies may be attached according to their attributes, request context and browser policy. A service worker may get an opportunity to intercept the request if the page is within its scope.
Step 5: receive the response
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Cache-Control: private, max-age=60
Content-Security-Policy: default-src 'self'
<!doctype html>
...
Headers affect interpretation, caching and security. Content-Type tells the browser how to interpret the body. Security policy may constrain resources and script execution. The browser can begin processing streamed HTML before the entire response arrives.
Step 6: commit the navigation
The browser associates the response with a document, chooses or creates the appropriate renderer under its isolation policy, updates session history and begins constructing the page. Redirects, downloads, authentication challenges, certificate failures and non-HTML responses alter this story.
Your first prediction exercise
Before opening DevTools, answer these:
- If DNS fails, will the HTML parser run? No; there are no document bytes.
- If HTML arrives but CSS is slow, can DOM construction begin? Yes.
- If a fragment changes on the same page, must the document always be downloaded again? Usually no.
- If TLS protects the connection, can a buggy server still return another user's data? Yes.
30. “How does the browser discover resources before the parser reaches them?”
While the main HTML parser works, browsers can look ahead for resource references and begin fetching them. You may hear this called a speculative parser or preload scanner. It helps discover CSS, scripts, fonts and images before the main parser reaches the point where each is needed.
<head>
<link rel="stylesheet" href="/styles/site.css">
<script defer src="/scripts/app.js"></script>
</head>
Place critical resource references where the browser can discover them naturally. Resources hidden behind late JavaScript cannot be discovered from initial markup.
// The image URL is invisible to the HTML scanner until this code runs.
hero.innerHTML = '<img src="/images/hero-large.webp" alt="Learner studying">';
preload can intentionally request a critical resource early:
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
But preload is a strong instruction, not decorative metadata. Overusing it competes with genuinely critical work. Verify priority and reuse in DevTools.
31. “Why can the resulting DOM differ from my indentation?”
The HTML parser tokenizes bytes and builds nodes according to detailed parsing rules. HTML is forgiving: the parser often repairs malformed markup. The resulting DOM can differ from the indentation you wrote.
<p>First paragraph
<p>Second paragraph
The parser closes the first paragraph when the second begins. DevTools Elements shows the resulting DOM, while View Source shows the delivered source. These are not interchangeable.
A table surprise
Browsers apply special parsing modes for structures such as tables. Invalid children may be moved or reinterpreted. This is one reason valid semantic markup matters: it makes the resulting tree predictable.
innerHTML invokes a parser too
container.innerHTML = "<strong>Welcome</strong>";
This parses a fragment and replaces descendants. It also creates an injection risk when the string is untrusted.
// Safe for ordinary text because no HTML is interpreted.
container.textContent = untrustedDisplayName;
Use DOM creation APIs when constructing structured content without interpreting a string:
const heading = document.createElement("h2");
heading.textContent = course.title;
container.replaceChildren(heading);
Parser-blocking scripts
A classic script without async or defer can pause parsing. The browser may need to wait for earlier stylesheets because the script could inspect computed styles. Then the script executes before parsing continues.
<script src="legacy.js"></script>
This behavior protects program expectations but can delay discovery and rendering. Use it only when the ordering requirement is real.
32. “Can we compare script loading without relying on folklore?”
Let us compare common choices.
<script src="classic.js"></script>
<script async src="analytics.js"></script>
<script defer src="application.js"></script>
<script type="module" src="main.js"></script>
- A plain classic external script blocks the parser while it is fetched and executed.
- An
asyncclassic script downloads in parallel and executes when ready; execution order among async scripts is not guaranteed by document order. - A
deferclassic script downloads in parallel and executes after parsing, preserving order among deferred classic scripts. - A module script is deferred by default, follows a module dependency graph and uses module semantics.
DOMContentLoaded fires after the document has been parsed and deferred/module scripts have executed. load waits for the page and dependent resources such as images. Neither means “the application has finished all future work.”
document.addEventListener("DOMContentLoaded", () => {
console.log("DOM parsed and deferred scripts completed");
});
window.addEventListener("load", () => {
console.log("load event completed");
});
Dynamic imports let code load on demand:
button.addEventListener("click", async () => {
const { openEditor } = await import("./editor.js");
openEditor();
});
This can reduce initial JavaScript, but a chunk requested only after a click may introduce interaction delay. Architecture is a tradeoff: load neither everything immediately nor everything at the last possible moment.
33. “Why can an HTML attribute differ from a DOM property?”
The DOM exposes objects representing document nodes. HTML attributes initialize or describe elements, while JavaScript properties represent current object state. They sometimes reflect one another and sometimes differ.
<input id="name" value="Initial value">
const input = document.querySelector("#name");
input.value = "User edited value";
console.log(input.value); // current value
console.log(input.getAttribute("value")); // initial attribute
This distinction matters when debugging forms. The Elements panel may show DOM properties separately from serialized attributes.
Static and live collections
const staticMatches = document.querySelectorAll(".lesson");
const liveMatches = document.getElementsByClassName("lesson");
querySelectorAll returns a static NodeList; some older collection APIs return live collections that update as the DOM changes. Mutating a tree while iterating a live collection can produce surprising behavior.
for (const item of [...liveMatches]) {
item.classList.remove("lesson");
}
Copying into an array makes this iteration stable.
34. “What happens when one CSS declaration is invalid?”
Browsers parse stylesheets and ignore declarations they do not understand instead of discarding the whole file.
.card {
color: navy;
completely-unknown-property: 12;
padding: 1rem;
}
The unknown declaration is ignored; valid declarations remain. This error-recovery model supports progressive enhancement.
.gallery { display: flex; }
@supports (display: grid) {
.gallery { display: grid; }
}
The CSS Object Model allows inspection and modification of stylesheet rules, though cross-origin stylesheet access is restricted.
const style = getComputedStyle(document.querySelector(".card"));
console.log(style.display, style.paddingInlineStart);
Computed style is the value after cascade and much value processing. It does not necessarily tell you the final used pixel geometry; layout may still resolve percentages, intrinsic sizes and constraints.
35. “How does the cascade settle competing claims?”
When two declarations mention the same property, the browser does not simply choose “the last CSS.” It considers several dimensions.
At a useful high level:
- Relevance: does the rule's condition and selector match?
- Origin and importance: browser, user and author rules; normal or important.
- Cascade layers.
- Specificity.
- Scoping proximity where relevant.
- Source order as the final tie-breaker.
@layer base, components, utilities;
@layer base {
button { color: #1f2937; }
}
@layer components {
.primary-button { color: white; }
}
Layer order can make priority explicit without specificity warfare.
Specificity gently
IDs are generally more specific than classes, and classes more specific than type selectors. Inline styles have their own place. Rather than memorizing a score and trying to win, keep selectors intentionally modest.
/* Difficult to override and tightly coupled to structure. */
#dashboard main .course-list article.card h2.title { color: navy; }
/* A smaller component contract. */
.course-card__title { color: navy; }
:where() contributes zero specificity for its contents, which can be useful for defaults:
:where(.prose) :where(h2, h3) { text-wrap: balance; }
Inheritance is not the cascade
Inheritance supplies a value from a parent when an inheritable property has no winning value on the child. The cascade first decides declarations for the element. Properties such as color and font settings commonly inherit; margins do not.
.course { color: #334155; }
.course button { color: inherit; }
inherit, initial, unset, revert and revert-layer are different tools. Learn them from experiments rather than treating “reset” as one operation.
36. “Does every DOM node create something visible?”
Developers often use the phrase render tree for the representation containing visible renderable content and computed style. The precise internal structures differ by engine, but the model is useful.
Not every DOM node produces a visible box:
.gone { display: none; }
.invisible { visibility: hidden; }
An element with display: none and its descendants do not participate in ordinary layout. visibility: hidden generally preserves layout space while suppressing visibility. Pseudo-elements may generate boxes despite not being ordinary DOM elements.
.required::after { content: " *"; color: crimson; }
Generated visual content should not carry essential meaning without accessible text. Rendering and accessibility are connected, but the visual render representation and accessibility tree are not the same tree.
37. “How are CSS relationships turned into geometry?”
Style answers questions such as “Is this a grid?” and “What is the font size?” Layout uses those answers plus viewport size, content and neighboring boxes to calculate geometry.
Suppose you write:
.shell {
inline-size: min(90%, 70rem);
margin-inline: auto;
}
.content {
display: grid;
grid-template-columns: minmax(14rem, 1fr) 3fr;
gap: 2rem;
}
The browser must determine the containing block, resolve the percentage and maximum, measure intrinsic contributions, distribute grid tracks and position descendants. If the viewport changes, those relationships may need recalculation.
Containing blocks
Percentages and positioned offsets need a reference rectangle called a containing block. The containing block is not always the nearest visually obvious parent.
.card { position: relative; }
.badge {
position: absolute;
inset-block-start: .5rem;
inset-inline-end: .5rem;
}
Here the positioned .card establishes the reference for the absolutely positioned badge. Transforms and other properties can also affect containing-block behavior, including for fixed-position descendants. When an overlay drifts, inspect its containing block before changing random offsets.
Intrinsic sizing
Content has natural size contributions. A long unbreakable string can make a flex or grid child resist shrinking.
.main-column {
min-inline-size: 0;
overflow-wrap: anywhere;
}
min-inline-size: 0 is a common repair for a child whose automatic minimum prevents shrinking. Do not memorize it as magic; understand that you are permitting a smaller used size.
Layout scope
A geometry change can affect descendants, siblings and ancestors. Browsers optimize aggressively and do not necessarily recompute the whole page. CSS containment can explicitly limit certain relationships:
.course-card {
contain: layout paint;
}
Containment changes behavior, not merely performance, so apply it after understanding the component. content-visibility can let a browser skip work for off-screen content, but accessibility, find-in-page and intrinsic-size behavior deserve testing.
38. “Why can reading width make JavaScript unexpectedly expensive?”
Browsers prefer to batch style and layout work. JavaScript can accidentally demand fresh geometry immediately.
for (const card of cards) {
card.style.width = `${container.offsetWidth / 3}px`; // read after writes
}
Changing style invalidates calculations; reading offsetWidth may force the browser to update them before JavaScript continues. Repeating read/write alternation creates layout thrashing.
Batch reads, calculate, then batch writes:
const width = container.getBoundingClientRect().width;
const cardWidth = width / 3;
for (const card of cards) {
card.style.width = `${cardWidth}px`;
}
Better still, ask whether CSS Grid can own the layout without JavaScript:
.cards {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
}
The best optimization often removes the measurement entirely.
39. “What does paint do after layout is known?”
After geometry is known, paint determines how boxes should be drawn: backgrounds, borders, text, images, outlines and shadows, in an order that respects stacking.
Changing background-color commonly needs paint but not layout because geometry is unchanged. Changing width commonly needs layout and then paint. These are useful tendencies, not promises for every engine and circumstance.
.card {
box-shadow: 0 1rem 3rem rgb(15 23 42 / .2);
border-radius: 1rem;
}
Large blurred shadows and complex clipping can be expensive to paint. Do not remove visual quality based on folklore; record a real interaction and inspect paint activity.
Paint order and stacking
z-index participates in stacking contexts. A child with a huge z-index cannot necessarily appear above an element in a higher ancestor stacking context.
.sidebar {
position: relative;
z-index: 2;
}
.main {
transform: translateZ(0); /* creates a stacking context */
z-index: 1;
}
When a menu is trapped, inspect ancestor stacking contexts. Portals or top-layer platform features such as modal dialogs may provide a more appropriate architectural boundary than escalating numbers.
40. “What is compositing, and why can it make animation smoother?”
The browser may paint parts of a page onto separate layers and have a compositor assemble them. This can permit scrolling or animations without repainting all content.
Animations of transform and opacity can often be handled through compositing after initial paint:
.drawer {
transform: translateX(100%);
transition: transform 180ms ease;
}
.drawer[data-open="true"] {
transform: translateX(0);
}
Compare animating left, which changes geometry:
/* Often triggers layout during animation. */
.drawer { left: 100%; }
.drawer.open { left: 0; }
This does not mean “put every element on its own layer.” Layers consume memory and have management costs. will-change is a short, evidence-based hint—not a universal speed switch.
.drawer[data-about-to-open="true"] { will-change: transform; }
Remove the hint after the interaction when appropriate.
41. “How much work fits into one responsive frame?”
A 60 Hz display offers roughly 16.7 milliseconds per frame, but the application does not own all of that time, and screens may refresh at other rates. The useful lesson is not a sacred number. It is that long main-thread tasks delay input and visual updates.
requestAnimationFrame(timestamp => {
// Update animation state shortly before a browser rendering opportunity.
indicator.style.transform = `translateX(${progress * 100}%)`;
});
requestAnimationFrame schedules a callback for an upcoming rendering opportunity. It is appropriate for visual work, but expensive code inside it can still miss frames.
Break long work into chunks or move computation to a worker when suitable:
const worker = new Worker("/search-index-worker.js", { type: "module" });
worker.postMessage({ lessons });
worker.addEventListener("message", event => renderResults(event.data));
A worker cannot directly manipulate the DOM. It communicates through messages, which encourages a clean data boundary but introduces serialization and lifecycle costs.
42. “Can we predict tasks and microtasks before running the code?”
Yes, and prediction is the best way to learn scheduling. Do not begin by memorizing every queue in an engine diagram. Begin with three promises: current synchronous code runs to completion; queued microtasks are drained at the checkpoint after that task; a timer callback becomes eligible as later task work. Then run the example and refine your model.
The event loop selects work from task queues. After a task runs, the browser performs a microtask checkpoint before it gets an opportunity to render. Promise reactions and queueMicrotask use the microtask mechanism.
Predict this output:
console.log("script start");
setTimeout(() => console.log("timer"), 0);
Promise.resolve().then(() => {
console.log("promise one");
queueMicrotask(() => console.log("nested microtask"));
});
console.log("script end");
The synchronous script logs first: script start, then script end. Microtasks run after the current task: promise one, then nested microtask. The timer is eligible in a later task.
Now consider starvation:
function continueForever() {
queueMicrotask(continueForever);
}
continueForever();
Because each microtask queues another, the browser may not regain a normal rendering opportunity. “Promise-based” does not automatically mean “non-blocking.”
async and await
An async function runs synchronously until it reaches an await that yields. Its continuation is scheduled through Promise machinery.
async function example() {
console.log("inside before await");
await null;
console.log("inside after await");
}
console.log("before call");
example();
console.log("after call");
Output begins before call, inside before await, after call; the final message appears in a microtask. Practise predicting ordering before relying on intuition.
43. “How does an event travel from the outer tree to its target and back?”
When a pointer activates a nested element, the event travels conceptually through three phases:
- capturing, from the outer tree toward the target;
- target;
- bubbling, from target back outward.
<section id="course">
<button id="complete"><span>Complete lesson</span></button>
</section>
course.addEventListener("click", log, { capture: true });
complete.addEventListener("click", log);
course.addEventListener("click", log);
function log(event) {
console.log({
current: event.currentTarget.id,
target: event.target.id,
phase: event.eventPhase,
});
}
target is where the event originated; currentTarget is the node whose listener is currently executing. Clicking the means the target may be the span even though the button listener runs.
Event delegation
One ancestor can handle actions for many current and future descendants:
lessonList.addEventListener("click", event => {
const button = event.target.closest("button[data-action]");
if (!button || !lessonList.contains(button)) return;
if (button.dataset.action === "complete") {
completeLesson(button.dataset.lessonId);
}
});
This depends on bubbling and robust target matching. Not every event bubbles in the same way; learn the specific event rather than assuming.
Stopping propagation
stopPropagation() prevents further propagation, while stopImmediatePropagation() also prevents later listeners on the same target. These are powerful and can break analytics, menus or reusable components. Prefer designing clear event contracts before stopping events broadly.
preventDefault() is different: it asks the browser not to perform a cancelable default action.
form.addEventListener("submit", event => {
if (!form.checkValidity()) {
// Native validation normally participates here.
return;
}
event.preventDefault();
saveWithJavaScript(new FormData(form));
});
Preventing default does not stop propagation. Stopping propagation does not automatically prevent default.
44. “What useful behavior does a native control already provide?”
Browsers already know how links, buttons, checkboxes and forms behave. Native controls provide keyboard behavior and semantics.
<button type="button" id="open-course">Open course</button>
is stronger than:
<div class="button" onclick="openCourse()">Open course</div>
The div requires you to recreate focus, keyboard activation, role, disabled behavior and more. Browser knowledge is a reason to use the platform, not replace it.
Passive listeners
For events such as touch or wheel, a browser may need to know whether a listener will cancel scrolling. A passive listener promises not to call preventDefault:
window.addEventListener("touchstart", observeTouch, { passive: true });
Use this when the promise is true. Do not mark a listener passive if the feature genuinely must cancel the default action.
Event listener cleanup
const controller = new AbortController();
window.addEventListener("resize", updateLayout, {
signal: controller.signal,
});
// When the screen is destroyed:
controller.abort();
An abort signal makes grouped cleanup explicit and helps prevent stale behavior and retained objects.
45. “What happens when an event crosses a Shadow DOM boundary?”
Web Components may place implementation markup inside a shadow root. Event propagation across this boundary involves retargeting: outer code may see the host as the target rather than an internal node. An event must also be composed to cross the boundary.
this.dispatchEvent(new CustomEvent("course-selected", {
detail: { courseId: this.courseId },
bubbles: true,
composed: true,
}));
event.composedPath() reveals the propagation path permitted to the caller. This matters when delegation appears to “lose” an internal target.
Design custom events as public contracts. Use meaningful names and stable data. Do not expose internal DOM nodes as your component API.
46. “Which observer matches visibility, size or DOM changes?”
Repeated polling and geometry reads are often unnecessary. Browser observers express intent.
Intersection Observer
const observer = new IntersectionObserver(entries => {
for (const entry of entries) {
if (entry.isIntersecting) {
loadLessonPreview(entry.target);
observer.unobserve(entry.target);
}
}
}, { rootMargin: "200px" });
document.querySelectorAll("[data-lazy-lesson]").forEach(element => {
observer.observe(element);
});
This is useful for visibility-related work. It is asynchronous and does not promise exact pixel-by-pixel notification.
Resize Observer
const resizeObserver = new ResizeObserver(entries => {
for (const entry of entries) {
entry.target.dataset.size = entry.contentRect.width > 600 ? "wide" : "narrow";
}
});
resizeObserver.observe(coursePanel);
Prefer CSS container queries for styling when possible. Resize Observer helps when JavaScript behavior genuinely depends on element size.
Mutation Observer
const mutations = new MutationObserver(records => {
console.log("DOM changed", records);
});
mutations.observe(list, { childList: true, subtree: true });
It reports DOM mutations, not every visual consequence. Disconnect observers when their owner is gone.
47. “How is the accessibility tree different from DOM and pixels?”
Browsers expose semantic information to assistive technologies through an accessibility tree. It draws from HTML semantics, attributes, CSS visibility and ARIA.
<nav aria-label="Course chapters">
<a href="#parsing">HTML parsing</a>
<a href="#layout" aria-current="page">Layout</a>
</nav>
The DOM, visual render structures and accessibility tree overlap but are not identical. A visually present decorative image can be absent from the accessibility tree with empty alternative text. An element hidden with display: none is generally unavailable there too.
Do not add ARIA to compensate for incorrect HTML when a native element already expresses the role. Inspect the Accessibility pane in DevTools to see computed role, name and properties.
48. “Can we diagnose a janky animation together?”
Junior: “The animation is janky. Should I add will-change everywhere?”
Mentor: “First, record it. Is the main thread busy with JavaScript? Are layouts repeating? Are paints large? Is memory pressure high?”
Junior: “The trace shows many layouts after getBoundingClientRect.”
Mentor: “Then layers are not our first problem. Look for alternating style writes and geometry reads. Can CSS own the layout? Can we read once and write together?”
Junior: “After batching, the layout events collapse.”
Mentor: “Good. We changed the cause indicated by evidence. Browser performance work should feel like this: form a model, measure, change one cause and measure again.”
49. “Why is browser storage more like a cupboard than one drawer?”
The word “storage” is dangerously broad. A browser offers mechanisms with different lifetimes, capacities, access models and security properties.
| Mechanism | Useful mental model | Common use |
|---|---|---|
| Cookies | Small notes automatically considered for matching HTTP requests | Sessions and server/browser coordination |
localStorage | Synchronous origin-scoped key/value cupboard | Small non-sensitive preferences |
sessionStorage | Per-tab session cupboard | Temporary navigation state |
| IndexedDB | Asynchronous transactional object database | Larger structured/offline data |
| HTTP cache | Reusable HTTP responses governed by cache semantics | Faster network resource reuse |
| Cache Storage | Script-controlled request/response store | Service-worker offline strategies |
Browsers may evict storage, users may clear it and private browsing changes persistence expectations. Client storage is not your only durable source of truth for valuable server data.
50. “When are localStorage and sessionStorage appropriate?”
The Web Storage API stores strings:
localStorage.setItem("theme", "dark");
const theme = localStorage.getItem("theme") ?? "system";
localStorage.removeItem("theme");
Objects require serialization:
const preference = { density: "comfortable", fontSize: 18 };
localStorage.setItem("reading-preference", JSON.stringify(preference));
const raw = localStorage.getItem("reading-preference");
const parsed = raw ? JSON.parse(raw) : null;
Parsing can fail if old or corrupted data no longer matches expectations. Treat stored data as untrusted input and validate it.
const Preference = z.object({
density: z.enum(["compact", "comfortable"]),
fontSize: z.number().min(14).max(28),
});
function readPreference(): Preference | null {
try {
const raw = localStorage.getItem("reading-preference");
return raw ? Preference.parse(JSON.parse(raw)) : null;
} catch {
return null;
}
}
Web Storage is synchronous. Large repeated reads and writes can block the main thread. It is also readable by JavaScript running in the origin, so an XSS vulnerability can expose its contents. Do not store session secrets there solely because the API is convenient.
sessionStorage is scoped to a browsing context session and is useful for per-tab transient values. It is not a server session.
The storage event can inform other same-origin documents about changes:
window.addEventListener("storage", event => {
if (event.key === "theme") applyTheme(event.newValue);
});
The document that made the change does not use this as its own change callback.
51. “When has my data outgrown simple string storage?”
IndexedDB is asynchronous, transactional and suitable for more structured client data. Its event-based native API is verbose, so teams often use a maintained wrapper while retaining the underlying model.
Conceptually:
const request = indexedDB.open("studydesk", 2);
request.onupgradeneeded = () => {
const db = request.result;
if (!db.objectStoreNames.contains("notes")) {
const store = db.createObjectStore("notes", { keyPath: "id" });
store.createIndex("courseId", "courseId");
}
};
Schema changes occur in version upgrades. Transactions have lifetimes and can become inactive if work leaves their expected flow. Design offline data around synchronization states, not merely local CRUD:
type OfflineNote = {
id: string;
courseId: string;
body: string;
syncStatus: "clean" | "pending" | "conflict";
updatedAt: string;
};
Now ask the difficult questions. What if the same note changes on two devices? Which clock is trusted? Can pending data be deleted safely? Offline support is a distributed-systems feature living inside the browser.
52. “What makes cookies useful and security-sensitive?”
Cookies are unusual because the browser may attach them to matching HTTP requests automatically. That is wonderfully useful for server sessions, but it also means the page does not need to read a cookie for the cookie to carry authority. Cookie attributes are delivery rules, and the server must still authorize every action performed with the session.
A server sets a cookie in a response:
Set-Cookie: session=opaque-random-id; Path=/; Secure; HttpOnly; SameSite=Lax
For later matching requests, the browser may send:
Cookie: session=opaque-random-id
Important attributes:
Securelimits transmission to secure contexts over HTTPS.HttpOnlyprevents ordinary page JavaScript from reading the cookie.SameSiteaffects cross-site sending and helps reduce some CSRF risk.PathandDomaindefine sending scope; omittingDomaincreates a host-only cookie.Max-AgeorExpiresmakes it persistent; otherwise it is a session cookie.
Cookies are not automatically harmless
Because matching cookies are sent automatically, they can participate in CSRF. Because cookies add bytes to requests, oversized cookie collections cost network bandwidth. Because HttpOnly only blocks JavaScript reading—not browser sending—XSS can still perform authenticated actions through the page.
Store an opaque session identifier rather than sensitive profile data when using server-side sessions. Rotate identifiers after login or privilege changes. Expire the browser cookie and invalidate the server session on logout where immediate revocation matters.
First-party and third-party context
Modern browsers increasingly restrict cross-site cookies and partition some state for privacy. Exact policy evolves. Build authentication around documented platform behavior, test in target browsers and avoid depending casually on invisible third-party state.
53. “How does the HTTP cache know whether reuse is correct?”
The HTTP cache stores responses and uses request/response headers to decide whether they are fresh, reusable or need validation.
Cache-Control: public, max-age=3600
ETag: "course-list-v7"
For a fresh cached response, the browser may reuse it without contacting the origin server. When stale, it can validate:
If-None-Match: "course-list-v7"
If unchanged, the server replies:
HTTP/1.1 304 Not Modified
The browser reuses the stored body. 304 is not an error and normally has no new representation body.
no-cache versus no-store
These names confuse nearly everyone initially.
no-cacheallows storage but requires successful validation before reuse.no-storeasks caches not to store the response.
Cache-Control: public, max-age=31536000, immutable
Because the URL changes when content changes, a long lifetime is safe. For HTML entry documents, teams often use shorter freshness or revalidation so users discover new asset URLs.
Vary
Vary: Accept-Encoding, Accept-Language
Vary says which request-header values select distinct cached representations. Using Vary carelessly can fragment cache entries; omitting a necessary dimension can serve the wrong representation.
Memory cache, disk cache and DevTools labels
Browsers may store cache entries in memory or on disk as implementation choices. Focus first on HTTP semantics. In DevTools, “from memory cache,” “from disk cache,” service-worker responses and transferred size help explain where a response came from.
When debugging a first visit, disable cache while DevTools is open and reload. Then re-enable it to test repeat navigation. Otherwise you are testing only one reality.
54. “What exactly forms an origin and why does it matter?”
An origin is primarily the tuple of scheme, host and port.
https://app.example.com:443
Changing any of those normally changes the origin. Paths do not.
https://app.example.com/courses same origin
https://app.example.com/settings same origin
http://app.example.com different scheme
https://api.example.com different host
https://app.example.com:8443 different port
The same-origin policy restricts how documents or scripts from one origin interact with resources from another. Without it, a malicious page could read private content from sites where you are signed in.
Cross-origin resources can sometimes be embedded—images are a familiar example—without granting JavaScript general permission to read their contents. “Can send,” “can display” and “can read from script” are different capabilities.
55. “What permission is a CORS response actually granting?”
CORS does not tell an attacker, “You may use this API.” It tells a browser, “JavaScript running at this allowed origin may read this cross-origin response.” Authentication and authorization remain server responsibilities. This one sentence prevents a great deal of confusion.
Cross-Origin Resource Sharing is an HTTP mechanism through which a server tells a browser that specified origins may read a cross-origin response.
Access-Control-Allow-Origin: https://app.example.com
It is not a setting that makes an API secure. It does not replace authentication or authorization. Non-browser clients are not generally constrained by browser CORS enforcement.
Simple cross-origin requests
Some requests can be sent without a preflight if they meet the safelisted constraints. The browser still checks the response before exposing it to JavaScript.
Preflight
For requests outside those constraints, the browser may first send OPTIONS:
OPTIONS /courses/42 HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: PATCH
Access-Control-Request-Headers: content-type, authorization
The server can answer:
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, PATCH
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 600
Vary: Origin
Only after a successful preflight does the browser send the actual request. Preflight caching is separate from ordinary response caching.
Credentials
When credentials are involved, client and server settings must agree. A wildcard origin is not valid in the same way for credentialed access; return a specifically allowed origin and appropriate credentials header.
await fetch("https://api.example.com/profile", {
credentials: "include",
});
Never reflect any incoming Origin without checking an explicit allowlist. Add Vary: Origin when the response varies by origin so shared caches do not confuse policies.
Reading a CORS error
JavaScript often receives a generic network failure because revealing response details would defeat the boundary. Inspect Console and Network panels. Confirm the request origin, preflight, response headers, redirects and credentials. Do not add mode: "no-cors" expecting readable JSON; it produces an opaque response intended for limited scenarios.
56. “How can CSP restrict what a document loads or executes?”
Content Security Policy travels primarily in an HTTP response header and limits resource sources or capabilities.
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-r4nd0m'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'
The server generates a fresh unpredictable nonce for the response and attaches it to trusted inline script when inline script is truly needed:
<script nonce="r4nd0m">bootstrapApplication()</script>
CSP is defence in depth against classes of injection. It does not excuse unsafe DOM insertion, weak authorization or vulnerable dependencies.
CORS and CSP are different questions
- CORS asks: “May script from this origin read that cross-origin response?”
- CSP asks: “What may this document load or execute?”
script-src. A CSP violation is not repaired with Access-Control-Allow-Origin.
Start a new policy with Content-Security-Policy-Report-Only, collect violations, distinguish genuine requirements from injections/extensions and then enforce a carefully tested policy. Avoid solving every violation by allowing 'unsafe-inline' and broad wildcards; that can remove much of the protection.
Other useful directives include connect-src for fetch/WebSocket targets, img-src, style-src, font-src, form-action and frame-ancestors. Design policy around the application rather than copying a header blindly.
57. “Where does a service worker sit in the request journey?”
A service worker runs separately from the page, has no direct DOM access and can respond to fetches within its scope. It is event-driven and may be stopped when idle.
Registration from a secure context:
if ("serviceWorker" in navigator) {
window.addEventListener("load", async () => {
try {
const registration = await navigator.serviceWorker.register("/sw.js", {
scope: "/",
});
console.log("registered", registration.scope);
} catch (error) {
console.error("registration failed", error);
}
});
}
Install, wait and activate
A new worker installs, usually waits while an older worker controls pages, then activates when safe. This prevents two versions unexpectedly controlling one page at once.
const CACHE = "studydesk-shell-v4";
self.addEventListener("install", event => {
event.waitUntil(
caches.open(CACHE).then(cache => cache.addAll([
"/offline.html",
"/styles/app.css",
])),
);
});
self.addEventListener("activate", event => {
event.waitUntil(
caches.keys().then(names => Promise.all(
names.filter(name => name !== CACHE).map(name => caches.delete(name)),
)),
);
});
event.waitUntil extends the event's lifetime for the supplied Promise. Installation fails if essential precaching rejects, so keep the shell intentional.
Fetch strategies
There is no one correct strategy.
Cache first suits immutable assets:
self.addEventListener("fetch", event => {
if (event.request.destination === "style") {
event.respondWith(
caches.match(event.request).then(cached =>
cached ?? fetch(event.request)
),
);
}
});
Network first may suit changing documents with offline fallback:
async function networkFirst(request) {
try {
const response = await fetch(request);
const cache = await caches.open(CACHE);
cache.put(request, response.clone());
return response;
} catch {
return (await caches.match(request)) ?? caches.match("/offline.html");
}
}
Responses are streams, so caching and returning the same response requires cloning. Cache only appropriate successful responses, control growth and consider user-specific data. A shared offline cache must not reveal one user's private content to another user of the device.
Updating without trapping users
An old page may continue under an old worker while a new worker waits. Provide an update message and reload at a safe point rather than forcing a refresh in the middle of unsaved work. Test first install, repeat visit, update, offline, failed install and storage cleanup in DevTools Application panel.
58. “Why can removed UI still remain in memory?”
Garbage collection follows reachability, not visual presence. A node can disappear from the document while a listener, timer, cache or closure still points to it. The collector sees a reachable object and correctly keeps it. Our job is to find the retaining reference and fix ownership or cleanup—not to demand that the collector guess our intention.
JavaScript garbage collection reclaims objects that are no longer reachable from live roots. You do not manually free ordinary objects, but you control references that keep them reachable.
let currentCourse = { lessons: new Array(100_000).fill("data") };
currentCourse = null;
After the reference is removed and no others remain, the object becomes eligible for collection. Collection timing is controlled by the engine.
Common retention patterns
Forgotten listeners:
function mountScreen() {
const hugeModel = loadHugeModel();
const handler = () => render(hugeModel);
window.addEventListener("resize", handler);
return () => window.removeEventListener("resize", handler);
}
The listener keeps its closure, and the closure keeps hugeModel. Cleanup breaks that chain.
Timers:
const timer = setInterval(refreshProgress, 30_000);
// Later
clearInterval(timer);
Detached DOM: a node removed from the document can remain in memory if JavaScript still references it.
const removedCards = [];
removedCards.push(card);
card.remove(); // still referenced by removedCards
Unbounded caches:
const results = new Map();
function memoizedSearch(query) {
if (!results.has(query)) results.set(query, expensiveSearch(query));
return results.get(query);
}
If query values are unbounded, the cache is unbounded. Add maximum size, expiry or an eviction policy. A WeakMap helps only when weak object-key semantics match the problem; it is not a magical replacement for every cache.
A disciplined leak investigation
- Reproduce a cycle: open and close the suspect screen.
- Establish a baseline heap snapshot.
- Repeat the cycle several times.
- Allow garbage collection when the tool permits.
- Capture another snapshot.
- Inspect objects whose retained count grows.
- Follow retaining paths to the reference owner.
- Fix lifecycle ownership and repeat the same experiment.
59. “Can you mentor me through DevTools one panel at a time?”
DevTools is not merely a place to type console.log. Each panel answers a different class of question.
Elements
Use Elements to inspect the live DOM, matched rules, winning declarations, computed values, box geometry, event listeners and accessibility properties.
Practice:
- Select a heading.
- Find which declaration supplies its color.
- Toggle that declaration.
- Inspect inherited rules.
- Check its computed accessible name and heading level.
Console
Use Console for errors, warnings and small experiments. Select an element and inspect it, but do not paste unknown code into a production page. Preserve logs across navigation when diagnosing redirects or reloads.
console.table(performance.getEntriesByType("resource").map(entry => ({
name: entry.name,
duration: Math.round(entry.duration),
})));
Network
Open Network before reproducing the issue. Inspect:
- URL, method, status and initiator;
- request and response headers;
- cookies and why one was blocked;
- response body;
- transferred versus resource size;
- timing: queueing, DNS, connection, waiting and download;
- whether a service worker or cache supplied it.
Sources
Set breakpoints, step through code, inspect scope and closure values, add conditional breakpoints, pause on exceptions and use event-listener breakpoints. A breakpoint gives state at the cause; a later log often shows only the consequence.
Performance
Record only the interaction you need. Look for long tasks, scripting, style recalculation, layout, paint and screenshots. Zoom into the slow interval. Use the call tree or bottom-up views to connect cost to functions.
Do not conclude “JavaScript is slow” because a yellow region is visible. Determine which function, which input and which repeated relationship created the cost.
Memory
Heap snapshots show retained objects and references. Allocation instrumentation helps locate objects allocated during an interval and still alive later. Detached Elements highlights DOM nodes retained by JavaScript references.
Application
Inspect cookies, Web Storage, IndexedDB, Cache Storage, service workers and manifests. Clear one storage type deliberately rather than pressing “clear everything” before understanding the bug.
60. “Can we debug stale deployed code without guessing?”
Complaint: “After deploying, some users still see an old course screen, and the Save button sometimes calls an old API.”
Work calmly:
- In Network, inspect the HTML document. Was it cached? What does
Cache-Controlsay? - Inspect JavaScript asset URLs. Are they content-hashed? Did HTML reference the new hashes?
- Check Application → Service Workers. Which worker controls the page? Is a new worker waiting?
- Inspect Cache Storage. Does the fetch strategy return cached HTML before checking the network?
- Use “Update on reload” only as a diagnostic, not as the fix.
- Reproduce a clean first visit and an update from the previous release.
- Change the update/cache design, deploy and repeat both journeys.
Notice how several caches can exist at once: HTTP cache, Cache Storage, in-memory application cache and CDN cache. “Clear the cache” is not a precise diagnosis.
61. “What small laboratory will make these ideas real?”
Create a tiny page without a framework. It will teach you more because the platform remains visible.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Browser laboratory</title>
<link rel="stylesheet" href="lab.css">
<script defer src="lab.js"></script>
</head>
<body>
<main>
<h1>Browser laboratory</h1>
<button id="add">Add lesson</button>
<ul id="lessons"></ul>
</main>
</body>
</html>
const list = document.querySelector("#lessons");
const add = document.querySelector("#add");
let number = 0;
add.addEventListener("click", () => {
number += 1;
const item = document.createElement("li");
item.innerHTML = `<button data-number="${number}">Lesson ${number}</button>`;
list.append(item);
});
list.addEventListener("click", event => {
const button = event.target.closest("button[data-number]");
if (!button) return;
localStorage.setItem("last-lesson", button.dataset.number);
button.classList.toggle("complete");
});
Exercises:
- Predict parser and script timing, then inspect Network initiators.
- Inspect the DOM before and after adding a lesson.
- Explain event delegation using
targetandcurrentTarget. - Record a click in Performance.
- Watch
localStoragein Application. - Add a stylesheet rule and inspect cascade and computed values.
- Add a deliberate retained listener, observe growth, then clean it up.
- Add a service worker only after the normal network version is understood.
62. “How can I tell whether I truly understand the browser?”
Try answering aloud, gently and without rushing.
- Why is the DOM not identical to HTML source?
- How can parsing begin before download finishes?
- When does a classic script block parsing?
- How do
async,deferand modules differ? - What does the CSSOM contribute?
- Why are cascade and inheritance different?
- What is a containing block?
- What is intrinsic sizing?
- What is calculated during layout?
- What happens during paint?
- What does compositing assemble?
- Why can
transformanimation be cheaper thanleft? - Why can too many layers be harmful?
- How can a geometry read force layout?
- What runs before a timer: queued Promise reactions or the later timer task?
- Why can microtasks starve rendering?
- How do
targetandcurrentTargetdiffer? - How do
preventDefaultandstopPropagationdiffer? - Why does event delegation work?
- Why prefer native controls?
- How do cookies differ from Web Storage?
- Why is
no-cachenot the same asno-store? - What does
304mean? - What forms an origin?
- What question does CORS answer?
- What question does CSP answer?
- What can a service worker intercept?
- Why might a new worker wait?
- What keeps a detached DOM node alive?
- Which DevTools panel would provide the next piece of evidence?
63. “How should I practise this over four patient weeks?”
Week 1: documents and styles
- Day 1: trace navigation from URL to HTML bytes.
- Day 2: compare View Source with Elements.
- Day 3: test classic, async, defer and module scripts.
- Day 4: practise cascade, layers, specificity and inheritance.
- Day 5: inspect computed styles and box geometry.
- Day 6: build Flexbox/Grid layouts and identify containing blocks.
- Day 7: teach the pipeline to an imaginary junior in five minutes.
Week 2: pixels and interaction
- Day 8: record style, layout, paint and composite activity.
- Day 9: create and repair layout thrashing.
- Day 10: compare transform and geometry animations.
- Day 11: predict task and microtask ordering.
- Day 12: practise capture, target and bubble phases.
- Day 13: build event delegation and cleanup.
- Day 14: inspect the accessibility tree and keyboard behavior.
Week 3: browser data and boundaries
- Day 15: compare Web Storage and IndexedDB.
- Day 16: inspect secure cookie attributes.
- Day 17: test HTTP freshness and validation.
- Day 18: map same-origin examples.
- Day 19: reproduce a preflight and explain every header.
- Day 20: introduce a CSP in report-only mode.
- Day 21: explain why CORS, CSP and authentication are distinct.
Week 4: offline, memory and diagnosis
- Day 22: register a minimal service worker.
- Day 23: compare cache-first and network-first.
- Day 24: test worker update and offline failure.
- Day 25: create and diagnose a listener leak.
- Day 26: investigate a network problem without changing code.
- Day 27: investigate a rendering problem from a trace.
- Day 28: complete the laboratory and write your own browser checklist.
64. “What should remain after I close these notes?”
Junior: “There is so much here. Do I need to memorize every internal detail?”
Mentor: “No. Browser internals evolve, and engines differ. Keep the durable model: bytes become documents and styles; style becomes geometry and drawing; scheduled work competes for time; events travel through structures; storage mechanisms have distinct contracts; origins enforce boundaries; DevTools provides evidence.”
Junior: “What should I do when the page feels wrong?”
Mentor: “Name the symptom. Ask which browser system could produce it. Predict what evidence that cause would leave. Open the matching panel. Change one thing and observe again.”
Junior: “And when I get confused?”
Mentor: “Make the example smaller. Remove the framework for ten minutes. Use one element, one rule, one listener or one request. Confusion usually becomes teachable when the experiment becomes small.”
That is the real goal of this masterclass. You do not need to carry an entire browser engine in your head. You need a dependable map, patient experiments and the confidence to ask the browser for evidence.
