Saturday, August 23, 2025

"You Don't Know JS: Async & Performance" Kyle Simpson

 


Book notes:

“setTimeout(…) timers may not fire with perfect temporal accuracy. You’re guaranteed (roughly speaking) that your callback won’t fire before the time interval you specify, but it can happen at or after that time, depending on the state of the event queue.”

“Parallel Threading. It’s very common to conflate the terms “async” and “parallel,” but they are actually quite different. Remember, async is about the gap between now and later. But parallel is about things being able to occur simultaneously.”

“The most common tools for parallel computing are processes and threads. Processes and threads execute independently and may execute simultaneously: on separate processors, or even separate com-puters, but multiple threads can share the memory of a single process.”

“threaded programming is very tricky, because if you don’t take special steps to prevent this kind of interruption/interleaving from happening, you can get very surprising, nondeterministic behavior that frequently leads to headaches.”

“JavaScript never shares data across threads, which means that level of nondeterminism isn’t a concern.”

“setTimeout(…0) is not technically inserting an item directly onto the event loop queue. The timer will insert the event at its next opportunity. For example, two subsequent setTime out (… 0) calls would not be strictly guaranteed to be processed in call order, so it is possible to see various conditions like timer drift where the ordering of such events isn’t predictable. In Nodejs, a similar approach is process.next Tick(…). Despite how convenient (and usually more performant) it would be, there’s not a single direct way (at least yet) across all environments to ensure async event ordering.”

“A JavaScript program is (practically) always broken up into two or more chunks, where the first chunk runs now and the next chunk runs later, in response to an event. Even though the program is executed chunk-by-chunk, all of them share the same access to the program scope and state, so each modification to state is made on top of the previous state. Whenever there are events to run, the event loop runs until the queue is empty. Each iteration of the event loop is a tick. User inter-action, 10, and timers enqueue events on the event queue. At any given moment, only one event can be processed from the queue at a time. While an event is executing, it can directly or indirectly cause one or more subsequent events. Concurrency is when two or more chains of events interleave over time, such that from a high-level perspective, they appear to be running simultaneously (even though at any given moment only one event is being processed). It’s often necessary to do some form of interaction coordination between these concurrent “processes” (as distinct from operating system processes), for instance to ensure ordering or to prevent race conditions. These “processes” can also cooperate by breaking themselves into smaller chunks and to allow other “process” interleaving.”

“it was decided that the way to recognize a Promise (or something that behaves like a Promise would be to define something called a thenable as any object or function which has a then (…) method on it. It is assumed that any such value is a Promise-conforming thenable.”

“you should never rely on anything about the ordering/scheduling of callbacks across Promises. In fact, a good practice is not to code in such a way where the ordering of multiple callbacks matters at all. Avoid that if you can.”

“fundamental principle that Promises are immutable once resolved.”

“If you pass an immediate, non-Promise, non-thenable value to Promise. resolve(…), you get a promise that’s fulfilled with that value. In this case, promises p1 and p2 will behave identically”

“The most natural form of error handling for most developers is the synchronous try…catch construct. Unfortunately, it’s synchronous-only, so it fails to help in async code patterns”

“Promise.all([ … ]) expects a single argument, an array, consisting generally of Promise instances. The promise returned from the Promise.all([… ]) call will receive a fulfillment message (msgs in this snippet) that is an array of all the fulfillment messages from the passed in promises, in the same order as specified (regardless of fulfillment order).”

“Technically, the array of values passed into Promise.all(l … ]) can include Promises, thenables, or even immediate values. Each value in the list is essentially passed through Promise. resolve(…) to make sure it’s a genuine Promise to be waited on, so an immediate value will just be normalized into a Promise for that value. If the array is empty, the main Promise is immediately fulfilled.”

“A race requires at least one “runner,” so if you pass an empty array, instead of immediately resolving, the main race([…]) Promise will never resolve.”

none ([ … ]) - This pattern is like all ([ … ]), but fulfillments and rejections are transposed. All Promises need to be rejected-rejections become the fulfillment values and vice versa. any ([ … ]) - This pattern is like all([ … ]), but it ignores any rejections, so only one needs to fulfill instead of all of them. first([ … ]) - This pattern is like a race with any ([ … ]), which means that it ignores any rejections and fulfills as soon as the first Promise fulfills. last([ … ]) - This pattern is like first([ … ]), but only the latest fulfillment wins.”

“The revealing constructor Promise(.) must be used with new, and must be provided a function callback, which is synchronously/ immediately called. This function is passed two function callbacks that act as resolution capabilities for the promise. We commonly label these resolve(…) and reject… )”

