The ways we currently schedule and perform work in applications are suboptimal. This talk takes a deep dive into how Javascript runtimes and browsers work, and how we can write code to make our app's do their work more intelligently. You'll walk away with deeper knowledge and appreciation for MicroTasks, MacroTasks, the call stack, and requestAnimationFrame, as well as a preview of Igniter, a kernel for the web.
2. Chris Thoburn
@runspired
Ember + Mobile
@IsleOfCode
This is me, I recently took a timeout to wander a desert pondering the true meaning of Javascript.
This is the face I make when wondering what Tom Dale eats for breakfast.
4. This is how it feels to conquer the world and deliver an app that runs smoothly. Pedal to the Metal.
5. But too often it turns out like this cyclist.
It turns out that it’s hard to build optimized, performance focused applications without access to system metal.
6. What is our metal?
And how much access do we have?
When I began focusing on performance, I began asking this question.
Don’t worry, today isn’t going to be full of array or hash optimizations, it’s not about algorithms, Int32Arrays, Buffers, or Binary data.
Today, we’re going to talk a lot about requestAnimationFrame.
7. Doing Work Smarter
And we’re going to talk a lot about doing work smarter with requestAnimationFrame.
If you’ve never heard of requestAnimationFrame, I suggest you google it once we’re off of conference wifi, you may also want to consider a new home that’s not this
island, or a rock.
8. How does JS, JS?
But this isn’t a talk to tell you that requestAnimationFrame exists and why you should use it. Today we’ll dive deeper into what it is, how it functions, and how you should
be using it.
10. For a lot of us, a stack trace such as this is our first experience with the call stack.
11. We entered the stack by calling aFunction, which called bFunction and so on until it hit the function that threw the error. Because invocation began with aFunction, the
stack, and thus the trace, lead back to aFunction.
12. The Callback Queue
This is usually called the “Event Queue”, but in order to not confuse it with actual events, or a concept we’ll introduce later called an “event frame”, we’re going skip on
calling it that.
13. setTimeout(fn, 0);
This function is fancy magic. We know that Javascript is single threaded and that we can bump some work down the line by wrapping it in this call.
14. setTimeout(fnC, 0);
setTimeout(fnB, 0);
setTimeout(fnA, 0);
setTimeout(fnD, 0);
setTimeout(fnE, 0);
We might even use it to delay a lot of work. We get that there’s some queue of functions to be invoked, and that doing this pushed execution of a function to the end of
that queue.
How does this work?
17. Now, bar doesn’t immediately go onto the queue, instead it goes into this strange land of Web APIs, where a timer in a separate process is going to deliver it back to us
at the right time in case we say had called setTimeout with a number other than 0.
18. A timer kicks off, and at the right time, bar is sent to back of the queue.
19. And when the all the work from foo and from any other callbacks in front of this one completes, bar is invoked and becomes the start of a new call stack.
There are some concepts here for debugging asynchronous code “stack stitching” “async trace”
23. Ooof, there’s a lot going on in this picture, where to start.
For one, I’ve slyly renamed the callback queue to the “MacroTask” Queue. This just means these are higher level, but lower priority “jobs” or “tasks” that need to be
done. We’ve got a few waiting.
Off to the left, we’ve added a new purple box which we’ve called the Next AnimationFrame.
24. this time, foo calls requestAnimationFrame instead of setTimeout, and our web APIs see this as a new FrameTask.
25. and they schedule the job (baz) into the next AnimationFrame, a separate callback queue.
26. foo calls requestAnimationFrame several times, and each time a new job is pushed into the next AnimationFrame callback queue.
How will this flush?
27. I. Finish The Call Stack
II. Check if should flush AnimationFrame
I. Flush AnimationFrame
III. Do next MacroTask from MacroTask Queue
(repeat)
First, we complete the current call stack (all the work begun with foo).
Next, we check if it’s time to flush the AnimationFrame queue, and if so, we flush it.
Else, we do the next MacroTask, and repeat. We finish the callstack, check if we should flush the AnimationFrame, etc.
Let’s see this in action.
28.
29.
30.
31.
32.
33.
34. What happens if you requestAnimationFrame
during an Animation Frame flush?
if you’ve ever used raf, you already know the answer, but let’s see this quickly too.
40. The Window.requestAnimationFrame() method tells
the browser that you wish to perform an animation
and requests that the browser call a specified function
to update an animation before the next repaint. The
method takes as an argument a callback to be
invoked before the repaint.
https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame
41. If you’ve ever dug into Chrome’s “performance tuning” documentation, you may have seen this diagram.
43. raf flushes ~ every (1000 / 60 - X) ms
Where X is the amount of time spent in the previous AnimationFrame flush. This means that if we take 10ms to flush, the next flush will begin only a few milliseconds
later.
44. Style is separate
AnimationFrame’s budge will adjust
Here, we’re doing JS and Layout outside of the AnimationFrame, but it’s legal to do it within it as well.
50. FRP style updates: this is a snippet from Hammer.js 3.0 which is currently under active development.
Here, we don’t schedule work into raf, we use raf to poll new state and flush it.
51. This is a snippet from the next version of smoke-and-mirrors. This is a module that lets you add and remove scroll event handlers for an element.
52. But instead of binding an event to scroll, it passively polls the element’s offset and triggers the callback when it has changed.
54. A Better IntersectionObserver
radar, within smoke-and-mirrors, is basically a richer IntersectionObserver, I’ve been considering turning it into a polyfill + additional features.
55. In poll mechanism for watching scroll, you may have noticed that I was flushing the callbacks inside the success callback of a resolved promise. This begs a question.
56. Promises
In Ember, we use a lot of promises for managing asynchronous behavior. How do they inter operate with setTimeout and raf?
57.
58. We flushed our promise work before the console printed the return from our function. I promise you this is actually asynchronous, but don’t worry, I’ll explain.
61. I. Finish The Call Stack
II. Flush MicroTask Queue
III. Check if should flush Render MacroTasks
I. Flush Render MacroTasks
IV. Do next MacroTask from Event Queue
(repeat)
A fuller picture
63. A Kernel for the Web
So what’s this about a kernel for the web?
We want to get down to the system metal.
64. Houston, what’s our countdown at again?
We have a timing problem.
You do not know is the next macro task scheduled via setTimeout will trigger before or after the next AnimationFrame flush.
back burner (ember.run) flushes with setTimeout.
Ember’s rendering engine flushes by scheduling into backburner’s render queue.
This means, we do not know if our next render is before or after the next AnimationFrame flush.
65. Forced Layouts are a timing problem.
Layout is a lot like a computed promise. Various events and actions will invalidate it, but it isn’t recomputed until it’s requested.
Forced layouts happen when our JS code needs to read a layout related value and our layout is in an invalid state.
66. JS => L => JS => L => JS => L => Paint => Composite
L: Layout
FL: Forced Layout
JS: Javascript Logic (app code)
P: Paint
C: Composite
This is what the default state for most Ember apps is, multiple extra layouts
Tying Ember’s render to setTimeout causes extra layouts.
67. JS => L => JS => FL =>
JS => L => JS => FL =>
JS => L => Paint => Composite
L: Layout
FL: Forced Layout
JS: Javascript Logic (app code)
P: Paint
C: Composite
Each time we did layout, we might also have needed to measure or alter something in the DOM, perhaps checking or modifying a class name, or maybe we wanted to
know the new dimensions or location of an object.
Now we additionally have multiple forced layouts to go with our extra layouts.
69. We do a lot of unnecessary work because of this.
I. (multiple) forced layouts
II. extraneous render flushes / diffs
III. misaligned read/write operations
IV. not all mutations and reads are equal
70. requestAnimationFrame is not necessarily better
you can still force layout
you want to batch your DOM reads and your DOM writes
not all DOM writes are created equal.
Abusing Frame MacroTasks can be as choppy and risky as setTimeout
if we aren’t organized.
Animation Frame’s flush stops the world,
but we do not know when.
71. Scheduling work via MacroTasks is slow
and error prone.
setTimeout often takes 4-5ms to flush
we have very poor guarantees of when
72. Scheduling work via MicroTasks is fast
but introduces order-of-operations issues.
We don’t know the order in which promises will be flushed.
We can’t control batching reads and writes.
Doing DOM work in a promise callback can quickly lead to Forced Layouts
73. Timing Implications
- our app is doing extra work
- the browser is doing extra work
- we’re increasing the likelihood of a
minor or a major GC event.
- we don’t know if work we
scheduled in RAF is happening
before or after the render we care
about.
74. We are even further away from the “Metal” now.
(not that we were very good at these things before either)
The frameworks we are building over have abstracted
rendering, in the process, we have lost control we need.
75. Major and Minor Garbage Collection events
stop the world and have unpredictable timing, but they
do have predictable causes!
76. What we really want, is a way to schedule work
to happen at the optimal time for what it is.
77. Igniter
I want to introduce you to a project I’ve been building called Igniter.
78. Event Frame Render Frame Measure Frame Idle Frame
- sync
- actions
- cleanup
- render
- afterRender
- cleanup
- destroy (legacy)
- measure
- affect
- gc
- query
schedule(‘sync’, () => {});
Why do we defer work, debounce, throttle, schedule?
- "do it at render"
- "do it in the right order"
- "do it at a better time"
- "do it later, just not now"
- "do this only once"
Igniter tries to answer these questions.
79. Event Frame
- sync
- actions
- cleanup
schedule(‘sync’, () => {});
EventFrame flushes as a micro task, and represents work that should be done asynchronously but immediately and in a better order.
80. Render Frame
- render
- afterRender
- cleanup
- destroy (legacy)
Render flushes within RAF, specifically at the beginning of RAF. This lets us avoid extra layouts and extra forced layouts that were present in the setTimeout version.
81. Measure Frame
- measure
- affect
Measure also flushes within raf, but is guaranteed to flush after render. It itself is separated into “measure” and “affect”, think “read” and “write”.
82. Idle Frame
- gc
- query
if you know that an operation is going to cause a significant GC and can be deferred, schedule it into the idle frame
85. Primary Advantages
I. Align work correctly
II. Avoid duplicated App work
III. Ease the Browser’s Workload
IV. Meaningful Stack Traces
V. Easier Experimentation