35.0 What this chapter gives you#
- You will be able to draw the line between frontend and backend, say exactly
what runs on the user’s device and what runs on a server you control, and
state the security rule that follows from it.
- You will be able to say what HTML, CSS and JavaScript each are, why they are
three separate things, and why only one of them is a programming language.
- You will be able to explain the CSS cascade, specificity with the real
calculation, the box model, flexbox and grid, and stop guessing at layout.
- You will be able to describe the JavaScript event loop with the call stack,
the task queue and the microtask queue, and predict the output order of a
piece of asynchronous code by hand.
- You will be able to trace an HTML file all the way to lit pixels: parse, DOM,
CSSOM, render tree, layout, paint, composite, and say what reflow costs.
- You will be able to say what a frontend framework actually solves, how a
virtual DOM and reconciliation work, and when you do not need one at all.
- You will be able to write a minimal web server in two languages, explain
routing and middleware, and compare Node, Python, Java, Go, Ruby, PHP
and .NET on honest grounds rather than fashion.
- You will be able to design a small relational schema, write SQL with joins
and grouping, explain how a B-tree index turns a scan into a lookup, read an
EXPLAIN plan, and define ACID one letter at a time.
- You will be able to store a password correctly, choose between sessions and
tokens, describe the OAuth 2.0 authorization code flow step by step, and
separate authentication from authorization.
- You will be able to replace the “180-day roadmap” with an order of learning
that matches how long these things really take.
35.1 Frontend, backend and the line between them#
PLAIN35.1.1 in simple words#
- A web application lives in two places at once.
- Part of it runs on the user’s own device, inside the browser. That part is
called the frontend.
- Part of it runs on a computer you own or rent, far away. That part is called
the backend.
- The frontend is what the user sees and touches: text, pictures, buttons,
boxes to type in.
- The backend is what the user never sees: the stored data, the rules about
who may do what, the money, the emails, the search.
- The two talk to each other over the network, using the request and reply
pattern from the networking chapters.
- The device asks, the server answers. The device asks again, the server
answers again. That is the whole shape of it.
- This is called client-server. The client is the asking side. The server
is the answering side.
- Here is the single most important rule in this whole chapter. Everything on
the user’s device can be changed by the user.
- The user can edit the page, change the numbers you sent, delete your checks,
and send you anything at all.
- So the frontend can be polite, helpful and fast. It can never be trusted.
- Every rule that matters must be checked again on the server, where the user
cannot reach it.
PLAIN35.1.2 a picture in your head#
- Think of a bank branch.
- The counter, the forms, the pen on a chain and the queue barrier are the
frontend. They are in the public part of the building.
- The vault, the ledgers and the staff-only room are the backend.
- The form asks you to write your account number in the right shape. That is
helpful. It stops honest people making honest mistakes.
- But nobody thinks the form is security. A person can write anything on a
form. They can bring their own form. They can shout their request instead.
- So the clerk checks the request again, against the real ledger, behind the
glass, before any money moves.
- That second check is the backend check. The form was the frontend check.
- Both are useful. Only one of them is protection.
Where this comparison breaks:
- In a bank you can see the customer. On the web you cannot. The “customer”
may be a program sending ten thousand requests a second from another
country.
- In a bank the forms live in the branch. On the web, you hand the customer a
complete copy of your entire form-processing machine and let them run it at
home. They can take it apart first.
- And a bank branch serves one person at a time at each counter. A server
handles thousands of half-finished conversations at once.
PLAIN35.1.3 a worked example#
- A shop page shows a jacket at 4,000 rupees. The page has a discount box.
- The frontend code checks the discount code, works out 10 percent off, and
shows 3,600.
- The user then presses Buy. The browser sends a message to the server.
- A careless developer sends the price in that message:
{ "item": "jacket-01", "price": 3600, "code": "SAVE10" }
- The user opens the browser’s developer tools, changes one number, and sends
this instead:
{ "item": "jacket-01", "price": 1, "code": "SAVE10" }
- If the server saves that order, the shop has just sold a jacket for one
rupee. Nothing was hacked. The user simply typed a different number.
- The correct message contains no price at all:
{ "item": "jacket-01", "code": "SAVE10", "qty": 1 }
- The server looks up the real price of
jacket-01 in its own database, checks
whether SAVE10 is real, unexpired and allowed for this user, and computes
the total itself.
- The frontend showed 3,600 so the user was not surprised. The server decided
3,600 because it is the only side that can decide.
- This exact mistake has cost real companies real money many times.
PLAIN35.1.4 what is really happening inside#
- The browser is a program on the user’s machine. Chapter 18 called such a
program a process: code plus memory, run by the operating system.
- When you visit a site, the browser makes a network connection to a server,
as in Chapters 28 and 32, and asks for a document.
- The server sends back text. Usually HTML, which describes the content.
- The browser reads that text and builds an internal model of the page in its
own memory. Then it draws that model on the screen.
- The HTML normally refers to more files: stylesheets, scripts, images, fonts.
The browser fetches each of those with more requests.
- Once a script is running, it can make further requests on its own, without
loading a new page. That is how modern pages update in place.
- Every one of those requests is a separate arrival at the server, and the
server has no memory of the previous one unless you build that memory.
- That last point is what “HTTP is stateless” means, and it is why sessions
and tokens exist at all, which is section 35.12.
- The split exists for three reasons. Speed, because a round trip to a server
costs tens or hundreds of milliseconds and a local reaction costs none.
- Control, because your data and your rules must live where users cannot edit
them.
- Scale, because one server can serve many clients only if the clients do
their own drawing.
TECHNICAL35.1.5 the engineer’s version#
- The client-server model is an architectural style, not a protocol. HTTP is
one instance of it. It was covered from the wire upward in Chapters 23 to 33.
- HTTP/1.1 is defined by RFC 9110 to RFC 9112 (June 2022), which replaced the
older RFC 7230 series. HTTP/2 is RFC 9113. HTTP/3 over QUIC is RFC 9114.
- HTTP is stateless by design: each request carries everything needed to
process it. State is reconstructed from cookies, headers or tokens.
- The trust boundary sits at the network edge of your server. Everything on the
far side of it is untrusted input, including your own JavaScript.
- The formal rule is the end-to-end argument from Saltzer, Reed and Clark
(1981, 1984): a check is only meaningful at the point that owns the outcome.
| Concern |
Frontend can |
Server must |
| Field format |
show hints early |
validate again |
| Price, totals |
display only |
compute, authoritative |
| Permissions |
hide buttons |
enforce on every call |
| Rate limits |
debounce clicks |
count and reject |
| Secrets |
never hold any |
hold all of them |
- Client-side validation is a usability feature with a measurable benefit: it
removes a network round trip, typically 30 to 300 ms depending on distance.
- Anything shipped to the browser is public. API keys in a bundle, “hidden”
admin routes and commented-out code are all readable. Minification is not
obfuscation and obfuscation is not security.
- Tools that make this concrete: browser DevTools Network and Sources panels,
curl -v, Burp Suite and mitmproxy for intercepting and editing requests.
- Server-side rendering and client-side rendering shift where HTML is
produced, but they do not move the trust boundary at all. That is fixed.
WORDS35.1.6 remember these#
- Frontend — the part running on the user’s device — client-side code
executing in the browser’s JavaScript engine and rendering engine.
- Backend — the part running on your machine — server-side processes you
control, holding data, secrets and authorization logic.
- Client-server — one side asks, the other answers — an architectural style
where a requesting process contacts a listening service process.
- Stateless — the server forgets between requests — no server-held context is
implied by the protocol; state must be carried or looked up.
- Trust boundary — the line where you stop believing input — the point where
data crosses from an environment you do not control into one you do.
35.2 HTML: the structure of a page#
PLAIN35.2.1 in simple words#
- HTML stands for HyperText Markup Language.
- A markup language is a way of adding labels to text to say what each part
of the text is.
- It does not say what the text should look like. It says what the text means.
- You mark up a piece of text by wrapping it in a tag. A tag is a word inside
angle brackets.
<p>Hello</p> says: the word Hello is a paragraph.
- Most tags come in pairs. An opening tag, then the content, then a closing tag
with a slash.
- The opening tag, the content and the closing tag together are called an
element.
- Tags can carry extra information called attributes, written as
name="value" inside the opening tag.
- Elements go inside other elements. That is nesting, and it makes the page
a tree, like folders inside folders.
- HTML is not a programming language. It cannot add two numbers, make a
decision, or repeat something. It only describes structure.
- That is not an insult. Describing structure well is the whole job, and most
accessibility and search problems come from doing it badly.
PLAIN35.2.2 a picture in your head#
- Think of a printed book being prepared before typesetting.
- An editor goes through the manuscript with a pencil and writes in the margin:
this line is a chapter title, this block is a quotation, this is a footnote,
this is a list.
- The editor does not choose the font. The editor does not choose the size.
Those come later, from the designer.
- The editor’s marks are the markup. They record what each piece of text is.
- A different designer can take the same marked-up manuscript and produce a
paperback, a large-print edition, or an audiobook script.
- Because the marks say “chapter title”, the audiobook narrator knows to pause
and change tone. The word “chapter title” carries meaning that “big bold
text” does not.
Where this comparison breaks:
- The editor’s marks are read by a human who can use judgement. HTML is read by
a machine that will do exactly and only what the tags say.
- A book is finished once. An HTML page is re-laid-out every time the window
size changes, on screens the author never saw.
- And HTML is more forgiving than any editor would be. Browsers repair broken
markup silently, which hides mistakes rather than reporting them.
PLAIN35.2.3 a worked example#
- Here is a complete, valid page. Everything in it earns its place.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport"
content="width=device-width, initial-scale=1">
<title>Bookshop - Contact</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<header>
<h1>Ambleside Bookshop</h1>
<nav aria-label="Main">
<a href="/">Home</a>
<a href="/contact" aria-current="page">Contact</a>
</nav>
</header>
<main>
<h2>Send us a message</h2>
<form action="/contact" method="post">
<label for="name">Your name</label>
<input id="name" name="name" type="text" required>
<label for="email">Email</label>
<input id="email" name="email" type="email" required>
<label for="msg">Message</label>
<textarea id="msg" name="msg" rows="5"></textarea>
<button type="submit">Send</button>
</form>
</main>
<footer>
<p>Open 9am to 6pm, closed Sunday.</p>
</footer>
</body>
</html>
- Line by line, the parts that people get wrong.
<!DOCTYPE html> is not a tag. It is a switch. Without it, browsers fall
into “quirks mode” and copy 1990s bugs on purpose.
lang="en" tells screen readers which language to pronounce, and tells
search engines who this page is for.
<meta charset="utf-8"> says the bytes are UTF-8, from Chapter 7. Leave it
out and names with accents may turn into rubbish.
- The viewport meta tag tells a phone not to pretend it is a 980-pixel desktop.
Without it, your responsive CSS does nothing on mobile.
<label for="name"> is joined to <input id="name"> by the matching value.
Now clicking the label focuses the box, and a screen reader announces the
right words.
name="name" is what the server receives. id is for the page. They are
different jobs that beginners merge.
type="email" makes phone keyboards show the at sign, and lets the browser
check the shape for free.
PLAIN35.2.4 what is really happening inside#
- The browser receives the file as a stream of bytes, not as a page.
- It decodes those bytes into characters using the declared encoding, then
splits the characters into tokens: start tag, end tag, text, comment.
- It builds a tree from those tokens. Each element becomes a node with a
parent and a list of children. That tree is the DOM, which is section 35.5.
- Semantic elements are tags whose names describe the role of the content:
header, nav, main, article, section, aside, footer, h1 to
h6, figure, time.
- They matter for two audiences you cannot see.
- Screen readers build a list of headings and landmarks from them, so a blind
user can jump straight to the main content instead of hearing the menu on
every page.
- Search engines use them to work out what the page is about and which part is
the real content.
- A
<div> says nothing. It is a plain box. A page built only from divs looks
identical and is much harder to use without sight.
- Forms are the one place plain HTML can send data. A form with
method="post"
packages the named fields and sends them to the server on its own, with no
JavaScript at all.
- The browser gives you validation, keyboard handling, autofill and mobile
keyboards for free, but only if you use real form elements.
TECHNICAL35.2.5 the engineer’s version#
- HTML was created by Tim Berners-Lee at CERN; the first public description
circulated in 1991. HTML 2.0 was standardized as RFC 1866 in November 1995.
- HTML 4.01 became a W3C Recommendation in December 1999. XHTML 1.0 followed
in 2000 and pushed strict XML syntax, which the web rejected.
- WHATWG formed in 2004 in response. HTML5 became a W3C Recommendation on
28 October 2014. Since a 2019 agreement, the WHATWG HTML Living Standard is
the single normative specification. There are no numbered versions now.
- The parsing algorithm is fully specified, including error recovery. Two
conforming browsers must build the same DOM from the same broken input.
This is a standard, not a convention, and it is unusual in being so.
- Elements are void (
img, br, input, meta), raw text (script,
style), or normal. Void elements have no closing tag and no children.
- Attribute values should be quoted. Unquoted values are legal in HTML5 but
break on spaces. Quoting is a convention with a good reason.
- ARIA (Accessible Rich Internet Applications, WAI-ARIA 1.2, W3C
Recommendation June 2023) adds roles and states for widgets HTML lacks.
- The first rule of ARIA, stated in the specification’s own authoring practices,
is not to use ARIA when a native element exists. A real
<button> beats
<div role="button" tabindex="0"> on every measure.
| Input type |
Added in |
Mobile keyboard |
Free validation |
| text |
HTML 2.0 |
standard |
none |
| email |
HTML5 |
at sign |
shape check |
| number |
HTML5 |
digits |
min, max, step |
| date |
HTML5 |
date picker |
range |
| tel |
HTML5 |
phone pad |
none |
- Tools: the W3C Nu HTML Checker (
validator.w3.org/nu), axe DevTools, and
the accessibility tree view in Chrome and Firefox DevTools.
WORDS35.2.6 remember these#
- Markup — labels added to text — a syntax that annotates content with
structural and semantic roles rather than presentation.
- Element — a tag plus its content — a node in the document tree with a tag
name, attribute set and child node list.
- Attribute — extra detail on a tag — a name-value pair on an element, exposed
in the DOM as content attributes and IDL properties.
- Semantic HTML — tags that say what content is — markup chosen so the
accessibility tree and machine readers infer correct roles.
- Void element — a tag with no closing half — an element defined as having no
end tag and no permitted children, such as
img or input.
- Doctype — the switch that turns on modern rules — a legacy string that
selects no-quirks mode in the HTML parser.
35.3 CSS: presentation, and the cascade that confuses everyone#
PLAIN35.3.1 in simple words#
- CSS stands for Cascading Style Sheets.
- HTML says what things are. CSS says what they look like.
- Keeping those two apart is the idea called separation of content and
presentation.
- The benefit is real. Change one stylesheet and every page changes. Give the
same HTML a different stylesheet and you get a different design.
- A CSS rule has two halves. A selector picking which elements to affect,
and a block of declarations saying what to do to them.
- A declaration is a property and a value:
color: black;.
- Many rules can hit the same element. When two rules disagree, the browser has
to pick one. The picking procedure is called the cascade.
- The cascade is where almost everyone gets stuck, so we will do it properly
and give you the real calculation.
- Every element is drawn as a rectangle. Understanding that rectangle, the box
model, removes most layout confusion.
- Two layout systems, flexbox and grid, do nearly all modern layout. Learn
those two well and you can stop guessing.
PLAIN35.3.2 a picture in your head#
- Imagine a school with a stack of rulebooks about uniform.
- The national rule says shoes must be black. The school rule says shoes must
be brown. The class teacher’s note says shoes must be blue.
- A pupil arrives. Which colour wins?
- You need an agreed order for settling arguments, or the pupil cannot dress.
- The school’s order is: a note about this named pupil beats a note about
this class, which beats a note about all pupils.
- If two notes are equally specific, the one written most recently wins.
- And a rule marked “no exceptions, headmaster” beats everything, which is why
the headmaster should use it almost never.
- That is the cascade. Origin, then importance, then specificity, then order.
Where this comparison breaks:
- Rules about pupils apply to whole pupils. CSS settles each property
separately. Colour can come from one rule and size from another, in the same
element, at the same time.
- And some CSS properties are inherited by children automatically, like text
colour, while others are not, like border. No school rule works that way.
PLAIN35.3.3 a worked example#
- Here is a real box, measured.
.card {
width: 300px;
padding: 20px;
border: 2px solid black;
margin: 16px;
box-sizing: content-box;
}
- With
content-box, which is the original CSS behaviour, width means the
content area only.
- Total horizontal space the box occupies on screen:
| Part |
Left |
Right |
Total |
| content |
- |
- |
300 |
| padding |
20 |
20 |
40 |
| border |
2 |
2 |
4 |
| margin |
16 |
16 |
32 |
- Drawn width is 300 + 40 + 4 = 344 px. Space consumed in the flow, including
margin, is 376 px.
- That surprise is why almost every project now writes:
*, *::before, *::after { box-sizing: border-box; }
- With
border-box, width: 300px means the border edge is 300 px, so the
content area shrinks to 300 - 40 - 4 = 256 px. What you type is what you
measure.
- Now specificity, calculated properly. Specificity is a triple: (A, B, C).
| Selector |
A ids |
B classes |
C elements |
p |
0 |
0 |
1 |
.note |
0 |
1 |
0 |
p.note |
0 |
1 |
1 |
#main p.note |
1 |
1 |
1 |
:is(#a, p) |
1 |
0 |
0 |
- Compare left to right. (1,0,0) beats (0,9,9). A single id beats nine classes.
The columns are not digits of one number, they are separate ranks.
- Attribute selectors and pseudo-classes count in column B. Pseudo-elements
such as
::before count in column C. The universal selector * counts zero.
:where(...) always contributes zero, which makes it the correct tool for
library defaults you want authors to override easily.
PLAIN35.3.4 what is really happening inside#
- The browser collects every declaration that could apply to one element and
one property, then sorts them by these steps in order.
- Step one, origin and importance. Order from weakest to strongest: browser
defaults, then user settings, then your stylesheet, then your
!important
rules, then user !important, then browser !important.
- Note the flip. Among
!important rules the order reverses, so a user’s
accessibility override can beat an author’s. That is deliberate.
- Step two, since 2023, cascade layers declared with
@layer. Later layers
beat earlier ones, and this is decided before specificity.
- Step three, specificity, using the (A, B, C) triple above.
- Step four, source order. If everything else ties, the declaration that
appears later in the stylesheet wins.
- Inline
style="..." is not a selector. It sits above all normal author rules
and is only beaten by !important.
- If no rule sets a property, the element may still get a value by
inheritance from its parent. Text properties inherit. Box properties do
not.
- If nothing inherits either, the property takes its initial value from the
specification.
- The honest version: people say “the most specific rule wins”. That is only
step three of six, and it is not even the first tiebreaker. The full order is
origin, importance, layer, specificity, then source order.
TECHNICAL35.3.5 the engineer’s version#
- CSS was proposed by Hakon Wium Lie on 10 October 1994 at CERN, with Bert Bos.
CSS Level 1 became a W3C Recommendation on 17 December 1996. CSS 2.1 became a
Recommendation in June 2011.
- There is no “CSS3” specification. Since Level 2 the language is split into
independently versioned modules: Selectors Level 4, Grid Level 2, and so on.
Saying “CSS3” is a marketing habit, not a standard.
- Display types:
block, inline, inline-block, flex, grid, none, and
the two-value syntax display: inline flex. none removes the element from
layout entirely; visibility: hidden keeps its space.
- Flexbox is one-dimensional: items are distributed along a main axis. It
reached W3C Candidate Recommendation status in 2012 and shipped broadly
from 2013.
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.toolbar .spacer { flex: 1 1 auto; }
- Grid is two-dimensional: rows and columns declared together. It shipped in
Chrome 57 and Firefox 52 in March 2017, which is the date modern CSS layout
really begins.
.page {
display: grid;
grid-template-columns: 240px minmax(0, 1fr);
grid-template-rows: auto 1fr auto;
gap: 16px;
min-height: 100vh;
}
- The
fr unit means one share of the leftover space. minmax(0, 1fr) is the
standard fix for grid children that refuse to shrink below their content.
- Positioning:
static (default), relative (offset from its own place, keeps
its space), absolute (removed from flow, positioned against the nearest
positioned ancestor), fixed (against the viewport), sticky (relative
until a scroll threshold, then fixed).
| Unit |
Relative to |
Typical use |
| px |
device-independent pixel |
borders, hairlines |
| em |
own font-size |
spacing inside a component |
| rem |
root font-size (16 px) |
type scale, layout |
| % |
parent’s same dimension |
fluid widths |
| vh / vw |
1% of viewport |
full-screen sections |
| ch |
width of “0” glyph |
text measure limits |
- One CSS px is defined as 1/96 inch at a nominal viewing distance, not one
hardware pixel. On a 3x phone screen, one CSS px covers nine device pixels.
- Media queries adapt to the environment:
@media (min-width: 48rem) { .page { --cols: 2; } }
@media (prefers-reduced-motion: reduce) {
* { animation-duration: 0.01ms !important; }
}
- Mobile-first means writing the small-screen rules first and using
min-width queries to add complexity. It is a convention, and a good one.
- Custom properties, usually called CSS variables, are real cascading values,
live at runtime, and are readable by JavaScript. They shipped in 2016.
:root { --gap: 12px; --ink: #111; }
.card { padding: var(--gap); color: var(--ink); }
- Container queries let a component respond to its own container rather than
the viewport. They reached cross-browser support in February 2023.
.sidebar { container-type: inline-size; }
@container (min-width: 30rem) {
.card { display: grid; grid-template-columns: 1fr 2fr; }
}
- Native CSS nesting reached cross-browser support during 2023 and 2024 and
removes one of the main historical reasons to use Sass.
:has(), the long-requested parent selector, shipped across browsers by
December 2023. .card:has(img) styles a card because of what it contains.
- Tools: DevTools Styles pane shows the winning declaration and strikes out
losers; the Layout pane overlays grid and flex lines;
@supports queries
let you ship progressive enhancement safely.
WORDS35.3.6 remember these#
- Cascade — the argument-settling procedure — the ordered algorithm of origin,
importance, layer, specificity and source order.
- Specificity — how targeted a selector is — the (id, class, type) triple
compared left to right, never summed.
- Box model — every element is a rectangle — content, padding, border and
margin boxes, with
box-sizing choosing what width measures.
- Flexbox — line things up along one direction — CSS Flexible Box Layout,
one-dimensional distribution along a main and cross axis.
- Grid — a real table-free grid — CSS Grid Layout, two-dimensional placement
with explicit and implicit tracks.
- rem — a size tied to the page’s base text — the computed font-size of the
root element, 16 px unless changed.
- Container query — react to my own box, not the window —
@container rules
evaluated against a queried ancestor’s size.
35.4 JavaScript: the language and its event loop#
PLAIN35.4.1 in simple words#
- JavaScript is the programming language that runs inside web browsers.
- Unlike HTML and CSS, it really is a programming language. It can decide,
repeat, calculate, store and react.
- It was written in 1995 at a company called Netscape, by one person, Brendan
Eich, in about ten days.
- Ten days is not enough time to design a language carefully. Many of its odd
corners come straight from that deadline.
- It was never removed, because the web could not break old pages. So the
world’s most used language is one that was rushed and then frozen.
- Since 1997 the language has had a written standard, and it now gets a new
edition every June, so it improves without breaking what came before.
- The single hardest idea in JavaScript is asynchrony: starting something
slow, not waiting for it, and dealing with the answer when it arrives.
- Asynchrony exists because JavaScript in a browser has only one worker. If
that worker waits, the whole page freezes: no scrolling, no clicking.
- So instead of waiting, it writes a note saying what to do when the answer
comes, and gets on with other work.
- The machine that manages those notes is called the event loop, and it is
the thing you must understand to write correct JavaScript.
PLAIN35.4.2 a picture in your head#
- Picture a single cook in a small kitchen with a strict rule: finish the task
in your hands before starting the next one.
- An order arrives: boil rice, twenty minutes. The cook does not stand and
watch the pot. That would block every other order.
- The cook puts the rice on, sets a timer, writes “when the timer rings, drain
the rice” on a slip, and puts the slip in a tray.
- Then the cook takes the next order and starts chopping.
- When the cook finishes a task and has nothing in hand, they look in the tray,
take the oldest slip, and do it.
- That is the event loop. One cook, no waiting, a tray of pending slips.
- There is a second, smaller tray, right next to the cook, checked before the
big one and emptied completely each time.
- Anything urgent, like “the sauce reduced, add salt now”, goes in the small
tray and is done before the next big order.
- The small tray is the microtask queue. The big tray is the task queue.
Where this comparison breaks:
- A real cook can glance at the pot while chopping. JavaScript truly cannot.
If a function loops for three seconds, nothing else happens for three
seconds, including the timer that was due one second ago.
- And a timer set for 100 ms does not mean the slip is done at 100 ms. It
means the slip is put in the tray at 100 ms. If the cook is busy, it waits.
PLAIN35.4.3 a worked example#
- Predict the output of this. Most people get it wrong the first time.
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
queueMicrotask(() => console.log("4"));
console.log("5");
- The answer is 1, 5, 3, 4, 2.
- Here is why, step by step.
- The whole script is itself one task. It runs to the end before anything else
is considered. So “1” prints, then “5” prints.
- Along the way,
setTimeout handed a callback to the timer system with a
delay of zero. When zero milliseconds pass, that callback is placed in the
task queue.
Promise.resolve().then(...) placed its callback in the microtask queue
immediately, because the promise was already settled.
queueMicrotask placed its callback in the microtask queue too, after the
promise one.
- The script ends. The call stack is now empty.
- Before touching the task queue, the engine drains the microtask queue
completely. So “3” prints, then “4”.
- Only now does it take one item from the task queue. “2” prints.
| Moment |
Stack |
Microtasks |
Tasks |
| script running |
script |
3, 4 |
(timer pending) |
| script ends |
empty |
3, 4 |
2 |
| drain micro |
3 then 4 |
empty |
2 |
| next task |
2 |
empty |
empty |
- The rule to memorize: microtasks run to exhaustion after every task, before
the next task, and before the browser gets a chance to paint.
- That gives you a real bug. A microtask that queues another microtask forever
starves the page. The browser never repaints. Nothing on screen moves.
PLAIN35.4.4 what is really happening inside#
- The call stack is a pile of function calls. Calling a function pushes a
frame on. Returning pops it off. When the pile is empty, the engine is idle.
- Slow things are not done by JavaScript at all. Network requests, timers, disk
access and user input are handled by the browser’s own code, in other
threads, written in C++ or Rust.
- When one of those finishes, the browser does not interrupt your code. It
places your callback in a queue.
- The event loop is a simple repeating check: is the stack empty? If yes, drain
all microtasks, then take one task, run it, then possibly render.
- Promises,
async/await, queueMicrotask and MutationObserver use the
microtask queue. Timers, network events, clicks and messages use task queues.
await is not magic. It splits your function in two at that point. The rest
of the function becomes a microtask attached to the awaited promise.
- Rendering happens between tasks, not during one. That is why a long loop
freezes the screen even if you change styles inside it.
- Three ways to write the same wait, in historical order.
// 1. Callbacks (1995 onward)
getUser(id, (err, user) => {
if (err) return done(err);
getOrders(user, (err, orders) => {
if (err) return done(err);
done(null, orders);
});
});
// 2. Promises (standardized in ES2015)
getUser(id)
.then(user => getOrders(user))
.then(orders => done(null, orders))
.catch(done);
// 3. async / await (ES2017)
try {
const user = await getUser(id);
const orders = await getOrders(user);
return orders;
} catch (err) {
handle(err);
}
- All three do the same thing. The third is easiest to read and easiest to get
right, because errors travel through ordinary
try and catch.
- The trap in version three: two sequential
await calls that do not depend on
each other waste time. Use Promise.all to run them together.
const [user, rates] = await Promise.all([
getUser(id),
getRates(),
]);
TECHNICAL35.4.5 the engineer’s version#
- Brendan Eich wrote the first prototype in May 1995 at Netscape
Communications. It was called Mocha, renamed LiveScript in September 1995,
then JavaScript in December 1995 as part of a marketing deal with Sun.
- The name is the source of endless confusion. JavaScript and Java are
unrelated languages.
- Netscape submitted it to Ecma International; ECMA-262 1st edition was
published in June 1997. The standard language is called ECMAScript;
JavaScript is a trademarked implementation name.
- ES5 (December 2009) added strict mode and JSON. ES2015, also called ES6,
was the large modernization:
let, const, arrow functions, classes,
modules, promises, template literals, destructuring.
- Since 2015 there is one edition each June. ECMAScript 2025 was the 16th
edition (June 2025) and ECMAScript 2026 the 17th (June 2026).
- Types:
number (IEEE 754 double, so integers are exact only to 2^53 - 1),
bigint, string (UTF-16 code units), boolean, undefined, symbol,
null, and object.
- Coercion is the automatic conversion between types, and it is the source of
the famous oddities.
| Expression |
Result |
Reason |
0.1 + 0.2 |
0.30000000000000004 |
binary floats |
[] + {} |
“[object Object]” |
both to string |
"5" - 2 |
3 |
minus forces number |
"5" + 2 |
“52” |
plus prefers string |
NaN === NaN |
false |
IEEE 754 rule |
typeof null |
“object” |
1995 bug, frozen |
0.1 + 0.2 is not a JavaScript flaw. It is IEEE 754 binary floating point,
from Chapter 7, and it happens in Python, Java and C too.
- Use
=== not ==. == applies the coercion table and produces surprises
such as "" == 0 being true.
- Closures: a function keeps a live reference to the variables of the
scope where it was defined, even after that scope returns.
function counter() {
let n = 0;
return () => ++n;
}
const next = counter();
next(); // 1
next(); // 2
- JavaScript uses prototypal inheritance. Every object has a hidden link
to another object; property lookup walks that chain.
class syntax, added
in ES2015, is sugar over prototypes, not a separate system.
- Modules: ES modules (
import, export) are the standard, defined in
ES2015 and shipped in browsers from 2017. CommonJS (require,
module.exports) is Node’s older system and is still everywhere.
- Engines: V8 (Chrome, Node, Edge, Deno), SpiderMonkey (Firefox),
JavaScriptCore (Safari, Bun). All are JIT compilers with multiple tiers,
described in Chapter 16.
- Error handling rules that matter: never swallow an error silently; attach a
.catch or try/catch to every promise chain; an unhandled rejection
terminates a Node process by default since Node 15 (2020).
- Tools:
node --prof, Chrome DevTools Performance panel for long tasks,
performance.now() for sub-millisecond timing.
WORDS35.4.6 remember these#
- Event loop — the one-worker queue system — the algorithm that drains
microtasks and dispatches one task per turn on a single thread.
- Call stack — the pile of calls in progress — the LIFO structure of execution
contexts; overflow raises RangeError.
- Microtask — an urgent note done before anything else — a job queued to the
microtask queue, drained fully after each task.
- Promise — a receipt for a future value — an object in pending, fulfilled or
rejected state, with
then scheduling microtasks.
- Closure — a function that remembers where it was born — a function object
plus a reference to its defining lexical environment.
- Prototype — the object I fall back to — the internal
[[Prototype]] link
traversed during property lookup.
- ECMAScript — the written standard — ECMA-262, published annually each June
since 2015; JavaScript is an implementation of it.
35.5 The DOM and how a browser turns HTML into pixels#
PLAIN35.5.1 in simple words#
- HTML arrives as text. Text cannot be clicked, moved or measured.
- So the browser converts the text into a live tree of objects held in memory.
That tree is the DOM, the Document Object Model.
- Every tag becomes a node. Every node knows its parent and its children.
- JavaScript does not edit your HTML file. It edits this tree. Change the tree
and the screen follows.
- Your CSS is turned into a second structure with the same idea, called the
CSSOM.
- The browser combines the two into a render tree: only the things that
will actually be drawn, each with its final style.
- Then it works out where every box goes, which is layout. Then it fills in
the colours, which is paint. Then it stacks the layers, which is
composite.
- If you change something that affects size or position, the browser must redo
layout. That is called reflow and it is the expensive one.
- If you change only a colour, it can skip layout and just repaint. Cheaper.
- If you change only a transform or opacity, it can often skip both and just
re-stack existing layers. Cheapest of all.
- Knowing which of the three you triggered is most of frontend performance.
PLAIN35.5.2 a picture in your head#
- Think of a newspaper being made in a print room, before computers.
- The reporters’ typed pages are the HTML. Just text with notes on it.
- The editor’s style guide, saying headlines are 36 point and captions are
italic, is the CSS.
- The layout artist takes both and builds a paste-up board: which story goes in
which column, how many lines it runs to, where the picture sits. That is
layout.
- Then the ink goes on: black type, grey photographs. That is paint.
- Then the transparent overlay sheets are stacked in order. That is composite.
- Now the editor says one caption should be bold. The word gets wider. The line
rewraps. The column gets longer. The story below moves down. The whole board
must be redone.
- That is a reflow, and you can see why it costs.
- But if the editor only says “print this headline in a darker grey”, the
shapes do not move. Only the ink changes. That is a repaint.
Where this comparison breaks:
- A newspaper is laid out once. A browser may redo layout sixty times a second
while you drag a window.
- And in a print room the artist works on the whole board. Browsers do partial
invalidation: they mark a subtree as dirty and try to redo only that. The
cost still spreads upward more often than you would like.
PLAIN35.5.3 a worked example#
- Here is the classic layout-thrashing bug, in ten lines.
const items = document.querySelectorAll(".row");
// BAD: read, write, read, write, ...
for (const el of items) {
el.style.height = el.offsetHeight + 10 + "px";
}
offsetHeight is a read of a geometric value. style.height is a
write that invalidates geometry.
- The browser batches writes and flushes them lazily. But a read forces it to
flush immediately, so the answer is correct. That forced flush is a reflow.
- With 200 rows, that loop causes 200 forced synchronous layouts. On a mid
phone that can be 200 ms of frozen page.
- The fix is to separate all reads from all writes.
const items = document.querySelectorAll(".row");
// GOOD: read everything, then write everything
const heights = [...items].map(el => el.offsetHeight);
items.forEach((el, i) => {
el.style.height = heights[i] + 10 + "px";
});
- Now there is one layout pass, not 200. Same output, roughly one hundredth of
the cost.
- Properties that force a synchronous layout when read include
offsetTop,
offsetHeight, scrollTop, clientWidth, getBoundingClientRect() and
getComputedStyle().
- Now events. Suppose a list has 500 rows and each needs a click handler.
// 500 listeners, 500 objects, slow to attach
rows.forEach(r => r.addEventListener("click", onClick));
// 1 listener, using bubbling. This is delegation.
list.addEventListener("click", (e) => {
const row = e.target.closest(".row");
if (row) onClick(row.dataset.id);
});
- This works because a click on a row also fires on its parents, in order,
outward. That travelling is bubbling, and using it deliberately is
event delegation.
- Delegation also handles rows added later, which the first version cannot.
PLAIN35.5.4 what is really happening inside#
- The full sequence, in order, for a first page load.
bytes -> characters -> tokens -> nodes -> DOM tree
CSS bytes ------------------------------> CSSOM tree
DOM + CSSOM -> render tree -> layout -> paint -> composite
- Parsing is incremental. The browser starts building the DOM before the whole
file has arrived, which is why a slow page still shows its header early.
- A plain
<script> tag in the middle of the body stops parsing dead. The
browser must fetch and run it before continuing, because the script might
write into the document.
defer says: fetch it now in parallel, run it after parsing finishes, in
order. async says: fetch now, run the moment it arrives, order not
guaranteed. Use defer for application code.
- CSS is render-blocking. The browser will not paint until it has the
stylesheets, because painting with the wrong styles then correcting would
flash. That flash has a name, FOUC.
- Layout computes a box for every rendered node: x, y, width, height, in a
single tree walk, honouring the box model and the layout mode of each parent.
- Paint turns boxes into a list of drawing commands: fill this rectangle,
draw this glyph run, clip here.
- Composite runs those command lists into layers, often on the GPU from Chapter 22, and stacks them. Scrolling and CSS transforms usually only touch this
stage, which is why they are smooth.
- An event has three phases: capture, travelling down from the document to the
target; target; then bubble, travelling back up. Most code only ever uses
the bubble phase.
stopPropagation() halts travel. preventDefault() cancels the browser’s
built-in reaction, such as following a link. They are different and
beginners confuse them constantly.
TECHNICAL35.5.5 the engineer’s version#
- DOM Level 1 became a W3C Recommendation in October 1998. The DOM is now a
WHATWG Living Standard, and it is a language-independent API, not a
JavaScript feature.
- The critical rendering path is the minimum set of resources needed for
first paint: the HTML, the render-blocking CSS, and any parser-blocking
JavaScript.
- Rendering engines in production: Blink (Chrome, Edge, Opera, forked from
WebKit on 3 April 2013), WebKit (Safari), Gecko (Firefox).
- Style resolution is per element per property; matching selectors right to
left is the standard implementation trick that makes descendant selectors
cheap enough.
- Reflow cost is superlinear in tree depth and node count. Modern engines do
partial layout, but a change to a flow-root ancestor still invalidates its
whole subtree.
| Change |
Triggers |
Relative cost |
width, top, font-size |
layout, paint, composite |
highest |
background-color, color |
paint, composite |
medium |
transform, opacity |
composite only |
lowest |
| adding a class with all three |
full pipeline |
highest |
- Animate
transform and opacity. Animating left and width forces layout
on every frame and will not hold 60 fps on a mid-range phone.
- A frame budget at 60 Hz is 16.7 ms; at 120 Hz it is 8.3 ms. Chapters 21 and
22 traced that budget from key press to photons.
requestAnimationFrame schedules a callback just before the next paint,
which is the correct hook for animation. setTimeout(fn, 16) is not.
content-visibility: auto and contain let you tell the engine that a
subtree cannot affect the outside, allowing it to skip work entirely.
- The DOM API is genuinely slow relative to plain objects, mostly because each
call crosses from the JavaScript engine into the C++ engine. Batch changes
with
DocumentFragment or by building a string once.
- Tools: Performance panel with the Frames and Main tracks, the “Layout Shift
Regions” and “Paint flashing” overlays in Rendering, and the
PerformanceObserver API for long-animation-frame entries.
WORDS35.5.6 remember these#
- DOM — a live tree of the page in memory — the Document Object Model, a
language-neutral object representation defined by WHATWG.
- CSSOM — the same idea for styles — the parsed stylesheet object model used
during style resolution.
- Render tree — only the things that get drawn — the tree of styled boxes,
excluding
display: none and including generated content.
- Reflow — redo the positions — layout invalidation and recomputation of box
geometry; also called relayout.
- Repaint — redo the colours — rasterization of the display list without a
geometry pass.
- Composite — stack the finished layers — GPU-assisted assembly of rasterized
layers into the final frame.
- Bubbling — the event travels outward — phase 3 of DOM event dispatch, from
target up to the root.
- Delegation — one listener for many children — attaching a handler to a
common ancestor and using
event.target.
35.6 The browser as a platform#
PLAIN35.6.1 in simple words#
- A browser is not a document viewer any more. It is an operating system for
web pages.
- It gives your page a set of built-in services, the same way an operating
system gives a program files, memory and the network.
- It can store data on the user’s disk in three different ways, for different
sizes and purposes.
- It can fetch more data from a server at any time, without loading a new page.
- It can run your code on extra threads, so slow work does not freeze the
screen.
- It can keep a small program running in the background that answers requests
even with no network, which is how a web app works offline.
- It can hold a two-way conversation with a server, where the server speaks
first, instead of only answering questions.
- It can connect two users’ browsers directly to each other, for calls and
video.
- It can draw arbitrary graphics and run code on the graphics chip.
- And it does all this inside a sandbox: a locked room where the page can
touch only what it has been given, and must ask permission for the rest.
PLAIN35.6.2 a picture in your head#
- Think of a serviced office you rent by the hour.
- You get a desk, a locked drawer, a phone and a window. That is the platform:
storage, network, screen.
- You cannot walk into another tenant’s office. The doors are locked and the
locks are not yours to change. That is the same-origin rule.
- You cannot open the fire door to the roof without asking the building manager
first, and the manager asks you in front of the tenant. That is the
permission prompt for camera, microphone and location.
- There are two small back rooms you can hire someone to work in, out of sight
of your desk. One does heavy calculation. One sits by the post slot and
answers deliveries even when you have gone home.
- Those are web workers and service workers.
Where this comparison breaks:
- An office tenant can carry things down the corridor by hand. A page cannot
reach another origin’s storage at all, by any route, including through the
user’s own copy of the browser.
- And a real building manager can be persuaded. The sandbox is enforced by the
browser’s own code, and cannot be talked around by the page.
PLAIN35.6.3 a worked example#
- A small task: fetch a list, cache it, and survive going offline.
// 1. Ask the server for JSON
const res = await fetch("/api/books", {
headers: { "Accept": "application/json" },
});
if (!res.ok) throw new Error("HTTP " + res.status);
const books = await res.json();
// 2. Keep a copy the user can read while offline
localStorage.setItem("books", JSON.stringify(books));
- Note
res.ok. fetch does not reject on a 404 or a 500. It only rejects on
a network failure. Forgetting this is one of the most common bugs.
- Now the storage choices, with real limits.
| Store |
Holds |
Typical limit |
Sent to server |
| Cookie |
small strings |
4 KB each |
yes, every request |
| localStorage |
strings |
about 5 MB |
no |
| sessionStorage |
strings, per tab |
about 5 MB |
no |
| IndexedDB |
structured objects |
large, quota-based |
no |
| Cache Storage |
whole responses |
quota-based |
no |
- localStorage is synchronous. A 2 MB read blocks the single thread, and the
page freezes for that time. Do not use it for anything large.
- IndexedDB is asynchronous and can hold hundreds of megabytes, subject to a
quota that is usually a share of free disk space.
- Cookies travel with every matching request automatically. That is why they
suit session identifiers and suit nothing else.
- A minimal service worker turns that fetch into an offline-capable one.
// sw.js
self.addEventListener("fetch", (event) => {
event.respondWith(
caches.match(event.request).then(hit =>
hit || fetch(event.request)
)
);
});
- Now the browser asks the service worker before it asks the network. If the
worker has a copy, the page loads with the aeroplane mode switch on.
PLAIN35.6.4 what is really happening inside#
- The same-origin policy is the core rule. An origin is the triple of
scheme, host and port.
https://a.com and https://a.com:8443 are
different origins, and so are http and https versions of one host.
- Code from one origin cannot read the DOM, cookies or storage of another. It
can still cause requests to another origin, which is why CSRF exists.
- CORS is the controlled exception. The server adds a response header saying
which origins may read its answers. The browser enforces it. The rule lives
with the server, not the page.
- A web worker is a separate thread with its own memory and no access to
the DOM. You talk to it by sending messages that are copied, not shared.
- Use one for parsing a large file, image processing, or cryptography, so the
main thread stays free to draw at 60 frames per second.
- A service worker is a special worker that sits between the page and the
network, like a programmable proxy on the user’s own machine.
- It survives page closes, wakes for push messages, and must be served over
HTTPS, because a hostile one would be permanent.
- WebSockets solve a real problem. Plain HTTP is ask-and-answer, so a
server with news cannot speak first.
- The old workaround was polling: ask every few seconds, usually to be told
nothing happened. Wasteful in requests, battery and latency.
- A WebSocket starts life as an ordinary HTTP request with an Upgrade header,
then the same TCP connection becomes a two-way message pipe that stays open.
- WebRTC goes further and connects two browsers directly, so audio and
video do not pass through your server. Your server only helps them find
each other.
TECHNICAL35.6.5 the engineer’s version#
- Same-origin policy dates to Netscape Navigator 2.02 in 1995. CORS is defined
in the WHATWG Fetch Standard. Preflight uses an
OPTIONS request when the
method or headers are non-simple.
fetch() shipped from 2015 and replaced XMLHttpRequest, which came from
Microsoft Outlook Web Access around 1999 and reached IE 5 in 1999. The term
AJAX was coined by Jesse James Garrett in February 2005.
- WebSocket is RFC 6455, December 2011. Handshake is HTTP
Upgrade: websocket; frames then carry a 2 to 14 byte header, versus roughly 500 to
800 bytes of HTTP headers per polled request.
| Technique |
Direction |
Overhead |
Best for |
| Polling |
client asks |
high, constant |
rare updates |
| Long polling |
client waits |
medium |
legacy fallback |
| SSE |
server to client |
low, text only |
feeds, progress |
| WebSocket |
both ways |
lowest per message |
chat, games |
| WebRTC data |
peer to peer |
lowest latency |
calls, screen share |
- Server-Sent Events use the
text/event-stream MIME type, reconnect
automatically, and are part of the HTML Living Standard. They are one-way
and much simpler to operate than WebSockets.
- WebRTC was open-sourced by Google in May 2011 and became a W3C
Recommendation on 26 January 2021. It needs STUN and usually TURN servers to
traverse NAT, which Chapter 24 described. TURN relays cost real bandwidth.
- Graphics: Canvas 2D is immediate-mode drawing. WebGL 1.0 shipped March 2011
and is based on OpenGL ES 2.0; WebGL 2.0 (2017) on OpenGL ES 3.0. WebGPU
shipped in Chrome 113 in May 2023, in Safari 26 in 2025, and gives compute
shaders and a modern binding model.
- Storage quota is not fixed by specification. Chrome allows an origin up to
about 60 percent of total disk in practice; Safari applies stricter
eviction. This is an implementation detail and changes between versions.
- Permissions requiring an explicit user prompt: camera, microphone,
geolocation, notifications, clipboard read, MIDI, Bluetooth, USB, and
persistent storage. Most also require a secure context (HTTPS or localhost).
- Powerful features are gated three ways: secure context, transient user
activation (a real click within a few seconds), and Permissions Policy
headers set by the site.
- Tools: DevTools Application panel shows every store, service worker state,
and quota usage;
chrome://webrtc-internals shows live peer connections.
WORDS35.6.6 remember these#
- Sandbox — a locked room for the page — an isolated execution environment
with mediated access to host resources.
- Origin — the identity of a site — the tuple of scheme, host and port used
for all isolation decisions.
- CORS — the server’s note saying who may read this — Cross-Origin Resource
Sharing headers defined by the Fetch Standard.
- Service worker — a proxy of your own on the user’s machine — an
event-driven worker intercepting fetches, enabling offline and push.
- Web worker — a second thread with no screen access — a dedicated worker with
an isolated global scope and message-passing only.
- WebSocket — a phone line instead of letters — RFC 6455 full-duplex framed
messaging over a single upgraded TCP connection.
- IndexedDB — a real database in the browser — a transactional object store
with indexes and an asynchronous API.
35.7 Frontend frameworks, explained rather than listed#
PLAIN35.7.1 in simple words#
- Here is the problem a framework exists to solve.
- Your page shows information. That information changes. The page must change
to match, everywhere it appears, every time.
- Doing that by hand means writing instructions: find this element, change its
text, add this row, remove that one, toggle this class.
- That style is called imperative: you say the steps.
- With ten pieces of information it is fine. With two hundred, in a page where
any change can affect any other, it becomes impossible to keep straight.
- A framework lets you write declarative code instead: you describe what the
page should look like for the current data, and the framework works out the
steps to get there.
- The second big idea is the component: one self-contained piece of the page
with its own markup, styling and behaviour, that you can use many times.
- A page becomes a tree of components, the way a house is a set of rooms rather
than one giant space.
- That is all a framework is: a way to say “screen equals function of data”,
plus a way to cut a screen into reusable pieces.
PLAIN35.7.2 a picture in your head#
- Imagine a departures board at a station, made of physical letter tiles.
- The imperative way: a person with a ladder, told “change platform 4 from 12
to 9, and change the 14:05 to Delayed, and remove the 13:50 row”.
- Every instruction must be right, in the right order, or the board lies.
- The declarative way: you hand over a printed sheet showing what the board
should say now. A machine compares it with what the board says and changes
only the tiles that differ.
- You never think about tiles. You think about the correct board.
- The comparing step is called reconciliation, and the sheet you hand over
is the idea behind a virtual DOM.
Where this comparison breaks:
- The station board has fixed rows. A web page has a shifting tree, so the
comparison needs hints about identity. That is what the
key attribute is
for, and getting keys wrong causes real bugs.
- And comparison is not free. Some frameworks skip it entirely by working out
at build time exactly which tiles a given change touches.
PLAIN35.7.3 a worked example#
- The same counter, three ways.
// Imperative: plain DOM
let n = 0;
btn.addEventListener("click", () => {
n = n + 1;
out.textContent = "Count: " + n;
});
// Declarative: React
function Counter() {
const [n, setN] = useState(0);
return (
<button onClick={() => setN(n + 1)}>
Count: {n}
</button>
);
}
<!-- Declarative: Svelte 5 -->
<script>
let n = $state(0);
</script>
<button onclick={() => n++}>Count: {n}</button>
- In the first, you own the update. In the other two, you own the description
and the tool owns the update.
- With one counter the first is shorter. With a form of forty fields that feed
a summary, a chart and a save button, the first becomes a maze.
- What React does when
setN runs: it calls Counter() again, gets a new
description, compares it with the old one, sees only the text differs, and
changes that one text node. It does not rebuild the button.
- What Svelte does: at build time the compiler already worked out that
n
appears in exactly one text position, so it emits code that assigns to that
text node directly. There is no comparison at runtime.
PLAIN35.7.4 what is really happening inside#
- State is the data that can change. Props are the data a parent hands
to a child. A component re-renders when its own state or its props change.
- A virtual DOM is a plain tree of ordinary JavaScript objects describing
the wanted output. Cheap to build, cheap to throw away.
- Reconciliation walks the old tree and the new tree together. Same type at the
same position means update in place. Different type means destroy and
rebuild. Lists are matched by
key.
- Using an array index as a key is the classic bug: delete the first row and
every key shifts, so the framework updates the wrong rows and typed input
jumps to the wrong place.
- State management is the question of where shared data lives. Local state
inside a component is easy. Data that four distant components need is not.
- The escalation ladder is: local state, then lift state to a common parent,
then a context or provider, then a dedicated store, then a server-state
library that caches responses.
- Hydration happens when HTML is rendered on the server for speed, then the
same components run in the browser to attach event handlers to the existing
markup. The page looks ready before it is interactive.
- The honest version: hydration is a workaround, not a triumph. It ships the
data twice, once as HTML and once as JSON, and it is why some server-rendered
pages feel dead for a second. Islands, partial hydration and React Server
Components are all attempts to send less.
TECHNICAL35.7.5 the engineer’s version#
- React was created at Facebook by Jordan Walke and open-sourced in May 2013 at
JSConf US. Facebook renamed itself Meta in October 2021. React 19 is current,
with 19.2 published in 2026.
- JSX is a syntax extension compiled to function calls; it is not HTML and not
part of the language.
class becomes className because class is
reserved.
- React uses one-way data flow: data goes down through props, changes go up
through callbacks. Hooks arrived in React 16.8 (February 2019) and replaced
class components as the normal style.
- The core hooks are
useState, useEffect, useMemo, useCallback,
useRef and useContext. Rules: only at the top level, only in components
or other hooks, because hook identity is positional.
useEffect is for synchronizing with something outside React, not for
deriving data. Overuse of effects is the most common source of React bugs.
| Framework |
First release |
Approach |
Trade-off |
| React |
May 2013, Meta |
virtual DOM, hooks |
huge ecosystem, more rerenders |
| Angular |
Sept 2016 (v2) |
full framework, DI |
batteries included, steep start |
| Vue |
Feb 2014, Evan You |
reactive proxies |
gentle curve, smaller job market |
| Svelte |
Nov 2016, R. Harris |
compiler, runes |
least runtime code, smaller ecosystem |
| Solid |
2018, R. Carniato |
fine-grained signals |
very fast, small community |
- AngularJS (October 2010, Misko Hevery at Google) and Angular 2 onward
(14 September 2016) are different frameworks with a shared name. Angular
uses TypeScript, RxJS and dependency injection, and now ships version 22.
- Vue 3 (September 2020) uses ES Proxy-based reactivity and a composition API.
Svelte 5 (19 October 2024) introduced runes, replacing its label-based
reactivity. Solid uses signals with no component rerenders at all.
- Signals, which Solid popularized, have now been adopted by Angular, Vue,
Svelte and Preact, and a TC39 signals proposal exists. Experts disagree on
whether React should adopt them; React’s team argues its compiler achieves
the same result without changing the model.
- Meta-frameworks add routing, server rendering and data loading: Next.js for
React (version 16 in 2026), Nuxt for Vue, SvelteKit, Angular with its own
SSR, Remix and Astro.
- When you do not need a framework: content sites, marketing pages, blogs,
documentation, forms with a handful of fields, and anything where the server
can render the HTML and a hundred lines of plain JavaScript will do.
- A framework costs 40 to 150 KB of JavaScript before your own code, plus a
build step, plus upgrade work forever. Spend that only when interactivity
genuinely earns it.
WORDS35.7.6 remember these#
- Declarative — describe the result — express the target state and delegate the
transition to the runtime.
- Component — one reusable piece of screen — an encapsulated unit of template,
state and behaviour with a props interface.
- Virtual DOM — a cheap paper copy of the page — an in-memory tree diffed
against the previous render to compute minimal DOM mutations.
- Reconciliation — comparing old and new — the diffing algorithm, keyed by
element type and
key attribute.
- Hydration — waking up server HTML — attaching client event listeners and
state to markup already present in the document.
- Signal — a value that knows who reads it — a fine-grained reactive primitive
that updates only its dependents.
35.8 Build tooling: why frontend code needs a build step#
PLAIN35.8.1 in simple words#
- Twenty years ago you wrote a
.js file and put it on a server. Done.
- Today most projects run a program over the code first, and ship the result.
That program is the build step.
- There are four honest reasons for it.
- One, your code is split across hundreds of small files, and loading hundreds
of files is slow. So they are joined into a few. That is bundling.
- Two, you write modern syntax, and some browsers do not understand all of it.
So it is rewritten into older syntax. That is transpiling.
- Three, some files are not JavaScript at all: JSX, TypeScript, Svelte
components, Sass. Something must convert them.
- Four, shipping fewer bytes makes pages faster, so names are shortened,
spaces removed and unused code deleted.
- Nothing here is magic. The build step is a translator and a packer.
PLAIN35.8.2 a picture in your head#
- Think of moving house.
- Your belongings are spread through many rooms in many small containers. That
is your source code: many files, each doing one thing, easy for you to find.
- You would not carry two hundred small boxes to the van one at a time. You
pack them into a few large cartons. That is bundling.
- Anything broken or never used gets thrown out rather than moved. That is tree
shaking.
- Air is squeezed out and clothes are vacuum-packed. That is minification.
- And you keep an inventory sheet saying which carton each item is in, so that
when something is missing at the far end you can trace it back. That is the
source map.
Where this comparison breaks:
- When you move house, you unpack. The browser never unpacks: it runs the
packed form. The source map only exists for you, in the debugger.
- And you can add a room after moving in. Bundles are fixed at build time,
which is why code splitting has to be planned, not discovered.
PLAIN35.8.3 a worked example#
- A tiny
package.json, which is the project’s identity card.
{
"name": "bookshop-web",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"test": "vitest run"
},
"dependencies": { "react": "^19.2.0" },
"devDependencies": { "vite": "^8.2.0" }
}
dependencies are needed to run. devDependencies are needed only to build
and test, and never reach the browser.
^19.2.0 means “19.2.0 or any later 19.x”. That range is why two people can
install the same project on the same day and get different code.
- The lock file,
package-lock.json or pnpm-lock.yaml, records the exact
version and content hash of every package that was actually installed.
- Commit the lock file. Without it your builds are not reproducible, and a
working build today can break tomorrow with no change from you.
- Tree shaking, shown concretely:
// utils.js exports 40 functions
import { formatDate } from "./utils.js";
- Because ES modules declare their imports statically, the bundler can prove
the other 39 are unreachable and drop them. CommonJS
require is dynamic, so
it usually cannot.
- Code splitting turns one big file into several loaded on demand:
const Chart = lazy(() => import("./Chart.jsx"));
- Now the chart’s code downloads the first time a user opens the chart, not on
every page load.
PLAIN35.8.4 what is really happening inside#
- A bundler starts at an entry file, follows every import, and builds a graph
of modules. Then it writes that graph out as one or more files.
- A transpiler parses source into a syntax tree, rewrites nodes, and prints new
source. Babel does this for JavaScript syntax.
- Transpiling handles syntax. Missing functions need a polyfill, which
is real code implementing the missing feature. These are different jobs.
- TypeScript adds a type layer on top of JavaScript. It checks your code, then
erases every type annotation. Nothing is checked at runtime.
- That erasure is the key point people miss. TypeScript protects you while you
write. It gives you nothing at all against bad data arriving from a network
request. You still have to validate.
- Minification renames local variables to single letters, removes whitespace
and comments, and simplifies expressions. Typical saving is 30 to 60 percent
before compression.
- A source map is a separate file mapping every position in the output back to
a line and column in your source, so the debugger can lie helpfully.
node_modules is where installed packages live, honestly: a very large
folder containing your dependencies and all of their dependencies, often tens
of thousands of files and hundreds of megabytes.
- It is not a virus and it is not waste by design. It is the visible cost of a
culture of very small packages. It should never be committed to git, and it
is always rebuildable from
package.json plus the lock file.
TECHNICAL35.8.5 the engineer’s version#
- npm launched in January 2010, written by Isaac Schlueter, and is now
operated by GitHub. The registry holds well over three million packages.
- Alternatives: Yarn (2016), pnpm (2017, which hard-links a global store and
cuts disk use sharply), and Bun’s built-in installer (2022).
- Semantic versioning is MAJOR.MINOR.PATCH.
^1.2.3 allows minor and patch
updates, ~1.2.3 allows patch only, and an exact pin allows neither. This
is a convention, and packages break it regularly.
| Tool |
First release |
Written in |
Role |
| webpack |
2012, T. Koppers |
JavaScript |
bundler, loaders |
| Rollup |
2015, R. Harris |
JavaScript |
library bundler |
| Babel |
2014, S. McKenzie |
JavaScript |
transpiler |
| esbuild |
2020, E. Wallace |
Go |
very fast bundler |
| Vite |
2020, Evan You |
JS plus Rust |
dev server, build |
| SWC |
2019 |
Rust |
transpiler, minifier |
- Vite’s development server does not bundle. It serves native ES modules and
transforms each file on request, so start-up is near instant regardless of
project size. For production it bundles with Rollup.
- Speed differences are large and measurable: esbuild and SWC are commonly 10
to 100 times faster than Babel and webpack on the same input, because they
are compiled languages with parallelism rather than single-threaded
JavaScript.
- TypeScript was released by Microsoft on 1 October 2012, designed by Anders
Hejlsberg. TypeScript 7, a native port of the compiler to Go, shipped in
2026 with roughly a tenfold type-checking speed-up.
- Supply chain risk is real. Compromised packages have shipped malware to
millions of installs. Mitigations: lock files,
npm audit, npm ci in CI,
--ignore-scripts, and provenance attestations.
- Bundle budgets that hold up in practice: under 100 KB of compressed
JavaScript for a content site, under 300 KB for a rich application. Measure
compressed transfer size, not the raw file size.
- Tools:
npx vite build --sourcemap, rollup-plugin-visualizer, Chrome
DevTools Coverage panel to find unused bytes, npm ls <pkg> to find who
pulled a package in.
WORDS35.8.6 remember these#
- Bundler — the packer — a tool that resolves a module graph and emits fewer,
larger output files.
- Transpile — rewrite new syntax as old — source-to-source compilation between
language versions.
- Polyfill — supply a missing function — runtime code implementing a standard
API absent in the host.
- Tree shaking — drop code nobody uses — dead-code elimination over a static ES
module graph.
- Source map — the inventory sheet for debugging — a JSON mapping from
generated positions back to original source.
- Lock file — the exact recipe of what was installed — a resolved dependency
tree with versions and integrity hashes.
- Code splitting — download it when needed — emitting separate chunks loaded by
dynamic
import().
35.9 The backend: what a server program actually does#
PLAIN35.9.1 in simple words#
- A server program is an ordinary program with one unusual habit: it never
finishes.
- It opens a port, which is a numbered door on the machine, and waits.
- When a request arrives, it reads it, decides what it means, does the work,
writes an answer, and goes back to waiting.
- Deciding what a request means is routing: matching the method and path,
such as
GET /books/12, to the piece of code that handles it.
- The piece of code that handles it is called a handler, or in some
traditions a controller.
- Work that must happen on many requests, such as logging, checking who the
user is, or parsing the body, is factored out into middleware: small
steps a request passes through on the way in.
- There are two shapes of backend. One builds finished HTML pages and sends
them. The other sends only data, usually JSON, and lets the frontend draw.
- The first is templating. The second is API-only. Both are fine, and mixing
them is normal.
PLAIN35.9.2 a picture in your head#
- Think of a hospital reception.
- The door is the port. Anyone may walk in.
- Before anyone sees a doctor they pass a fixed sequence: security check, then
registration, then the file is pulled, then vitals are taken. Every patient,
every time, in that order. That is middleware.
- Reception then reads the complaint and sends the patient to the right
department. That is routing.
- The specialist who actually treats them is the handler.
- The discharge letter handed back is the response.
- If registration fails, the patient never reaches the specialist, and is sent
away at that step. That is middleware returning early, which is exactly how
authentication is enforced.
Where this comparison breaks:
- A hospital treats patients one at a time per doctor. A server holds thousands
of half-finished requests at once, interleaved.
- And a hospital remembers you between visits. HTTP does not. Every arrival is
a stranger until a cookie or token proves otherwise.
PLAIN35.9.3 a worked example#
- The same tiny API, in Node with Express and in Python with FastAPI.
// server.js - Node with Express 5
import express from "express";
const app = express();
app.use(express.json()); // middleware
app.use((req, res, next) => { // logging
console.log(req.method, req.url);
next();
});
app.get("/books/:id", async (req, res) => {
const book = await db.findBook(req.params.id);
if (!book) return res.status(404).json({ error: "no" });
res.json(book);
});
app.listen(3000);
# main.py - Python with FastAPI
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/books/{book_id}")
async def read_book(book_id: int):
book = await db.find_book(book_id)
if book is None:
raise HTTPException(status_code=404)
return book
- The shapes are the same: declare a path, name the variable part, do the work,
return data or an error code.
- FastAPI reads the
book_id: int annotation and rejects /books/abc with a
422 before your function runs. Express does not; you must check yourself.
- The request lifecycle, drawn:
TCP accept
-> parse HTTP request line and headers
-> middleware 1 (parse body)
-> middleware 2 (log)
-> middleware 3 (authenticate) -- may stop here
-> router match: GET /books/:id
-> handler runs, queries database
-> serialize response, set status and headers
-> write bytes, maybe keep connection alive
PLAIN35.9.4 what is really happening inside#
- Under the routing there is a socket, from Chapter 28.
listen puts the
process into the accepting state; the kernel queues incoming connections.
- Two families of design handle concurrency.
- Thread or process per request: each request gets its own worker, which is
allowed to block. Simple to reason about. Costs memory per worker, roughly
0.5 to 8 MB of stack, so thousands of workers is expensive.
- Event loop: one thread handles many connections by never blocking, using
the same idea as the browser event loop in section 35.4. Cheap per
connection, but one slow synchronous function stalls everyone.
- Node.js uses the second. So do FastAPI with async handlers, Go’s runtime with
very cheap goroutines, and modern Java with virtual threads.
- This is why a Node server is excellent for many connections doing little work
each, such as an API or a chat gateway, and a poor choice for heavy
number-crunching in the request path.
- The way out for Node is not to fight it: move heavy work to a worker thread,
a separate service, or a background job queue.
- In every design, the database is usually the real bottleneck, not the
language. A 40 ms query dwarfs a 0.4 ms difference in framework overhead.
TECHNICAL35.9.5 the engineer’s version#
- Node.js was released by Ryan Dahl in 2009 and presented at JSConf EU in
November 2009. It pairs V8 with libuv for asynchronous input and output.
Node 24 “Krypton” is the active LTS as of 2026; Node 26 became the current
line in May 2026.
- Node is single-threaded for JavaScript but not single-threaded overall:
libuv keeps a thread pool, four by default, for file system work, DNS and
some crypto.
- Express began in 2010 by TJ Holowaychuk; Express 5.0 was released in 2024
after a very long wait. Fastify and Hono are faster modern alternatives.
Deno (2018) and Bun (2022) are competing runtimes.
| Stack |
Since |
Concurrency |
Strong at |
| Node.js |
2009 |
event loop |
APIs, realtime, one language |
| Django |
2005 |
threads, async |
admin, ORM, batteries |
| FastAPI |
Dec 2018 |
async, ASGI |
typed APIs, auto docs |
| Spring Boot |
Apr 2014 |
threads, virtual |
large enterprise systems |
| Go |
2009, 1.0 2012 |
goroutines |
throughput, single binary |
| Rails |
July 2004 |
threads |
fast product iteration |
| Laravel |
June 2011 |
processes |
shared hosting, PHP shops |
| ASP.NET Core |
June 2016 |
async tasks |
Windows shops, strong tooling |
- Django was created at the Lawrence Journal-World by Adrian Holovaty and
Simon Willison and released publicly in July 2005; Django 6.1 is current.
FastAPI was released by Sebastian Ramirez in December 2018.
- Ruby on Rails was extracted from Basecamp by David Heinemeier Hansson in
July 2004 and popularized convention over configuration. Rails 8.1 is
current in 2026.
- Go was announced by Google on 10 November 2009 (Robert Griesemer, Rob Pike,
Ken Thompson) with 1.0 in March 2012; Go 1.26 is current. A goroutine starts
at about 2 KB of stack, versus about 1 MB for an operating system thread.
- Spring Framework began in 2003 from Rod Johnson’s book; Spring Boot arrived
in April 2014 and Spring Boot 4.1 is current in 2026. Java 25, released
16 September 2025, is the newest long-term-support release.
- PHP dates from 1995 (Rasmus Lerdorf); PHP 8.5 arrived November 2025. Laravel
was released by Taylor Otwell in June 2011 and is now at version 13.
- .NET Core 1.0 shipped June 2016 and unified with .NET 5 in November 2020;
.NET 10, released 11 November 2025, is the current long-term-support version.
- Honest guidance: pick the language your team already knows. Every framework
in that table serves millions of users somewhere. The differences that
matter are hiring, libraries and operational familiarity, not benchmarks.
- Tools:
curl -i, httpie, ab and wrk for load, strace or dtruss
for syscalls, and clinic.js or pprof for profiling.
WORDS35.9.6 remember these#
- Route — the address-to-code map — a match on HTTP method and path pattern
dispatching to a handler.
- Middleware — a step every request passes through — a composable function in
the request pipeline that may short-circuit the response.
- Handler — the code that answers — the terminal function producing a status,
headers and body; also called a controller action.
- Templating — the server builds the HTML — server-side rendering of markup
from data plus a template language.
- Event loop server — one worker, never waiting — non-blocking I/O multiplexed
with epoll, kqueue or IOCP through libuv or similar.
- Port — a numbered door on a machine — a 16-bit TCP or UDP endpoint
identifier bound by a listening socket.
35.10 Databases#
PLAIN35.10.1 in simple words#
- Programs forget everything when they stop. A database is the place data goes
so it survives, and so many users can share it safely.
- You could use files. People do, and then discover the hard parts: two users
writing at once, finding one record among ten million, and a crash halfway
through an update.
- A database solves exactly those three problems: concurrency, fast search, and
never being left half-done.
- The oldest and still best default is the relational database: data stored
as tables of rows and columns, queried with a language called SQL.
- A table is one kind of thing, such as books. A row is one of them. A
column is one fact about it.
- A primary key is the column that uniquely names a row. A foreign key
is a column in one table holding the key of a row in another, which is how
tables are joined together.
- NoSQL is a loose family of databases that gave up some of those rules for
speed, scale or a friendlier shape. Sometimes that is the right trade. Often
it is not.
- If you have no strong reason otherwise, start with PostgreSQL. This is not
fashion; it is the advice most experienced engineers give.
PLAIN35.10.2 a picture in your head#
- Think of a library’s paper records before computers.
- One drawer holds a card per book: title, author number, year. Another drawer
holds a card per author: number, name, country.
- The author’s name is written once, in the author drawer. The book card only
stores the author’s number.
- That is normalization, and the reason for it is simple: if an author’s
name is wrong, you fix one card, not four hundred.
- Looking up a book by title is fast because the drawer is sorted and you can
jump to the right divider. That sorted arrangement is an index.
- Without an index you would read every card in the drawer. With one you touch
about twenty. That gap is the whole reason indexes exist.
- And when a book moves between branches, both the “removed from A” and “added
to B” cards must change together, or the book vanishes. Doing both or
neither is a transaction.
Where this comparison breaks:
- Library cards do not lie to each other. Two clerks writing at the same moment
can produce a state neither intended, which is why isolation levels exist and
why they are subtle.
- And a card drawer holds thousands. A real index handles billions with roughly
the same number of steps, because it is a tree, not a list.
PLAIN35.10.3 a worked example#
- Start with a badly designed table. One row per sale.
| id |
book |
author |
author_country |
qty |
| 1 |
Dune |
F. Herbert |
US |
2 |
| 2 |
Emma |
J. Austen |
UK |
1 |
| 3 |
Dune |
F. Herbert |
US |
3 |
- Three faults. The author’s country is repeated; if it is wrong it is wrong in
many places. You cannot record an author with no sales. And updating one row
can leave the data disagreeing with itself.
- Normalized into three tables:
CREATE TABLE authors (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
country CHAR(2) NOT NULL
);
CREATE TABLE books (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
author_id INT NOT NULL REFERENCES authors(id)
);
CREATE TABLE sales (
id SERIAL PRIMARY KEY,
book_id INT NOT NULL REFERENCES books(id),
qty INT NOT NULL CHECK (qty > 0),
sold_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
- Now a real query: total copies sold per country, best first, ignoring
countries with fewer than 10 sales.
SELECT a.country,
SUM(s.qty) AS copies
FROM sales s
JOIN books b ON b.id = s.book_id
JOIN authors a ON a.id = b.author_id
WHERE s.sold_at >= '2026-01-01'
GROUP BY a.country
HAVING SUM(s.qty) >= 10
ORDER BY copies DESC
LIMIT 5;
JOIN follows the foreign keys to stitch the three tables back together.
GROUP BY collapses many rows into one per country. WHERE filters rows
before grouping; HAVING filters groups after. That difference is examined
constantly and understood rarely.
- Now indexes, with real arithmetic. A
sales table with 10 million rows.
- Without an index, finding rows for one
book_id reads all 10,000,000 rows.
At perhaps 2 million rows per second in memory, that is about 5 seconds.
- With a B-tree index, the rows are held in a sorted tree with a high branching
factor. If each node holds about 200 keys, then 200^3 is 8 million and
200^4 is 1.6 billion.
- So four levels cover the table. The lookup reads four pages instead of ten
million. That is roughly a million times less work, and it is why the answer
comes back in under a millisecond.
CREATE INDEX idx_sales_book ON sales (book_id, sold_at);
EXPLAIN ANALYZE
SELECT * FROM sales WHERE book_id = 42;
EXPLAIN prints the plan the database intends to use. Seq Scan means it
is reading everything; Index Scan means it is using the tree. Adding
ANALYZE actually runs it and reports real timings and row counts.
- The single most useful debugging habit in backend work is comparing the
planner’s estimated rows with the actual rows. A large gap means the
statistics are stale, and the plan is probably bad.
PLAIN35.10.4 what is really happening inside#
- ACID, one letter at a time.
- A, Atomicity. A transaction happens completely or not at all. Money
leaving one account and arriving in another either both happen or neither
does. A crash in between leaves no half-state.
- C, Consistency. The database never ends a transaction in a state that
breaks the rules you declared: foreign keys, uniqueness, checks.
- I, Isolation. Concurrent transactions do not see each other’s unfinished
work. How strictly is a setting, and that is the interesting part.
- D, Durability. Once the database says “committed”, the data survives an
immediate power cut, because it was written to a log on disk and flushed.
- Isolation levels, weakest to strongest, and the anomaly each one stops:
| Level |
Prevents |
Still allows |
| Read uncommitted |
nothing |
dirty reads |
| Read committed |
dirty reads |
non-repeatable reads |
| Repeatable read |
non-repeatable reads |
phantoms (in theory) |
| Serializable |
all of the above |
nothing, but retries |
- A dirty read is seeing another transaction’s uncommitted change. A
non-repeatable read is reading the same row twice and getting different
values. A phantom is running the same query twice and getting a new row.
- PostgreSQL defaults to read committed and never permits dirty reads at all.
Its repeatable read also prevents phantoms, because it uses snapshots.
MySQL’s InnoDB defaults to repeatable read. These are implementation details
worth checking for your engine.
- Connection pooling. Opening a database connection is expensive: a TCP
handshake, authentication, and on PostgreSQL a new operating system process,
costing several megabytes.
- So the application keeps a small set of open connections and lends them out.
A pool of 10 to 30 usually serves far more concurrent users than that,
because each request holds a connection for only milliseconds.
- The classic production failure is one pool per instance times many
instances, exhausting the server’s connection limit. PgBouncer exists to sit
in the middle and fix exactly that.
TECHNICAL35.10.5 the engineer’s version#
- The relational model comes from Edgar F. Codd’s 1970 paper at IBM,
“A Relational Model of Data for Large Shared Data Banks”. SEQUEL was designed
by Donald Chamberlin and Raymond Boyce in 1974 and renamed SQL.
- SQL was standardized by ANSI in 1986 and ISO in 1987; the current edition is
SQL:2023. Every engine adds extensions, so portable SQL is a discipline.
- B-trees come from Rudolf Bayer and Edward McCreight in 1970, published 1972.
Databases use B+ trees, where all values sit in the leaves and leaves are
linked, making range scans cheap.
- ACID as a concept is Jim Gray’s, 1981; the acronym was coined by Theo Harder
and Andreas Reuter in 1983.
- PostgreSQL descends from POSTGRES, begun by Michael Stonebraker at Berkeley
in 1986; Postgres95 added SQL, and the name PostgreSQL dates from 1996.
PostgreSQL 18 was released on 25 September 2025.
| Type |
Example |
Shape |
Good at |
| Relational |
PostgreSQL 18 |
tables, SQL |
almost everything |
| Document |
MongoDB 8.3 |
JSON documents |
flexible schema |
| Key-value |
Redis 8.10 |
key to value |
cache, counters, queues |
| Wide-column |
Cassandra 5 |
partitioned rows |
huge write volume |
| Graph |
Neo4j |
nodes and edges |
relationship traversal |
| Search |
Elasticsearch |
inverted index |
full-text ranking |
| Time-series |
TimescaleDB |
time-partitioned |
metrics, sensors |
- Origins: MongoDB from 10gen in 2009 (Dwight Merriman, Eliot Horowitz); Redis
in 2009 by Salvatore Sanfilippo; Cassandra at Facebook in 2008, Apache in
2010; Neo4j in 2007; Elasticsearch in 2010 by Shay Banon.
- Redis changed licence in March 2024, which produced the Valkey fork under the
Linux Foundation, then adopted AGPLv3 with Redis 8 in May 2025. If you use a
managed cache, check which one you are actually running.
- CAP theorem, stated correctly. It was conjectured by Eric Brewer in a
July 2000 keynote and proved by Seth Gilbert and Nancy Lynch in 2002.
- It says: when a network partition occurs, a distributed system must
choose between consistency (every read sees the latest write) and
availability (every request gets a non-error answer).
- The common misuse is to describe systems as “CP” or “AP” as a permanent
personality, or to say you may “pick two of three”. You do not choose
partitions; the network chooses them for you. C versus A is a choice made
only during a partition, often per operation.
- Daniel Abadi’s PACELC (2012) is the honest extension: else, when there is no
partition, you still trade latency against consistency.
- Five scenarios, and the honest answer for each.
| Scenario |
Choose |
Why |
| Payments, orders |
PostgreSQL |
transactions decide correctness |
| Session cache |
Redis |
sub-ms reads, expiry built in |
| Product search |
Elasticsearch |
ranking, typo tolerance |
| Device telemetry |
Timescale |
time partitioning, retention |
| Social graph paths |
Neo4j |
multi-hop traversal |
- Note what is missing: no scenario says “MongoDB because we do not know the
schema yet”. PostgreSQL has a
jsonb column type with indexing, so you can
keep flexible documents inside a relational database and still have joins
and transactions when you need them.
- Tools:
psql, EXPLAIN (ANALYZE, BUFFERS), pg_stat_statements for the
slowest queries, pgbench for load, and the slow query log in MySQL.
WORDS35.10.6 remember these#
- Primary key — the unique name of a row — a column set with a uniqueness and
not-null constraint identifying each tuple.
- Foreign key — a pointer to another table’s row — a referential integrity
constraint enforced on insert, update and delete.
- Normalization — store each fact once — decomposing relations to remove update
anomalies, usually to third normal form.
- Index — the sorted shortcut — an auxiliary B+ tree or hash structure mapping
key values to row locations.
- Transaction — all or nothing — a unit of work bounded by BEGIN and COMMIT
with ACID guarantees.
- Isolation level — how much of others’ work you may see — the setting choosing
which concurrency anomalies are permitted.
- CAP theorem — under a network split, pick consistency or answers — during a
partition a system cannot be both linearizable and available.
- Connection pool — a reused set of open lines — a bounded cache of database
connections shared across requests.
35.11 ORMs and data access#
PLAIN35.11.1 in simple words#
- Your program thinks in objects. The database thinks in tables. Something has
to translate.
- An ORM, an Object-Relational Mapper, is a library that does that
translation for you.
- You write
user.orders in your language and it writes SQL, runs it, and
turns the rows back into objects.
- The gain is real: less repetitive code, fewer typos, and safety against SQL
injection because values are sent separately from the query text.
- The cost is also real: it is easy to write one innocent line that causes
hundreds of database round trips.
- An ORM usually brings migrations too: small, ordered, versioned scripts
that change the shape of the database and are applied in the same order on
every machine.
PLAIN35.11.2 a picture in your head#
- An ORM is a translator at a meeting between two people who share no language.
- For ordinary sentences the translation is faithful and saves everyone effort.
- But if you ask an ambiguous question, the translator will produce something
grammatical and wrong, and you will not notice because you cannot read the
other language.
- So a good engineer learns enough of the other language to check the
translation, even when using the translator daily.
Where this comparison breaks:
- A human translator gets tired. An ORM is perfectly consistent, which means a
bad pattern is repeated identically ten thousand times a second.
PLAIN35.11.3 a worked example#
- The N+1 query problem, the single most common performance bug in backend
work.
orders = Order.objects.filter(status="paid") # 1 query
for o in orders:
print(o.customer.name) # 1 query per order
- With 200 orders that is 1 + 200 = 201 round trips. At 2 ms each, that is
about 0.4 seconds of pure waiting, for data one query could fetch.
- The fix is to tell the ORM to fetch the related rows in advance:
orders = (Order.objects
.filter(status="paid")
.select_related("customer")) # 1 query, joined
- Now it is one query with a JOIN. Roughly 2 ms instead of 400 ms.
- The same fix has different names in different tools:
select_related and
prefetch_related in Django, include in Prisma, joinedload in
SQLAlchemy, includes in Rails, with in Laravel.
- The way to catch it is to log queries per request in development. If one page
view issues 300 queries, you have found it.
PLAIN35.11.4 what is really happening inside#
- The ORM keeps a map from classes to tables and from attributes to columns,
plus rules for relationships.
- Accessing a relationship attribute that has not been loaded triggers a
lazy load: a fresh query, issued right there in the loop. That is where
N+1 comes from.
- Many ORMs also keep an identity map and a unit of work, so the same row read
twice gives the same object and changes are written in one flush at the end.
- Migrations work by recording which scripts have run in a table inside the
database itself. On deploy, the tool compares that list with the files on
disk and applies the missing ones in order.
- Never edit an applied migration. Write a new one. The applied list is the
shared history and rewriting it desynchronizes environments.
- Raw SQL is better when the query is the point: complex reporting, window
functions, recursive queries, bulk updates, and anything where you need to
control the plan.
- Modern practice is to use the ORM for the ordinary 90 percent and drop to SQL
for the 10 percent that matters, rather than choosing one forever.
TECHNICAL35.11.5 the engineer’s version#
- The term object-relational impedance mismatch describes the structural gap:
inheritance, identity and associations have no direct relational equivalent.
- Notable tools and dates: Hibernate for Java (2001, Gavin King), Rails
ActiveRecord (2004), Django ORM (2005), SQLAlchemy (2006, Michael Bayer),
Entity Framework (2008), Eloquent (2011), Prisma (2019, now version 7),
Drizzle (2022).
- Two design patterns, from Martin Fowler’s 2002 book Patterns of Enterprise
Application Architecture: Active Record, where a row and its behaviour are
one object, and Data Mapper, where they are separate.
- Query builders such as Knex, jOOQ and Drizzle sit between raw SQL and a full
ORM: typed composition, no object graph, no lazy loading, so no N+1.
- Parameterized queries prevent SQL injection because the driver sends the
query text and the values on separate paths, so a value can never become
syntax. String concatenation into SQL is the vulnerability.
- Migration tools: Alembic, Django migrations, Flyway, Liquibase, Prisma
Migrate, Rails Active Record migrations, EF Core migrations.
- Zero-downtime schema change is a separate discipline: add a nullable column,
backfill in batches, start writing both, switch reads, then drop the old
column, across several deploys. Never rename in one step.
- Tools:
django-debug-toolbar, SQLAlchemy echo=True, Prisma’s query event
log, and pg_stat_statements to find the query issued most often rather
than the slowest single one.
WORDS35.11.6 remember these#
- ORM — a translator between objects and tables — a mapping layer generating
SQL and materializing rows as domain objects.
- N+1 — one query, then one more per result — repeated lazy loads inside an
iteration, fixed by eager loading or a join.
- Migration — a versioned change to the schema — an ordered, recorded script
applied idempotently across environments.
- Lazy loading — fetch it only when touched — deferred query execution on first
access to an unloaded association.
- Parameterized query — values sent apart from the text — placeholders bound by
the driver, preventing SQL injection by construction.
35.12 Authentication and sessions#
PLAIN35.12.1 in simple words#
- Authentication is proving who you are. Authorization is deciding what
you are allowed to do once you are known.
- They are two different questions, they fail in two different ways, and mixing
them up is one of the most common security bugs there is.
- A site must never keep your password the way you typed it. If a site can
email your old password back to you, it stored it wrongly.
- Instead it stores a scrambled fingerprint of the password, produced by a
one-way function called a hash.
- One-way means you can go from password to fingerprint easily, and from
fingerprint back to password essentially never.
- It also mixes in a random extra value called a salt, different for every
single user, so two people with the same password get different fingerprints.
- And the hash used for passwords is made deliberately slow. That sounds mad,
and it is the entire point.
- Once you have logged in, the site has to remember you on the next click,
because HTTP forgets everything between one request and the next.
- There are two ways to remember. Keep a note on the server and hand you a
ticket number: that is a session. Or hand you a sealed pass that you carry
and the server does not store: that is a token.
- When you click “sign in with Google”, you never give Google’s password to the
site you are visiting. A protocol called OAuth 2.0 arranges that carefully.
PLAIN35.12.2 a picture in your head#
- Think of a cloakroom at a theatre, and a wristband at a festival.
- A session is the cloakroom ticket. Your coat stays with the cloakroom staff
and you carry nothing but a number.
- The number means nothing on its own. Only the staff’s book turns number 47
into “the long grey coat belonging to that person”.
- If you lose the ticket, the staff can cross out number 47 in the book
instantly, and it stops working everywhere, at once.
- A token is the festival wristband. Your name is printed on it and it carries a
tamper-proof seal that any gate can check without phoning anyone.
- That makes gates fast and independent. Ten gates can check ten thousand
wristbands with no shared book at all.
- But if you are thrown out of the festival, the wristband is still on your
wrist. It keeps working until it expires, unless every gate is handed a list
of banned wristbands - and a shared list is a cloakroom book again.
Where this comparison breaks:
- A photograph of a wristband gets you nowhere at a real gate, because a human
looks at your face. A copied token is not a picture of you. To the server it
simply is you, completely, until the moment it expires.
- A real cloakroom holds a few hundred coats in one room. A session store holds
millions of tickets across many servers, which is why real systems keep it in
Redis or a database rather than in one server’s memory.
PLAIN35.12.3 a worked example#
- Here is a bcrypt password record, the kind that sits in a
users table. It
is one string that carries four things at once.
$2b$12$KIXqf8vN1sT.u2WcQ7pOyeR3hZ...
$2b$ the algorithm and version (bcrypt)
12$ the cost factor: 2^12 = 4096 key rounds
KIX... the first 22 characters are the salt
R3h... the rest is the hash itself
- Notice there is no separate salt column. The salt travels inside the record,
because it is not a secret. It only has to be unique and unpredictable.
- Checking a login runs the same function again with the stored salt and cost,
then compares the results. It never reverses anything.
- The cost factor is a power of two, so each step up doubles the work. Rough
times on one ordinary 2025 server core:
| Cost |
Rounds |
Time per hash |
| 8 |
256 |
about 6 ms |
| 10 |
1,024 |
about 25 ms |
| 12 |
4,096 |
about 100 ms |
| 14 |
16,384 |
about 400 ms |
- Why slow is the goal: an attacker who steals the table wants to try billions
of guesses. At cost 12 a single core manages about 10 guesses a second.
- A fast hash such as SHA-256 runs at billions of guesses a second on a GPU.
The same stolen table would fall in hours instead of centuries.
- The salt kills the other attack. Without salts, an attacker hashes the top ten
million common passwords once and looks up every user at the same time.
- With a unique salt per user, that precomputed table is worthless, and the
attacker must redo all the work separately for every single account.
- Now authorization, with a worked role-based example. Three roles, four
permissions, and one rule table.
| Role |
Read post |
Edit any post |
Delete user |
| reader |
yes |
no |
no |
| editor |
yes |
yes |
no |
| admin |
yes |
yes |
yes |
- A user is given roles. A role is given permissions. Code never asks “is this
user an admin”; it asks “does this user have
post.edit”.
ROLE_PERMS = {
"reader": {"post.read"},
"editor": {"post.read", "post.edit"},
"admin": {"post.read", "post.edit", "user.delete"},
}
def can(user, perm):
return any(perm in ROLE_PERMS[r] for r in user.roles)
- The difference matters when the rules change. Adding a “moderator” role who
may edit but not delete is one line in the table.
- If the code had said
if user.is_admin in forty places, the same change is
forty edits and you will miss one.
- Authentication failed gives HTTP 401. Authorization failed gives HTTP 403.
The 401 name in the standard is “Unauthorized”, which is a historical
mistake; it really means unauthenticated.
PLAIN35.12.4 what is really happening inside#
- Session flow, step by step. You POST your email and password over HTTPS.
- The server looks up the row, runs the hash function with the stored salt, and
compares. It uses a constant-time comparison so timing leaks nothing.
- On success it creates a random session identifier, at least 128 bits from a
cryptographically secure generator, and stores it with your user id.
- It sends that identifier back in a
Set-Cookie header. The browser stores it
and attaches it to every later request to that site automatically.
- The cookie carries flags that do the real security work.
HttpOnly hides it
from JavaScript. Secure sends it only over HTTPS. SameSite controls
whether other sites can cause it to be sent.
- Logging out deletes the server record. The ticket is dead immediately.
- Token flow, step by step. Same login, but the server returns a JWT, a
JSON Web Token, and stores nothing.
- A JWT is three base64url-encoded parts joined by dots: a header saying which
algorithm, a payload of claims such as user id and expiry, and a signature.
- The signature is computed by the server over the first two parts using a
secret key. Anyone can read a JWT; only the key holder can forge one.
- Read that again, because it is the mistake people make: a JWT is signed, not
encrypted. Never put anything private in the payload.
- To check a request, the server verifies the signature and the expiry. No
database lookup at all, which is why tokens scale across many servers.
- The cost is revocation. There is no record to delete, so a stolen token works
until it expires. That is why access tokens are given minutes, not days.
- The standard fix is a pair: a short access token, plus a long-lived refresh
token kept in an
HttpOnly cookie, which can be revoked and rotated.
- Now the delegated login flow. The point of OAuth 2.0 is that the site you are
visiting never sees your Google password, and never can.
- The four parties: you (the resource owner), the site (the client), Google’s
login server (the authorization server), and Google’s API (the resource
server).
You Site (client) Auth server API
| | | |
|--click sign in-->| | |
| |--redirect---->| |
|<---------- login page -------| |
|--password + consent--------->| |
| |<--code (in URL redirect)--|
| | | |
| |--code+secret+verifier---->|
| |<--access token + id token-|
| | | |
| |--token------------------->|
| |<--your profile data-------|
|<--logged in--| | |
- Step 1: the site sends your browser to the authorization server with its
client id, the scopes it wants, a redirect address, a random
state value,
and a code challenge.
- Step 2: you log in on Google’s own page, on Google’s own domain, and approve
the list of scopes. The site is not involved and cannot see any of it.
- Step 3: Google redirects your browser back to the site with a short-lived
authorization code in the URL.
- Step 4: the site’s server exchanges that code, over a direct back-channel
call, for tokens. This step needs the client secret and the code verifier,
which never touch the browser.
- Step 5: the site calls the API with the access token to fetch the data it was
granted, and nothing more.
- Why the extra hop with a code, instead of just handing over a token? Because
URLs leak. They land in browser history, server logs and referrer headers.
A code is single-use and worthless without the secret.
- OpenID Connect is a thin layer on top. OAuth 2.0 alone answers “may this site
read your calendar”. It does not answer “who are you”.
- OpenID Connect adds an ID token, a JWT about you specifically, plus a
standard
userinfo endpoint and the standard scopes openid, profile
and email. That is the part that makes it a login system.
TECHNICAL35.12.5 the engineer’s version#
- bcrypt was published by Niels Provos and David Mazieres at USENIX in 1999,
built on the Blowfish cipher (Bruce Schneier, 1993). Its cost parameter is
log-2, so cost 12 means 2^12 key expansion rounds.
- bcrypt’s real limits: it truncates input at 72 bytes, and some
implementations mishandle the NUL byte. Do not pre-hash into it without
base64 encoding first, or you can shorten the effective input.
- scrypt came from Colin Percival in 2009 and is specified in RFC 7914 (2016).
It added memory hardness, to blunt custom hardware.
- Argon2 won the Password Hashing Competition in July 2015. Authors: Alex
Biryukov, Daniel Dinu and Dmitry Khovratovich. It is specified in RFC 9106
(September 2021). Use the Argon2id variant.
- Current OWASP Password Storage Cheat Sheet settings, as of 2026:
| Algorithm |
Recommended setting |
| Argon2id |
m=19456 KiB, t=2, p=1 |
| bcrypt |
cost factor 10 or more |
| scrypt |
N=2^17, r=8, p=1 |
| PBKDF2-SHA256 |
600,000 iterations |
- A pepper is a site-wide secret mixed in before or after hashing, kept
outside the database, ideally in a key management service. It protects
against a database-only leak. It is optional; salting is not.
- NIST SP 800-63B-4, “Digital Identity Guidelines: Authentication and
Authenticator Management”, published 31 July 2025, is the current reference.
- It says: require at least 8 characters, accept at least 64, allow all
printable ASCII and Unicode, and do not impose composition rules such as
“one capital and one symbol”.
- It also says do not force periodic rotation without evidence of compromise,
and do check new passwords against known-breached lists. Forced 90-day
rotation is now explicitly discouraged, reversing decades of advice.
- JWT is RFC 7519 (May 2015), sitting on JWS (RFC 7515), JWE (RFC 7516), JWK
(RFC 7517) and JWA (RFC 7518). Bearer usage is RFC 6750.
- The four classic JWT mistakes, all still seen in production:
| Mistake |
Why it is fatal |
| Storing in localStorage |
any XSS reads it directly |
No exp claim |
the token never dies |
Accepting alg: none |
signature check skipped |
| Not pinning the algorithm |
RS256 forced down to HS256 |
- The
alg: none and algorithm-confusion attacks were publicized by Tim
McLean in March 2015. In the confusion attack the server is tricked into
verifying an HMAC using the public RSA key as the shared secret, which the
attacker also has. Always pin the expected algorithm on the verify side.
localStorage versus cookies, stated plainly: localStorage is readable by
any script running on the page, so one cross-site scripting hole exports
every token. An HttpOnly cookie is not reachable from JavaScript at all.
- The cost of cookies is cross-site request forgery, which
SameSite=Lax or
Strict plus a CSRF token handles. Chrome made SameSite=Lax the default
in version 80, February 2020.
- OAuth 2.0 is RFC 6749 (October 2012), replacing OAuth 1.0 (RFC 5849, 2010).
PKCE, Proof Key for Code Exchange, is RFC 7636 (September 2015).
- RFC 9700, “Best Current Practice for OAuth 2.0 Security”, published
30 January 2025 as BCP 240, is now the document to follow. It requires PKCE
for all clients including confidential ones, forbids the implicit grant and
the resource owner password credentials grant, and requires exact string
matching of redirect URIs.
- OAuth 2.1 is an IETF draft, not yet an RFC as of 2026. It folds RFC 9700’s
advice into one document. Say “draft”, not “standard”.
- OpenID Connect Core 1.0 was published by the OpenID Foundation in February
2014. Discovery lives at
/.well-known/openid-configuration; signing keys
at a JWKS endpoint.
- Single sign-on in enterprises is still largely SAML 2.0, an OASIS standard
from March 2005, using signed XML assertions over HTTP POST. New systems use
OIDC; you will meet SAML because it is entrenched, not because it is better.
- Multi-factor authentication means two of: something you know, something you
have, something you are. Two passwords are not two factors.
| Factor |
Standard |
Real weakness |
| SMS code |
none, carrier route |
SIM swap, SS7 |
| TOTP app |
RFC 6238, 2011 |
phishable in real time |
| Push approve |
vendor specific |
fatigue, blind taps |
| Passkey |
WebAuthn, FIDO2 |
device loss, recovery |
- TOTP (RFC 6238, May 2011) builds on HOTP (RFC 4226, December 2005): a
30-second time step, 6 digits, HMAC-SHA1 by default over a shared secret.
- WebAuthn became a W3C Recommendation on 4 March 2019, with Level 2 in April
2021. Passkeys are discoverable WebAuthn credentials synced by the platform.
- Passkeys are phishing-resistant by construction: the credential is bound to
the origin, so a look-alike domain simply gets no signature. That property is
an established fact from the protocol, not a marketing claim.
- RBAC was formalized by David Ferraiolo and Richard Kuhn at NIST in 1992 and
by Ravi Sandhu and colleagues in 1996, and standardized as INCITS 359-2004.
- ABAC, attribute-based access control (NIST SP 800-162, 2014), decides from
attributes of subject, object, action and environment. It is more expressive
and much harder to audit. Most teams want RBAC with a few ownership checks.
- Tools:
bcrypt, argon2-cffi, libsodium; jwt.io and the jose
libraries for inspecting tokens; openssl rand -base64 32 for secrets;
Keycloak, Auth0, Okta and Entra ID as identity providers.
WORDS35.12.6 remember these#
- Authentication — proving who you are — verifying a claimed identity against
one or more authenticators.
- Authorization — deciding what you may do — evaluating a policy over subject,
action and resource after identity is established.
- Salt — a per-user random extra — a unique non-secret value that defeats
precomputed rainbow tables.
- Pepper — a site-wide secret extra — a key held outside the database and mixed
into the hash input or output.
- Work factor — how slow the hash is on purpose — the cost, memory and
parallelism parameters tuned to a target verify time.
- Session — a ticket the server remembers — server-side state keyed by an opaque
high-entropy identifier held in a cookie.
- JWT — a signed pass the user carries — a JSON Web Token, base64url header,
payload and signature under RFC 7519.
- Access token — the short-lived key to the API — a bearer credential with a
minutes-long
exp claim.
- Refresh token — the long-lived key to get new keys — a revocable credential
exchanged at the token endpoint, usually rotated on each use.
- OAuth 2.0 — letting a site act for you without your password — a delegated
authorization framework, RFC 6749, with PKCE per RFC 9700.
- OpenID Connect — the login layer on top of OAuth — an identity layer adding
an ID token and a userinfo endpoint.
- RBAC — permissions attached to job titles — role-based access control per
INCITS 359-2004, users to roles to permissions.
35.13 Application architecture#
PLAIN35.13.1 in simple words#
- Architecture is the set of decisions that are expensive to change later. It is
not about which framework you pick.
- A monolith is one program that does everything. One codebase, one build,
one deploy, usually one database.
- Microservices means the same work split into many small programs that talk
over the network, each with its own deploy and often its own database.
- The honest advice, given by most experienced engineers, is this: start with a
monolith. Almost every team should.
- Inside any of them, the code is arranged in layers, so that the part
drawing the screen is not also the part talking to the database.
- Some answers are expensive to compute and rarely change, so we keep a copy
ready to hand. That copy is a cache.
- Some work does not need to happen while the user waits. We drop a note into a
queue and a separate worker picks it up later.
- Sometimes the server needs to tell the browser something without being asked,
which normal request-and-reply cannot do.
- And because anyone can call your server as fast as they like, you have to cap
how often each caller may do it. That is rate limiting.
PLAIN35.13.2 a picture in your head#
- Think of a restaurant.
- A monolith is one kitchen. Everybody cooks in the same room, shares the same
fridge, and shouts across the pass when something is ready.
- Communication is instant and free. Nobody has to book a phone call to ask
whether the sauce is done.
- When the room gets too crowded, you cannot make just the grill section bigger.
You have to build a second identical kitchen.
- Microservices are several small kitchens in separate buildings: one does
bread, one does sauces, one does desserts.
- Each can be scaled, replaced or rebuilt without closing the others. But every
request between them is now a phone call that can be busy, slow or dropped.
- The caching layer is the pass with finished dishes under a heat lamp. Serving
from there takes seconds instead of minutes.
- The queue is the ticket rail. Orders go up, cooks take them down in order, and
nothing is lost if one cook steps away.
Where this comparison breaks:
- A kitchen phone call fails loudly and immediately. A network call can succeed
at one end and be lost at the other, so the sender never learns which.
- Real kitchens do not silently duplicate an order because the first
acknowledgement went missing. Distributed systems do exactly that, which is
why every message consumer must be safe to run twice.
PLAIN35.13.3 a worked example#
- A product page that shows a price, a stock count and a review average.
- Version one asks the database on every single request. At 500 requests a
second, that is 1,500 queries a second for data that changes hourly.
- Version two caches the assembled result in Redis for 60 seconds.
def product(pid):
key = f"product:{pid}"
hit = redis.get(key)
if hit:
return json.loads(hit) # about 0.5 ms
data = build_from_db(pid) # about 40 ms
redis.setex(key, 60, json.dumps(data))
return data
- With a 95 percent hit rate the average response time falls from about 40 ms to
about 2.5 ms, and the database sees 75 queries a second instead of 1,500.
| Layer |
Typical hit time |
Who it serves |
| CDN edge |
5 to 30 ms |
everyone, worldwide |
| HTTP browser cache |
0 ms |
one returning user |
| Redis |
0.3 to 1 ms |
all app servers |
| In-process memory |
0.0001 ms |
one server only |
| PostgreSQL |
1 to 50 ms |
the source of truth |
- Now the queue. When an order is placed we must charge the card, send an
email, generate a PDF invoice and update a search index.
- Doing all four inline makes the user wait about 3 seconds and couples the
checkout button to the email provider being awake.
- Doing only the charge inline, and pushing the other three onto a queue, gives
a 200 ms response and a checkout that survives the email provider going down.
@app.post("/orders")
def create_order(req):
order = save_order(req) # inline, must be certain
charge_card(order) # inline, user needs the answer
queue.enqueue("send_receipt", order.id)
queue.enqueue("build_invoice", order.id)
queue.enqueue("index_order", order.id)
return {"id": order.id} # about 200 ms
- The rule that makes this safe: the worker must be idempotent, meaning
running it twice has the same effect as running it once.
- So
send_receipt records that it sent receipt number 5512 before it exits,
and checks that record on entry. Otherwise a retry mails the customer twice.
PLAIN35.13.4 what is really happening inside#
- Layers, from the outside in. The transport layer receives HTTP and knows
nothing about your business. The service or application layer holds the rules.
The data access layer talks to the database. The domain holds the concepts.
- The discipline is that dependencies point one way, inward. The database layer
must never import the HTTP layer.
- MVC is the oldest version of this idea. The Model holds data and rules,
the View renders, the Controller receives input and coordinates.
- In a modern web app the View often lives in the browser and the Controller is
an HTTP route handler, which is why the term drifted.
- Caching happens at four separate places and they are independent: in the
browser, at a CDN, in a shared store like Redis, and inside the process.
- HTTP caching is negotiated with headers.
Cache-Control: max-age=3600 says
how long a copy stays fresh. ETag gives a version fingerprint, so a stale
copy can be revalidated with a cheap 304 Not Modified instead of a resend.
- A CDN is a network of servers placed near users. It caches your responses
physically close to the reader, which cuts the speed-of-light cost that no
amount of server tuning can remove.
- The hard part of caching is not storing. It is knowing when a stored answer
became a lie. There are two strategies: expire on a timer, or actively delete
the entry when the underlying data changes.
- Queues decouple in time. The producer writes a message and returns. The broker
stores it durably. A consumer takes it, does the work, and acknowledges.
- If the consumer crashes before acknowledging, the message reappears. That is
at-least-once delivery, and it is why duplicates happen.
- Failed messages retry with growing gaps, and after a limit are moved to a
dead-letter queue so one poisoned message cannot block the line forever.
- Server-to-browser messaging has three shapes. Polling asks repeatedly.
Server-sent events keeps one HTTP response open and streams text down it.
WebSockets upgrades the connection to a two-way channel.
- Rate limiting works by counting requests per key, usually per API key or per
IP address, and refusing once the count passes a threshold in a time window.
TECHNICAL35.13.5 the engineer’s version#
- Conway’s Law, from Melvin Conway’s 1967 paper “How Do Committees Invent?”,
states that a system’s structure copies the communication structure of the
organization that built it. Microservices without matching team boundaries
produce a distributed monolith, which has every cost and no benefit.
- The term microservices was popularized by James Lewis and Martin Fowler in
March 2014. Fowler’s own follow-up, “MonolithFirst” (June 2015), argues that
teams who started monolithic and split later did better than those who began
distributed.
- The honest trade-off table:
| Concern |
Monolith |
Microservices |
| Local call cost |
nanoseconds |
milliseconds, can fail |
| Deploy |
one unit, all at once |
independent per service |
| Transaction |
one ACID commit |
sagas, eventual consistency |
| Debugging |
one stack trace |
distributed tracing needed |
| Scaling |
whole app together |
per service, precisely |
| Team size that fits |
1 to 30 |
many teams, own on-call |
- Amazon’s Prime Video team published in March 2023 that moving an
audio-and-video monitoring service from distributed serverless components
back into a single process cut infrastructure cost by over 90 percent. It is
one case, not a law, but it punctured the idea that splitting always wins.
- A modular monolith is the current mainstream advice: one deployable unit
with strictly enforced internal module boundaries, so a later split is
mechanical rather than archaeological.
- MVC comes from Trygve Reenskaug at Xerox PARC in 1979, in Smalltalk-80.
Related patterns: MVP, MVVM (Microsoft, 2005), and hexagonal architecture
(Alistair Cockburn, 2005).
- HTTP caching is specified in RFC 9111 (June 2022), which replaced RFC 7234.
Key directives:
max-age, s-maxage for shared caches only, no-store,
private, must-revalidate, and stale-while-revalidate from RFC 5861.
- Validators are
ETag with If-None-Match, and Last-Modified with
If-Modified-Since. A match returns 304 with no body.
- Cache patterns: cache-aside (read through the app, the code above),
write-through, write-behind, and refresh-ahead. Cache-aside is the default
because it is the only one that is simple to reason about.
- A cache stampede happens when a popular key expires and a thousand
requests all miss and all rebuild it at once. Fixes: a mutex or single-flight
lock, probabilistic early expiry, or
stale-while-revalidate.
- Akamai, founded in 1998 out of MIT work by Tom Leighton and Daniel Lewin,
created the commercial CDN. Cloudflare followed in 2009, Fastly in 2011.
- Message brokers: RabbitMQ (2007, implementing AMQP 0-9-1), Apache Kafka
(built at LinkedIn by Jay Kreps, Neha Narkhede and Jun Rao, open sourced in
2011), Amazon SQS (generally available 2006), NATS, and Redis Streams.
- Kafka is a distributed append-only log with consumer offsets, so replay is
normal. RabbitMQ is a broker that deletes on acknowledgement. They solve
different problems and are frequently confused.
- Job frameworks by language: Celery and RQ for Python, Sidekiq for Ruby,
BullMQ for Node, Quartz and Spring Batch for Java, Hangfire for .NET.
- Retry policy in practice: exponential backoff with full jitter, a maximum
attempt count, and a dead-letter queue. Without jitter, retries synchronize
and produce a thundering herd against a recovering service.
- The real-time comparison:
| Property |
Polling |
SSE |
WebSocket |
| Direction |
client asks |
server to client |
both ways |
| Protocol |
plain HTTP |
plain HTTP |
upgrade, RFC 6455 |
| Reconnect |
trivial |
automatic |
you write it |
| Binary data |
yes |
no, UTF-8 text |
yes |
| Proxy trouble |
none |
rare |
occasional |
| Good for |
slow updates |
feeds, tokens, logs |
chat, games, cursors |
- WebSocket is RFC 6455 (December 2011); it starts as an HTTP request with
Upgrade: websocket and then leaves HTTP framing entirely. RFC 8441 (2018)
added bootstrapping over HTTP/2.
- Server-sent events are part of the HTML Living Standard, exposed as the
EventSource API, with Content-Type: text/event-stream. The browser
reconnects on its own and resumes with Last-Event-ID.
- SSE is the quiet default for AI token streaming, dashboards and progress
bars, because it is ordinary HTTP and needs no special infrastructure.
- Rate limiting algorithms and what they actually do:
| Algorithm |
Behaviour |
| Fixed window |
simple, allows 2x burst at edge |
| Sliding log |
exact, memory grows with traffic |
| Sliding counter |
near-exact, cheap, common |
| Token bucket |
allows a controlled burst |
| Leaky bucket |
smooths output to a fixed rate |
- The response is 429 Too Many Requests, defined in RFC 6585 (April 2012),
with a
Retry-After header from RFC 9110. The RateLimit-Limit and
RateLimit-Remaining headers are widely used by convention but are still an
IETF Internet-Draft, not an RFC, as of 2026.
- Tools:
redis-cli --stat and INFO stats for hit ratio, curl -I to read
cache headers, nginx limit_req and Envoy for edge limiting, OpenTelemetry
with Jaeger or Tempo for distributed tracing.
WORDS35.13.6 remember these#
- Monolith — one program that does it all — a single deployable unit sharing one
process, build and usually one datastore.
- Microservice — a small program with its own deploy — an independently
deployable service owning its data behind a network API.
- Modular monolith — one deploy, firm inner walls — enforced module boundaries
inside a single artifact, splittable later.
- Cache — a ready-made copy of a slow answer — a store trading staleness for
latency, governed by a TTL or explicit invalidation.
- CDN — servers near the user — a geographically distributed edge cache for
static and cacheable dynamic responses.
- ETag — a version fingerprint for a response — a validator enabling conditional
requests and 304 Not Modified replies.
- Queue — a durable to-do list between programs — a broker giving at-least-once
delivery with acknowledgement and retry.
- Idempotent — safe to run twice — an operation whose repeated application gives
the same result as one application.
- Dead-letter queue — the box for messages that keep failing — a sink for
messages exceeding the retry limit, kept for inspection.
- WebSocket — a two-way phone line — a full-duplex framed protocol over one TCP
connection, RFC 6455.
- Server-sent events — a one-way news feed — a long-lived HTTP response
streaming
text/event-stream with automatic reconnection.
- Rate limit — a cap on how often you may ask — a per-key request budget over a
window, refused with HTTP 429.
35.14 Mobile apps#
PLAIN35.14.1 in simple words#
- A phone app can be built in three broadly different ways, and the choice
decides most of what follows.
- Native means writing in the language the phone maker intends, with the
phone maker’s own toolkit. Two apps, two codebases, two teams’ worth of work.
- Cross-platform means writing once in a shared language and letting a
framework produce something that runs on both phones.
- Web-based means it is really a website, wrapped so it can be installed and
opened from the home screen. That is a progressive web app, a PWA.
- On iPhone, native means Swift, usually with the SwiftUI toolkit. On Android,
native means Kotlin, usually with Jetpack Compose.
- The main cross-platform choices are React Native, which uses JavaScript and
the real platform controls, and Flutter, which uses Dart and paints every
pixel itself.
- There is one more thing that has no equivalent on the web: a stranger at
Apple or Google reads your app and decides whether it may exist.
- On the web you deploy and it is live. On a phone store you submit, you wait,
and you can be told no.
- What you actually upload is a file. On Android it is an APK or an AAB. On
iPhone it is an IPA. Both are, underneath, ordinary ZIP archives.
PLAIN35.14.2 a picture in your head#
- Think about writing a book that must be published in two countries with
different languages and different censors.
- Native is hiring two native writers, each producing a book that reads
perfectly naturally in its own country. Best result, double the cost, and the
two books drift apart over time.
- Cross-platform is writing once and translating. React Native is a good human
translator using each country’s own idioms and typesetting. Flutter is
printing your own book, with your own fonts and paper, and shipping it
unchanged to both countries.
- Flutter’s version always looks identical everywhere, which is a feature if you
want your brand, and a problem if readers expected the local style.
- A PWA is publishing on a website that both countries can already read. No
censor, no printing, instant updates, and no place on the bookshop shelf.
Where this comparison breaks:
- A translated book cannot suddenly need a printing press the translator does
not own. A cross-platform app frequently needs a device feature the framework
has not wrapped yet, and then you write native code anyway.
- And a censor reads a book once. App review happens on every single update, so
it is a recurring tax on shipping, not a one-time gate.
PLAIN35.14.3 a worked example#
- The same screen, a counter with a button, written four ways. Notice how
similar the modern declarative shapes have become.
// iOS native: Swift with SwiftUI
struct CounterView: View {
@State private var n = 0
var body: some View {
VStack {
Text("Count: \(n)")
Button("Add") { n += 1 }
}
}
}
// Android native: Kotlin with Jetpack Compose
@Composable
fun Counter() {
var n by remember { mutableStateOf(0) }
Column {
Text("Count: $n")
Button(onClick = { n++ }) { Text("Add") }
}
}
// Flutter: Dart
class Counter extends StatefulWidget { ... }
// inside State:
Column(children: [
Text('Count: $n'),
ElevatedButton(
onPressed: () => setState(() => n++),
child: const Text('Add')),
])
// React Native: JavaScript
function Counter() {
const [n, setN] = useState(0);
return (<View>
<Text>Count: {n}</Text>
<Button title="Add" onPress={() => setN(n + 1)} />
</View>);
}
- Now the honest comparison for a real product decision.
| Approach |
Performance |
Device access |
| Native Swift or Kotlin |
best available |
everything, day one |
| Flutter |
very close to native |
good, plugin gaps |
| React Native |
good, JS bridge cost |
good, plugin gaps |
| PWA |
fine for content |
most limited |
- And the effort side, which is what actually decides most projects.
| Approach |
Effort for both phones |
Team you need |
| Native |
roughly 180 percent |
iOS plus Android devs |
| Flutter |
roughly 110 percent |
Dart devs, some native |
| React Native |
roughly 110 percent |
web devs, some native |
| PWA |
roughly 60 percent |
your existing web team |
- The “180 percent” is not 200 because design, backend and product work are
shared. The extra 10 percent on cross-platform is the native code you end up
writing anyway for permissions, push and store plumbing.
- A concrete rule that holds up: if the app is mostly forms, lists and content,
any of these works and you should pick by team. If it is a camera, a game, a
map with heavy custom rendering, or something that must feel invisible, go
native.
PLAIN35.14.4 what is really happening inside#
- Native code is compiled ahead of time to machine code for the device’s
processor and calls the operating system’s own UI objects directly. There is
no layer in between, so there is no layer to be slow.
- React Native runs your JavaScript in a JavaScript engine on the phone, and
that JavaScript creates and updates real platform views. A
Text really is a
UILabel on iOS and a TextView on Android.
- So React Native inherits the platform’s look and accessibility for free, and
pays a cost whenever data crosses between JavaScript and the native side.
- Flutter does the opposite. It ships its own rendering engine and draws every
button, scrollbar and cursor itself onto a blank canvas.
- That gives identical output on both platforms and total design control, and
means Flutter must re-implement anything the platform changes, including
accessibility behaviour and text selection.
- A PWA is a website with three additions: a manifest file naming the app and
its icons, a service worker script that can serve files while offline, and a
requirement to be served over HTTPS.
- The service worker is the key piece. It is a script that sits between the page
and the network, can answer requests from a local cache, and keeps running
briefly after the page closes.
- Store review, step by step. You build a signed package, upload it, fill in
metadata and screenshots, and it enters a queue.
- Automated scanning runs first: malware checks, private API usage, permission
declarations, size limits. Then, on Apple’s store, a human opens the app.
- The human checks the guidelines: does it do something useful, does it match
its description, does it collect data it declared, does it route payments for
digital goods through the store’s own system.
- Rejection is normal and is not the end. You get a message, you fix it, you
resubmit, and the clock restarts.
- What this means for shipping is the real point: you cannot fix a live bug in
ten minutes. Plan for a review delay on every release, keep a server-side
switch to turn broken features off without a new build, and never ship on a
Friday hoping to patch on Saturday.
- Now the files themselves, which ties directly to the file-format material in
Chapter 20: a file’s identity comes from the bytes at its start, not from its
extension.
- Open an APK in a hex viewer and the first four bytes are
50 4B 03 04, which
is PK followed by two control bytes. That is the ZIP local file header
signature, named after Phil Katz.
- An APK is a ZIP. Rename it to
.zip, unzip it, and you can read the whole
structure. So is an IPA. So is a .jar, a .docx and an .epub.
- Inside an APK you find compiled code, compiled resources, a compiled manifest
and a signature folder. Inside an IPA you find a folder called
Payload
holding one .app bundle.
app.apk (ZIP) app.ipa (ZIP)
+- AndroidManifest.xml +- Payload/
| (binary XML, not text) | +- MyApp.app/
+- classes.dex | +- MyApp (Mach-O)
+- resources.arsc | +- Info.plist (binary)
+- res/ | +- Assets.car
+- lib/arm64-v8a/*.so | +- _CodeSignature/
+- assets/ | +- embedded.mobileprovision
+- META-INF/ (signatures) +- iTunesMetadata.plist
- The manifest inside an APK is not readable text. It is Android binary XML, a
compact encoding with a string pool, which is why opening it in a text editor
shows mush. The tool
aapt2 dump badging prints it properly.
classes.dex holds Dalvik Executable bytecode: all your Kotlin and Java
classes merged into one register-based bytecode file, not the stack-based
.class files the Java compiler first produced.
Info.plist inside an iOS app is a property list, usually stored in Apple’s
binary plist format starting with the bytes bplist00, not XML.
TECHNICAL35.14.5 the engineer’s version#
- Swift was announced at Apple’s WWDC in June 2014, designed by a team led by
Chris Lattner, and open sourced in December 2015. Swift 6.3 is the current
release in 2026, shipping with Xcode 26.
- SwiftUI was announced at WWDC in June 2019. UIKit, from iPhone OS 2.0 in 2008,
remains fully supported and is still required for some controls, so real apps
mix both through
UIViewRepresentable.
- Kotlin came from JetBrains, announced in 2011, version 1.0 in February 2016.
Google announced first-class Android support in May 2017 and a Kotlin-first
policy in May 2019.
- Jetpack Compose reached 1.0 in July 2021. The older approach, XML layouts with
findViewById or view binding, is still everywhere in existing code.
- React Native was open sourced by Facebook in March 2015. Its New Architecture,
with the Fabric renderer and TurboModules replacing the old asynchronous
bridge, became the default in version 0.76 (October 2024). Version 0.87 was
released on 10 August 2026.
- Flutter’s first stable release, 1.0, was December 2018, after a 2017 alpha.
Flutter 3.47 is current in 2026. It uses Dart, compiled ahead of time to
native code for release builds, and the Impeller renderer, which replaced
Skia as the default on iOS and on Android API 29 and above.
- Other real options: Kotlin Multiplatform, stable since November 2023, which
shares business logic while leaving the UI native; .NET MAUI (May 2022);
Capacitor with Ionic; and Tauri for desktop.
- Progressive web app is not a standard; it is a name for a set of standards
used together: the Web App Manifest (a W3C specification), Service Workers
(W3C), and a secure context requirement.
- The term was coined by Frances Berriman and Alex Russell in 2015. Safari on
iOS 16.4, released 27 March 2023, finally added Web Push and badging for
home-screen web apps, closing the largest gap.
- Remaining PWA gaps on iOS as of 2026: no Web Bluetooth, no Web NFC, limited
background execution, storage that can be evicted, and no place in the App
Store search results, which is a distribution problem, not a technical one.
- App Store review facts. The App Store opened on 10 July 2008. Apple states
that on average 90 percent of submissions are reviewed in less than 24 hours.
That is an average, and complex or first-time submissions take longer.
- Common rejection causes are guideline 2.1 (crashes and incomplete
information), 4.2 (minimum functionality, which kills thin website wrappers),
3.1.1 (digital goods must use in-app purchase), and privacy label mismatches.
- Commission is 30 percent, or 15 percent under the App Store Small Business
Program for developers below one million US dollars a year, and 15 percent on
subscriptions after a subscriber’s first year.
- The EU Digital Markets Act forced alternative app marketplaces and
third-party browser engines on iOS in the EU from iOS 17.4, March 2024. This
is jurisdiction-specific and still changing; check the current position
rather than trusting any book, including this one.
- Google Play review facts. Review is mostly automated with human escalation.
New personal developer accounts must run a closed test with at least 12
testers for 14 continuous days before applying for production access, a rule
introduced in November 2023 and later relaxed from 20 testers to 12.
- Google Play has required the Android App Bundle for new apps since August
2021. Play requires apps to target API level 36, Android 16, by
31 August 2026.
- An AAB is a publishing format, not an installable one. Play uses it to
generate split APKs per device: one base, plus configuration splits for the
device’s screen density, CPU architecture and language. Typical download size
saving is 15 to 20 percent.
- APK signing schemes, and which Android version introduced each:
| Scheme |
Since |
What it covers |
| v1 (JAR) |
Android 1.0 |
each file, per entry |
| v2 |
Android 7.0, 2016 |
whole APK bytes |
| v3 |
Android 9, 2018 |
adds key rotation |
| v3.1 |
Android 13, 2022 |
rotation targeting |
| v4 |
Android 11, 2020 |
streaming install |
- iOS code signing uses a certificate plus a provisioning profile embedded as
embedded.mobileprovision. _CodeSignature/CodeResources holds a hash of
every file in the bundle, so changing one byte after signing breaks launch.
- App thinning means the store delivers only the slices a device needs. Apple
calls the pieces app slicing, bitcode (deprecated with Xcode 14, 2022) and
on-demand resources.
- Inspection commands worth knowing:
unzip -l app.apk # it really is a ZIP
aapt2 dump badging app.apk # readable manifest
apksigner verify -v app.apk # which schemes signed it
unzip -p app.ipa 'Payload/*.app/Info.plist' | plutil -p -
codesign -dvvv MyApp.app # signing identity
- Where experts disagree: whether Flutter’s own-rendering approach is a
strength or a liability. One camp values pixel-identical output and one
codebase. The other argues that redrawing platform widgets means permanently
chasing platform changes, especially in accessibility and text input. Both
positions have shipped very large apps.
WORDS35.14.6 remember these#
- Native app — built with the phone maker’s own tools — compiled against the
platform SDK, using the platform’s UI framework directly.
- Cross-platform — one codebase, both phones — a shared runtime or compiler
targeting iOS and Android from common source.
- PWA — a website you can install — a manifest plus a service worker over HTTPS,
installable to the home screen.
- Service worker — a script that answers requests offline — a background worker
intercepting fetches and managing a cache.
- APK — the Android app file — a ZIP containing DEX bytecode, binary XML
manifest,
resources.arsc and signature blocks.
- AAB — what you upload, not what installs — the Android App Bundle, from which
Play generates per-device split APKs.
- IPA — the iPhone app file — a ZIP containing a
Payload folder holding one
signed .app bundle with a Mach-O binary.
- DEX — Android’s compiled code format — Dalvik Executable, register-based
bytecode ahead-of-time compiled by ART at install.
- App review — a person deciding if your app may ship — guideline conformance
checking applied to every submission and update.
- Code signing — a seal proving nobody changed the app — certificate-backed
hashes over every file, verified at install and launch.
35.15 Testing, performance and accessibility for the web#
PLAIN35.15.1 in simple words#
- Three things separate a demo from a product: it keeps working when you change
it, it is fast on a normal phone, and everyone can use it.
- Tests are code that checks your other code. You run them automatically, every
time, so a mistake is caught in seconds rather than by a customer.
- There are three sizes of test. Small ones check one function. Medium ones
check that two real parts fit together. Large ones drive a real browser like a
real person.
- Small tests are fast and cheap, so write many. Large tests are slow and
fragile, so write few, and only for the paths that must never break.
- Speed is not a feeling, it is a measurement, and Google publishes three
specific numbers called the Core Web Vitals.
- One measures how long until the main thing appears. One measures how long the
page takes to answer a tap. One measures how much the page jumps around while
loading.
- Lighthouse is a free tool built into Chrome that scores a page and tells
you what to fix.
- Accessibility means the site works for people who cannot see it, cannot
use a mouse, or need much larger text.
- That is not a nice extra. It is a legal requirement in many places, and the
basics are genuinely small: correct HTML tags, text for images, working
keyboard navigation, readable contrast, a visible focus outline, and labels on
inputs.
PLAIN35.15.2 a picture in your head#
- Think of building a bridge and checking it three ways.
- You test each bolt in a jig before it goes in. That is a unit test: thousands
of them, seconds each, and a failure tells you exactly which bolt.
- You test one assembled span under load. That is an integration test: fewer,
slower, and a failure tells you the span is wrong but not which bolt.
- You drive a lorry across the finished bridge. That is an end-to-end test: a
handful, minutes each, and a failure tells you only that something is wrong.
- All three are worth doing. Only doing the lorry is how you end up rebuilding
the bridge to find one bad bolt.
Where this comparison breaks:
- A bridge bolt does not change while you are testing it. Software does. Tests
also protect against your future self, which a bolt jig never has to do.
- And a lorry crossing is reliable. Browser tests fail randomly for reasons that
have nothing to do with your code, which is called flakiness, and a test suite
nobody trusts is worse than no suite at all.
PLAIN35.15.3 a worked example#
- A single button component, tested at all three levels.
// unit: does the function do arithmetic
test("adds tax", () => {
expect(withTax(100, 0.2)).toBe(120);
});
// integration: do component and state work together
test("shows total after adding", async () => {
render(<Cart />);
await user.click(screen.getByRole("button",
{ name: /add/i }));
expect(screen.getByText("120")).toBeVisible();
});
// end-to-end: a real browser, a real server
test("checkout works", async ({ page }) => {
await page.goto("/product/42");
await page.getByRole("button", { name: "Buy" }).click();
await expect(page.getByText("Order placed")).toBeVisible();
});
- Notice the queries use
getByRole and the accessible name. A test written
that way fails if the button stops being reachable by a screen reader, so the
test suite quietly enforces accessibility.
- Now the performance numbers. Here is one real page before and after work.
| Metric |
Before |
After |
Good if |
| LCP |
4.8 s |
1.9 s |
2.5 s or less |
| INP |
420 ms |
140 ms |
200 ms or less |
| CLS |
0.31 |
0.02 |
0.1 or less |
| Lighthouse |
38 |
94 |
90 or more |
- What actually produced those changes, in order of effect.
- LCP: the hero image was 2.4 MB PNG. Converting to a 180 KB WebP, adding
width and height, and marking it fetchpriority="high" removed most of
the delay.
- CLS: the same missing
width and height were letting the text jump when the
image finally arrived. Reserving the space fixed it. A web font swapping in
late caused the rest, fixed with font-display: optional.
- INP: a 300 ms sorting loop ran inside the click handler. Breaking it up and
deferring the non-urgent part let the browser paint first.
- The general lesson: two of the three fixes were one HTML attribute pair. Most
real performance problems are not clever, they are neglected basics.
PLAIN35.15.4 what is really happening inside#
- A test runner imports your code, runs functions, compares results, and reports
the ones that differ. There is no magic in it.
- An end-to-end runner starts a real browser process and drives it through an
automation protocol, then reads back the rendered page.
- Test doubles replace slow or unreliable parts. A mock stands in for a
thing and records how it was called. A fake is a working cheap version,
such as an in-memory database.
- The trap is mocking so much that the test only proves your mocks agree with
each other. Prefer a real database in a container over a mocked one.
- LCP, Largest Contentful Paint, is the time from navigation start until the
biggest image or text block in the visible area finishes rendering. The
browser keeps updating its guess and stops at the first user interaction.
- INP, Interaction to Next Paint, watches every click, tap and key press for
the whole visit, measures from the input to the next frame painted, and
reports roughly the worst one.
- CLS, Cumulative Layout Shift, adds up how much visible content moved
without the user causing it. Each shift scores the fraction of the screen
affected times the distance moved. It has no unit.
- There are two kinds of measurement and they answer different questions. Lab
data comes from one run on one machine with simulated conditions. Field data
comes from millions of real visits on real phones.
- Lighthouse gives lab data. It is repeatable, so it is good for finding causes
and for stopping regressions in a build pipeline.
- Core Web Vitals as Google actually uses them are field data, collected from
real Chrome users who opted in, and judged at the 75th percentile.
- That percentile choice matters: it means three quarters of your visits must
be good. Your own fast laptop on office fibre tells you nothing.
- Accessibility works through the accessibility tree, a second structure
the browser builds beside the DOM, where each element has a role, a name, a
state and a value.
- A screen reader reads that tree, not your pixels. A
<button> arrives with
role “button” and is focusable and clickable by keyboard, all for free.
- A
<div onclick=...> styled to look like a button arrives with no role, no
name, no keyboard focus and no Enter key handling. That is why the tag you
choose is the single biggest accessibility decision you make.
TECHNICAL35.15.5 the engineer’s version#
- The testing pyramid was described by Mike Cohn in his 2009 book “Succeeding
with Agile”. A common rule of thumb is roughly 70 percent unit, 20 percent
integration, 10 percent end-to-end, though the exact split is convention, not
a finding.
- Kent C. Dodds’ “testing trophy” (2018) is the main counter-argument: it puts
the most weight on integration tests, on the grounds that they catch the most
real bugs per minute spent. Experts genuinely disagree here; both shapes ship
good software.
| Level |
Tools |
Typical time |
| Unit |
Vitest, Jest, pytest, JUnit 5 |
1 to 50 ms |
| Integration |
Testing Library, supertest |
50 to 500 ms |
| Container-backed |
Testcontainers, MSW |
0.5 to 5 s |
| End-to-end |
Playwright, Cypress, Selenium |
5 to 60 s |
- Dates: Selenium began in 2004 with Jason Huggins at ThoughtWorks; WebDriver
became a W3C Recommendation in June 2018. Cypress launched in 2017.
Playwright came from Microsoft in 2020, written by former Puppeteer authors.
Jest came from Facebook in 2014; Vitest arrived in 2021 alongside Vite.
- Coverage percentages measure lines executed, not behaviour verified. A suite
at 100 percent coverage with no assertions proves nothing. Use coverage to
find untested files, never as a target to hit.
- Core Web Vitals thresholds, current as of 2026. A page passes only if all
three are in the good band at the 75th percentile, split by mobile and
desktop.
| Metric |
Good |
Needs work |
Poor |
| LCP |
2.5 s or less |
2.5 to 4.0 s |
over 4.0 s |
| INP |
200 ms or less |
200 to 500 ms |
over 500 ms |
| CLS |
0.1 or less |
0.1 to 0.25 |
over 0.25 |
- History: LCP and CLS became Core Web Vitals in May 2020. INP was announced as
a replacement for First Input Delay in May 2023 and became a Core Web Vital on
12 March 2024. FID was retired because it measured only the delay before the
first handler ran, not how long the page took to respond.
- Field data comes from the Chrome User Experience Report, CrUX, which publishes
monthly aggregates. PageSpeed Insights shows CrUX and Lighthouse side by side,
and they routinely disagree, which confuses people. They are measuring
different populations.
- Lighthouse performance score weights, current version:
| Metric |
Weight |
| Total Blocking Time |
30 percent |
| Largest Contentful Paint |
25 percent |
| Cumulative Layout Shift |
25 percent |
| First Contentful Paint |
10 percent |
| Speed Index |
10 percent |
- Score bands are 0 to 49 poor, 50 to 89 needs improvement, 90 to 100 good.
Lighthouse applies simulated mobile throttling by default, which is why a
local score is lower than what your machine feels like.
- Note that INP is not in the Lighthouse score. Lighthouse uses Total Blocking
Time as its lab proxy, because a single automated load performs no
interactions. Fixing TBT usually fixes INP, but not always.
- Other measurement tools: WebPageTest for filmstrips and multi-location runs,
the Chrome DevTools Performance panel for flame charts, and the
web-vitals
JavaScript library to report real user metrics to your own backend.
- Accessibility is governed by WCAG, the Web Content Accessibility Guidelines.
WCAG 2.0 became a W3C Recommendation in December 2008, 2.1 in June 2018, and
WCAG 2.2 on 5 October 2023. WCAG 3.0 remains a working draft.
- Conformance levels are A, AA and AAA. AA is the level almost all law and
procurement references. AAA is not expected for whole sites.
- The genuinely non-negotiable basics, each tied to its success criterion:
| Basic |
Criterion |
Concrete rule |
| Semantic HTML |
1.3.1, 4.1.2 |
real button, nav, h1 to h6 |
| Alt text |
1.1.1 |
describe it, or empty alt |
| Keyboard |
2.1.1, 2.1.2 |
everything reachable, no traps |
| Contrast |
1.4.3, 1.4.11 |
4.5:1 text, 3:1 large and UI |
| Focus visible |
2.4.7, 2.4.11 |
a clear ring, never obscured |
| Labels |
3.3.2, 1.3.5 |
every input has a <label for> |
- Contrast detail: 4.5 to 1 for normal text, 3 to 1 for large text, meaning
at least 18 point or 14 point bold, which is about 24 CSS pixels or 18.66
pixels bold. AAA raises those to 7 to 1 and 4.5 to 1.
- New in WCAG 2.2 and often missed: 2.4.11 Focus Not Obscured, 2.5.8 Target
Size Minimum at 24 by 24 CSS pixels, and 3.3.8 Accessible Authentication,
which forbids requiring a cognitive test such as transcribing a code by hand
with no paste allowed.
- The first rule of ARIA, from the W3C ARIA Authoring Practices Guide: do not
use ARIA if a native HTML element with the needed semantics exists. Incorrect
ARIA is measurably worse than none, because it overrides what the browser
already knew.
- Never remove a focus outline without replacing it.
outline: none with no
substitute is the single most common accessibility regression in modern CSS.
- Automated tools find only part of the problem. Deque, the maker of axe,
reports that automated testing catches roughly a third to a half of issues
depending on the ruleset; the figure varies by study, so treat it as
approximate. Nothing replaces a keyboard-only pass and a screen reader pass.
- Law, briefly and with dates, because it drives budgets: Section 508 in the
United States was refreshed in January 2017 to reference WCAG 2.0 AA. The
European Accessibility Act’s requirements applied from 28 June 2025. The
United States Department of Justice ADA Title II rule of April 2024 requires
WCAG 2.1 AA for state and local government sites, phasing in from 2026.
- Tools:
axe DevTools and @axe-core/playwright for automated checks,
Lighthouse’s accessibility category, WAVE, and the real screen readers:
NVDA and JAWS on Windows, VoiceOver on macOS and iOS, TalkBack on Android.
WORDS35.15.6 remember these#
- Unit test — checks one small piece alone — an isolated assertion over a single
function or module with no external dependencies.
- Integration test — checks two real parts together — a test exercising
collaborating components, often with a real database in a container.
- End-to-end test — drives the app like a person — a full-stack test through a
real browser via an automation protocol.
- Flaky test — passes and fails without changes — a non-deterministic test,
usually a timing or ordering race, that destroys trust in the suite.
- LCP — how long until the main thing appears — Largest Contentful Paint, good
at 2.5 seconds or less at the 75th percentile.
- INP — how long the page takes to answer a tap — Interaction to Next Paint,
good at 200 milliseconds or less.
- CLS — how much the page jumps about — Cumulative Layout Shift, unitless, good
at 0.1 or less.
- Lab data — one measured run — a synthetic test under fixed throttled
conditions, repeatable and good for regression gates.
- Field data — what real users got — real user monitoring aggregated at a
percentile, such as the Chrome User Experience Report.
- Accessibility tree — what a screen reader reads — a parallel tree of roles,
names, states and values derived from the DOM.
- WCAG AA — the level the law usually means — the middle conformance level of
the Web Content Accessibility Guidelines, currently 2.2.
- Semantic HTML — using the tag that means the thing — elements chosen for
meaning, giving roles and keyboard behaviour without extra code.
35.16 A realistic learning path#
PLAIN35.16.1 in simple words#
- You have almost certainly seen a picture called a 180-day web development
roadmap. It usually says something close to this.
| Days |
Topic |
| 1 to 25 |
HTML and CSS |
| 26 to 55 |
JavaScript |
| 55 to 60 |
responsive design |
| 61 to 75 |
React |
| 76 to 105 |
Node and MongoDB |
| 106 to 125 |
GitHub and APIs |
| 126 to 180 |
cloud and projects |
- We are going to take that roadmap seriously, because it is not stupid. Parts
of it are genuinely right, and it has helped a lot of people start.
- What it gets right: the big order. Structure first, then styling, then the
language, then a framework, then a server. You cannot begin at React.
- What it also gets right: giving JavaScript more time than HTML and CSS. That
ratio is correct, and most beginners get it backwards.
- And it ends with projects and putting things online, which is exactly where a
learning path should end.
- Now the three things it gets wrong, said kindly, because each one costs
beginners real months.
- First, it teaches MongoDB before SQL, and in fact never teaches SQL at all.
That is the wrong way round.
- Second, it puts GitHub on day 106. Version control belongs on day one, before
your first file, not after your first framework.
- Third, and biggest: days are not the unit. “25 days of HTML” measures how long
you sat there. It does not measure whether you can build a page.
- The honest replacement is a list of stages, each defined by a thing you have
built and can show, and each finished when the thing works.
- Some people reach stage four in two months. Some take eight. Both are normal
and neither number tells you anything about how good they will be.
PLAIN35.16.2 a picture in your head#
- Think about learning to cook rather than learning to watch cooking.
- A day-count roadmap is a timetable that says “week three: sauces”. You attend
week three. At the end of it you have watched sauces being made.
- A stage-based path says “you are finished with sauces when you have served
four different sauces to someone who ate them and told you the truth”.
- The second one cannot be faked. There is no way to have served the sauce
without having made the sauce.
- It also self-corrects. If your sauce splits, you go back and learn about
emulsions because you need to, and that lesson sticks permanently, in a way no
scheduled lecture on emulsions ever does.
- And notice what the timetable never mentions: cleaning as you go, knife
safety, tasting. Those are not week seven. They are every single day.
- Version control, testing and accessibility are the cleaning as you go.
Where this comparison breaks:
- Cooking gives you an answer in twenty minutes. Software gives you a bug that
takes two days, and during those two days it feels like you are learning
nothing. You are learning the most.
- Also, a chef can taste the dish. You cannot tell whether your code is good by
looking at it. That is what other people’s review and real users are for.
PLAIN35.16.3 a worked example#
- Here is the replacement. Eight stages. Each one names what to learn, what to
build, and the specific signal that you are finished with it.
| Stage |
Learn |
Build |
Done when |
| 0 Tools |
git, shell, editor |
a repo of notes |
you can undo a commit |
| 1 Pages |
HTML, CSS, layout |
3 real pages |
it works at 320px |
| 2 Language |
JavaScript, DOM |
a to-do, no library |
you explain the loop |
| 3 Data |
SQL, schema design |
a 4-table schema |
you write a join |
| 4 Server |
HTTP, one backend |
a JSON API + login |
passwords are hashed |
| 5 Client |
React or Svelte |
rebuild stage 2 |
you know what it saved |
| 6 Ship |
deploy, CI, logs |
put it online |
a stranger used it |
| 7 Depth |
tests, a11y, speed |
improve stage 6 |
Lighthouse over 90 |
- Stage 0, tools, roughly one week. Learn
git init, add, commit, push,
branch, merge, and how to read git log. Learn cd, ls, grep,
and how to kill a stuck process.
- Build: a repository where you keep your notes from this book, committed daily.
The point is not the notes. The point is that committing becomes automatic
before you have anything valuable to lose.
- Done when: you can deliberately make a mess, and get out of it with
git restore, git reset or git revert, without asking anyone.
- Stage 1, pages, roughly three to five weeks. HTML semantics, the box model,
flexbox, grid, and media queries. Responsive is not a separate topic; it is
how you write CSS from the first line.
- Build three pages that are not tutorials: a page about something you actually
care about, a copy of a real site’s layout, and a form-heavy page.
- Done when: you resize the browser to 320 pixels wide and nothing overlaps,
nothing overflows, and you did not need to look up flexbox.
- Stage 2, the language, roughly six to eight weeks, and this is the stage
people rush. Variables, types, functions, arrays, objects, closures,
this,
promises, async/await, modules, and the DOM API.
- Build a to-do application with no framework at all. Then a small game. Then
something that calls a public API and handles the error case properly.
- Done when: you can predict the output order of a piece of asynchronous code
on paper before running it, and you are right.
- Stage 3, data, roughly three weeks, and this is the stage the popular roadmap
skips entirely. Learn SQL before you learn any other database.
- Build a schema for something real with four or five related tables: users,
posts, comments, tags, and a join table. Load it with a few thousand rows.
- Done when: you can write a query with two joins, a
GROUP BY and a HAVING
clause, and explain why you added an index.
- Stage 4, the server, roughly four to six weeks. Pick one backend and stay
there. Node with Express or Fastify, or Python with FastAPI or Django.
- Build a JSON API with real registration and login, sessions or tokens done
properly, input validation, and correct status codes.
- Done when: passwords are stored with bcrypt or Argon2id, a wrong password
gives 401, a valid user hitting somebody else’s record gives 403, and you can
say why those two are different.
- Stage 5, the client framework, roughly four weeks. Now, and only now, learn
React, Svelte or Vue.
- Build: rebuild your stage 2 to-do app in the framework, against your stage 4
API. Same app, second time.
- Done when: you can name three specific things the framework did for you that
you had written by hand, and one thing it made harder.
- Stage 6, shipping, roughly two weeks. A domain, HTTPS, environment variables,
a build pipeline, a real deploy, and logs you can read at 2 a.m.
- Done when: someone you have never met has used the thing and told you
something about it. That single event teaches more than the previous month.
- Stage 7, depth, ongoing forever. Tests, accessibility, performance, security
and reading other people’s code.
- Done when: nothing. This stage does not end, and that is the job.
PLAIN35.16.4 what is really happening inside#
- Why SQL before MongoDB, concretely. A relational schema forces you to decide,
on day one, what your data actually is and how the pieces relate.
- That decision is the hard part of software, and doing it early with a database
that argues back is how you learn it.
- A document database lets you postpone every one of those decisions. That feels
faster and it genuinely is faster, for about six weeks.
- Then you need to know which users commented on which posts, and you discover
you now have to write the join yourself, in application code, without
transactions, at three in the morning.
- There is nothing wrong with MongoDB. It is a good database for documents that
really are documents. It is a bad first database because it does not teach
modelling, and modelling is the transferable skill.
- Also, in almost every job you will apply for, the database is PostgreSQL,
MySQL or SQL Server. SQL is asked about in interviews. Mongo query syntax
almost never is.
- Why git on day one. Every hour you spend without version control is an hour
where the worst outcome is losing everything and the second worst is being
afraid to try things.
- Version control is not an advanced topic. It is the thing that makes learning
safe, because you can break anything and get back.
- It is also, bluntly, the one skill every single job requires. Learning it on
day 106 means 105 days of work you cannot show anyone.
- And be precise about the words: git is the version control program, made
by Linus Torvalds in 2005. GitHub is one website that hosts git
repositories, founded in 2008 and bought by Microsoft in 2018. Learn git.
GitHub takes an afternoon once you know git.
- Why days are the wrong unit. Two people both “do 25 days of HTML”. One
watches videos for two hours a day. One builds and breaks pages for four.
They are not in the same place, and the calendar cannot tell them apart.
- Worse, a day count creates a false finish line. Day 26 arrives, you move to
JavaScript because the plan says so, and you carry a shaky understanding of
the box model into everything that follows.
- A build target has no such problem. The page either survives 320 pixels or it
does not. You cannot argue with it and you cannot schedule past it.
- What the popular roadmap leaves out entirely, and should not: testing,
accessibility, security, debugging, and reading code you did not write.
- Those are not advanced. Debugging in particular is most of the job, and no
roadmap ever allocates a single day to it.
- One more honest thing. You will feel, somewhere around stage 2, that everyone
else understands this and you do not. That feeling is not evidence. It is
what learning something genuinely hard feels like from the inside, and it
arrives for everyone, including the people you are comparing yourself to.
TECHNICAL35.16.5 the engineer’s version#
- Realistic hour costs, from teaching experience rather than from a course
listing. Treat these as approximate and personal.
| Stage |
Focused hours |
At 15 h/week |
| 0 Tools |
10 to 20 |
about 1 week |
| 1 Pages |
60 to 100 |
4 to 7 weeks |
| 2 Language |
120 to 200 |
8 to 13 weeks |
| 3 Data |
40 to 70 |
3 to 5 weeks |
| 4 Server |
80 to 120 |
5 to 8 weeks |
| 5 Client |
50 to 80 |
3 to 5 weeks |
| 6 Ship |
20 to 40 |
1 to 3 weeks |
- That totals roughly 380 to 630 focused hours to be employable at a junior
level, which at a genuine 15 hours a week is about 26 to 42 weeks.
- “180 days” at 3 hours a day is 540 hours, so the popular roadmap’s total is
not unreasonable. Its problem is the distribution and the ordering, not the
size.
- The distribution the roadmap gets wrong, in numbers: it gives 25 days to HTML
and CSS and 15 days to React, while giving zero to SQL, zero to testing and
zero to debugging.
- A portfolio that actually gets interviews contains three items, not fifteen:
| Project |
What it must prove |
| A full-stack app |
auth, a real schema, deploy |
| A focused tool |
you can finish something small |
| A contribution |
you can read others’ code |
- Concrete quality bar for the full-stack app: a live URL, a README with setup
steps that work on a clean machine, at least a handful of tests, migrations
rather than a hand-edited database, and no secrets in the repository.
- Reading the git history of a candidate’s project tells an interviewer more
than the project does. Many small commits with real messages beat one commit
called “initial commit” containing 40,000 lines.
- Order of technologies, with the reasoning stated as a rule: learn the thing
that is underneath before the thing that abstracts it, unless the underneath
thing is genuinely obsolete.
- So: HTML and CSS before Tailwind. JavaScript before React. SQL before an ORM.
HTTP before a framework’s router. Git before any hosting website.
- The exception is real. Nobody should learn assembly before Python, or
Objective-C before Swift. The rule applies one layer down, not all the way.
- TypeScript timing: learn it after stage 2, not during. Types over a language
you cannot yet read are noise. Once you can read JavaScript, TypeScript takes
about a week and pays for itself immediately.
- On tutorials, stated plainly: following a tutorial produces the feeling of
competence without the substance, because every decision was already made
for you. The cure is to build the same thing again, from an empty folder,
with the tutorial closed.
- On AI assistants, separating fact from claim. Established fact: they generate
working code for common tasks quickly and are excellent at explaining
unfamiliar code. Active research: how much they change long-term skill
acquisition in beginners; the studies are early and mixed. Marketing claim:
that they remove the need to understand the fundamentals. If you cannot read
the answer, you cannot tell when it is wrong, and it is confidently wrong
often enough to matter.
- A defensible use during learning: write it yourself first, then ask for a
critique. That order keeps the skill and gains the speed.
- What does not decide your career: which framework you chose. React, Vue,
Svelte and Angular all teach the same underlying ideas of state, components
and reconciliation, and moving between them takes a fortnight.
- What does decide it: whether you can take a vague problem, model the data,
build something that works, and explain the trade-offs you made.
WORDS35.16.6 remember these#
- Roadmap — a picture of what to learn next — an ordered curriculum, useful as a
sequence and misleading as a schedule.
- Stage gate — a built thing that proves readiness — an objective completion
criterion replacing elapsed time as the unit of progress.
- Tutorial trap — following along and learning nothing — passive completion
without independent recall or decision-making.
- Portfolio — the few things you can show — a small set of deployed, documented,
version-controlled projects with real commit history.
- git — the version control program — a distributed content-addressable version
control system, Linus Torvalds, 2005.
- GitHub — one website that stores git projects — a hosting and collaboration
platform for git repositories, founded 2008.
- Full stack — you can do both halves — competence across the browser client,
the server application and its datastore.
- Junior-ready — good enough to be paid to learn — able to ship a reviewed
feature end to end with guidance, not to work unsupervised.
35.98 Common wrong ideas#
- Wrong: HTML is a programming language and knowing it means you can program.
Right: HTML is a markup language. It describes structure and has no
variables, no conditions and no loops. JavaScript is the only one of the three
web languages that can compute anything.
- Wrong:
!important fixes a specificity problem. Right: it hides one. It wins
by leaving the normal cascade entirely, so the next person needs a second
!important to override it, and after four rounds nobody can predict which
rule applies. The real fix is a lower-specificity selector.
- Wrong:
async and await make JavaScript run on several threads. Right: your
JavaScript still runs on exactly one thread. await yields the thread while
waiting, so other queued work can run. Parallelism needs Web Workers.
- Wrong: the virtual DOM is faster than touching the DOM. Right: a hand-written
direct DOM update is always faster than a diff plus the same update. The
virtual DOM buys predictable performance from declarative code, not raw
speed, and its own authors have said so.
- Wrong: you need a framework to build a website. Right: a framework earns its
place when many pieces of shared state must stay in sync. A content site,
a form, a landing page and a blog are all better plain, and ship faster.
- Wrong: MongoDB should be your first database because it scales. Right:
PostgreSQL handles the workload of almost every application ever written, has
transactions and joins, and has a
jsonb column type when you genuinely need
flexible documents. Learn relational modelling first; it transfers everywhere.
- Wrong: hashing a password with SHA-256 is secure because SHA-256 is a strong
hash. Right: SHA-256 is strong and fast, and fast is exactly the wrong
property. A GPU tries billions of SHA-256 guesses a second. Use bcrypt,
scrypt or Argon2id with a per-user salt, tuned to take about 100 milliseconds.
- Wrong: JSON Web Tokens are more secure than sessions. Right: they are a
different trade, not a better one. They remove the server lookup and, with it,
the ability to revoke. Sessions in an
HttpOnly cookie are the safer default,
and a JWT in localStorage is stolen by the first cross-site scripting bug.
- Wrong: microservices scale better, so start with them. Right: they let teams
deploy independently, which is an organizational benefit with a technical
cost. Every in-process call becomes a network call that can fail, and one
ACID transaction becomes a saga. Start with a modular monolith and split when
a specific team or scaling boundary forces it.
- Wrong: accessibility is a polish step near launch, and mostly means adding
ARIA attributes. Right: it is decided by the tags you choose on the first
day. A real
<button> arrives with a role, keyboard focus and Enter
handling; a styled <div> arrives with none. The W3C’s own first rule of
ARIA is not to use ARIA when a native element already does the job.
35.99 Chapter summary in 20 lines#
- A web application runs in two places: a frontend on the user’s device that you
do not control, and a backend on a server that you do. Never trust the first.
- HTML gives structure, CSS gives presentation, JavaScript gives behaviour, and
only JavaScript is a programming language.
- Semantic HTML tags are not decoration. They set the role, keyboard behaviour
and screen-reader name of everything on the page, for free.
- CSS resolves conflicts by origin, then specificity counted as inline, id,
class and element, then source order. The box model, flexbox and grid replace
guessing with rules.
- JavaScript has one thread, a call stack, a task queue and a microtask queue.
Microtasks drain completely before the next task, which is why promise
callbacks beat
setTimeout(0) every time.
- The browser parses HTML into the DOM and CSS into the CSSOM, combines them
into a render tree, then runs layout, paint and composite. Changing geometry
forces a reflow; changing transform and opacity does not.
- The browser is a platform in its own right: storage, workers, fetch, canvas,
WebGL and WebAssembly, all inside a same-origin sandbox that CORS relaxes
deliberately and explicitly.
- A frontend framework’s real product is keeping the screen in agreement with
the state. Virtual DOM diffing, fine-grained signals and compilation are three
answers to that one problem.
- Build tooling exists because browsers want a few small optimized files while
humans want many readable ones: bundling, transpiling, minifying, tree shaking
and content-hashed names for caching.
- A backend program listens on a port, parses HTTP, routes the request through
middleware to a handler, talks to a database, and serializes a reply. Every
framework in every language is that same loop.
- Relational databases store facts once, in typed tables, joined on keys, with
B+ tree indexes turning scans into lookups and ACID transactions making
partial failure impossible.
- Choose a datastore by workload, not by fashion. PostgreSQL is the correct
default; Redis, Elasticsearch, Cassandra and Neo4j each answer a specific
question that PostgreSQL answers less well.
- An ORM removes repetitive code and adds the N+1 query trap. Use it for the
ordinary ninety percent, read the SQL it generates, and write SQL yourself
when the query is the point.
- Store passwords with a deliberately slow, salted hash: Argon2id or bcrypt,
never a bare fast hash, and never the password itself.
- Sessions keep state on the server and can be revoked instantly. Tokens keep
state with the client and scale without a lookup. Pick the trade knowingly,
and keep credentials in
HttpOnly cookies.
- OAuth 2.0 delegates authorization through an authorization code exchanged on
a back channel with PKCE; OpenID Connect adds the ID token that turns it into
a login system. Authentication is who you are; authorization is what you may
do, and RBAC keeps the second one auditable.
- Architecture is caching at the right layer, moving slow work to a queue,
choosing polling, server-sent events or WebSockets on merit, rate limiting
every public endpoint, and starting with a monolith you could split later.
- Mobile means choosing between native Swift or Kotlin, cross-platform React
Native or Flutter, and an installable web app, then living with a store
review on every release. An APK and an IPA are both ZIP archives, which is
Chapter 20’s lesson that the bytes, not the extension, define a file.
- Quality is measurable: a pyramid of fast unit tests under fewer browser
tests, LCP at 2.5 seconds, INP at 200 milliseconds, CLS at 0.1 measured at
the 75th percentile of real users, and WCAG 2.2 level AA as the floor rather
than the ambition.
- Learn in stages defined by what you have built, not in days spent: tools,
then pages, then the language, then SQL, then a server, then a framework,
then shipping, then depth forever.