“reject(…) simply rejects the promise, but resolve(…) can either fulfill the promise or reject it, depending on what it’s passed. If resolve(…) is passed an immediate, non-Promise, non-thenable value, then the promise is fulfilled with that value. But if resolve(…) is passed a genuine Promise or thenable value, that value is unwrapped recursively, and whatever its final resolu-tion/state is will be adopted by the promise.”

“A shortcut for creating an already-rejected Promise is Promise. reject(… )”

“Promise.resolve(…) is usually used to create an already-fulfilled Promise”

“And remember, Promise. resolve(…) doesn’t do anything if what you pass is already a genuine Promise; it just returns the value directly.”

“Each Promise instance (not the Promise API namespace) has then (…) and catch(…) methods, which allow registering of fulfillment and rejection handlers for the Promise. Once the Promise is resolved, one or the other of these handlers will be called, but not both, and it will always be called asynchronously”

“then(…) and catch(…) also create and return a new promise, which can be used to express Promise chain flow control.”

“Promise.wrap(…) does not produce a Promise. It produces a function that will produce Promises.”

“The act of wrapping a callback-expecting function to be a Promise-aware function is sometimes referred to as “lifting” or “promisify-ing”.”

“As of ES6, the way to retrieve an iterator from an iterable is that the iterable must have a function on it, with the name being the special ES6 symbol value Symbol. iterator. When this function is called, it returns an iterator. Though not required, generally each call should return a fresh new iterator.”

“The purpose of yield-delegation is mostly code organization, and in that way is symmetrical with normal function calling.”

“The key benefit of generators related to async flow control is that the code inside a generator expresses a sequence of steps for the task in a naturally sync/sequential fashion. The trick is that we essentially hide potential asynchrony behind the yield keyword-moving the asynchrony to the code where the generator’s iterator is controlled. In other words, generators preserve a sequential, synchronous, blocking code pattern for async code, which lets our brains reason about the code much more naturally, addressing one of the two key drawbacks of callback-based async.”

“an environment like your browser can easily provide multiple instances of the JavaScript engine, each on its own thread, and let you run a different program in each thread. Each of those separate threaded pieces of your program is called a (Web) Worker. This type of parallelism is called task parallelism, as the emphasis is on splitting up chunks of your program to run in parallel.”

“To kill a Worker immediately from the program that created it, call terminate() on the Worker object”

“Inside the Worker, you do not have access to any of the main program’s resources. That means you cannot access any of its global variables, nor can you access the page’s DOM or other resources. Remember: it’s a totally separate thread. You can, however, perform network operations (Ajax, WebSockets) and set timers. Also, the Worker has access to its own copy of several important global variables/features, including navigator, loca tion, JSON, and applicationCache.”

“What are some common uses for Web Workers? • Processing intensive math calculations • Sorting large data sets. • Data operations (compression, audio analysis, image pixel manipulations, etc.) • High-traffic network communications”

“typed arrays like UintArray (see the ES6 & Beyond title of this series) are Transferables.”

“Shared Workers survive the termination of a port connection if other port connections are still alive, whereas dedicated Workers are terminated whenever the connection to their initiating program is terminated.”

“Single instruction, multiple data (SIMD) is a form of data parallel-ism, as contrasted to task parallelism with Web Workers, because the emphasis is not really on program logic chunks being parallel-ized, but rather multiple bits of data being processed in parallel.”

“asm.js is a label for a highly optimizable subset of the JavaScript lan-guage. By carefully avoiding certain mechanisms and patterns that are hard to optimize (garbage collection, coercion, etc.), asm.js-styled code can be recognized by the JS engine and given special attention with aggressive low-level optimizations.”

“Luckily, smart folks like John-David Dalton and Mathias Bynens do understand these concepts, and wrote a statistically sound benchmarking tool called Benchmark.js. So I can end the suspense by simply saying: “just use that tool.””

“What this boils down to is that testing not real code gives you not real results. If possible and practical, you should test actual real, nontrivial snippets of your code, and under as best of real conditions as you can actually hope to. Only then will the results you get have a chance to approximate reality.”

“jsPerf. It uses the Benchmark.js library we talked about earlier to run statistically accurate and reliable tests, and makes the test on an openly available URL that you can pass around to others.”

jsPerf.com is a fantastic website for crowdsourcing performance benchmark test”

No comments:

Post a Comment

"You Don't Know JS: Async & Performance" Kyle Simpson

  Concentration timer (promo) Book notes: “setTimeout(…) timers may not fire with perfect temporal accuracy. You’re guaranteed (roughly spe...