Before React, before Node, before any of it: every browser has a JavaScript engine inside it. Chrome's is called V8. It can run a script and change the page.
On the right is a page, its script, and the document tree the script pokes. Click the button. Watch the tree change. That's the whole trick, and everything else is convenience on top of it.
The document tree is called the DOM: the browser's live, in-memory version of your HTML. Change the DOM, the screen changes.
// the script on this page button.addEventListener("click", () => { span.textContent = "clicked " + (++n) + "×"; });
One thread. If your code is busy, nothing else on the page can happen. Not clicks, not animations, not typing.
Try it. The spinner is a CSS animation the browser runs on its own. Click Block for 3 s, then immediately try the counter button.
This is real. That's a true busy loop, on the same thread this page uses. You are freezing the tab you're reading.
// "Block for 3 s" runs exactly this const end = Date.now() + 3000; while (Date.now() < end) {} // spin
fetch not freeze the page?A network request takes 300 ms. If JavaScript sat there waiting, every page would stutter on every request. It doesn't. Because slow things aren't done by JavaScript at all.
The browser has helpers (timers, network, the file picker) that run outside the thread. JS hands them a job and a function to call when it's done. That function waits in a queue. The event loop moves it onto the stack only when the stack is empty.
Place your bet. Run these three in order: log("A"), setTimeout(→ log("B"), 0), log("C"). What prints?
A C B. Even with a 0 ms delay, B goes to the queue and can only come back when the stack is empty, which is after C. Press run A · B · C to watch exactly that, then press the other buttons in any order.
await.A database call, a file read, a fetch, a timer: all of them are "hand the job to a helper, get called back later". async / await is just the polite way to write that hand-off.
Here's a todo list with no framework. Add an item. Toggle one. Delete one. It works. Now look at the code that makes it work: every change to the data needs a matching, hand-written change to the DOM. Forget one and the screen lies about the data.
At three features it's annoying. At thirty, with the same data shown in four places, it's the reason your app has bugs you can't reproduce.
Watch the counter of "DOM operations you wrote by hand" climb.
// on add: FIVE separate DOM writes, by hand const li = document.createElement("li"); li.textContent = text; // 1 li.append(makeToggle(), makeDelete()); // 2, 3 list.append(li); // 4 countEl.textContent = todos.length; // 5 // ...and "done" count, and empty-state, and...
React's whole pitch: stop writing DOM updates. Describe what the page should look like for the current data, and let React work out the edits.