Videos Adventures with the Event Loop

Description

The event loop completely underpins everything that happens in the browser. Yet many developers know very little about it. This talk will help them better understand the nitty-gritty of what’s really going on when you create a Promise, add an event listener, or request an animation frame.

Chapters

  • Introduction and Speaker Intro 0:00
  • What is the Event Loop? 0:48
  • Speaker Background and Slide Availability 2:09
  • The Event Loop and How It Controls JavaScript Execution 2:48
  • The Event Loop's Simple Model: A Continuous While Loop 4:13
  • Event Loop and Rendering Pipeline Interaction 5:18
  • Multiple Task Queues in the Event Loop 7:14
  • Microtasks: Tasks Between Tasks (Promises) 9:54
  • Animation Frame Callbacks and 𝚛𝚎𝚚𝚞𝚎𝚜𝚝𝙰𝚗𝚒𝚖𝚊𝚝𝚒𝚘𝚗𝙵𝚛𝚊𝚖𝚎 13:04
  • Why 𝚛𝚎𝚚𝚞𝚎𝚜𝚝𝙰𝚗𝚒𝚖𝚊𝚝𝚒𝚘𝚗𝙵𝚛𝚊𝚖𝚎? Smooth Animations and DOM Manipulation 17:59
  • Key Takeaways: Blocking, Microtasks, and 𝚛𝚎𝚚𝚞𝚎𝚜𝚝𝙰𝚗𝚒𝚖𝚊𝚝𝚒𝚘𝚗𝙵𝚛𝚊𝚖𝚎 18:55

Transcript

These community-maintained transcripts may contain inaccuracies. Please submit any corrections on GitHub.

Introduction and Speaker Intro0:00

And right now, ladies and gentlemen, with the next topic,

What is the Event Loop?0:48

Adventures with the Event Loop by Erin Zimmer, Senior Engineer and Thought Leader from Shine Solutions Australia!

Hi. For some reason, I insist on using weird versions of OSes when I'm going to give talks in strange places. I don't. All right, so today we're going to talk about the event loop, which is a pretty important part of how JavaScript works.

No.

Maybe we're just going to look at this picture of a dinosaur. That's fun, too.

The screen's not in focus, so the clicker's not receiving the clicks.

Just this one?

All right. I'm going to stand behind the lectern and do this. It's going to be fun.

Speaker Background and Slide Availability2:09

All right. My name is Erin Zimmer. I work for a consulting company in Melbourne called Shine Solutions. I am both a Mozilla Tech Speaker and a Google Developer Expert. And I'm available on Twitter if you would like to talk about the event loop or things going wrong in tech or anything like that. And if you'd like to play along at home, these slides are available at event-loop.ez.codes. I will warn you though, I built all the demos using web components with HTML imports,

which seemed like a good idea at the time, and isn't supported in any browser except for Chrome up to the current one. Cool.

The Event Loop and How It Controls JavaScript Execution2:48

So, what even is the event loop? So, the event loop is basically the part of the browser that controls when your JavaScript runs. So, if we have a page that's being loaded over the web, this one is actually Twitter. When the browser receives the HTML, it's going to parse the HTML and it's going to build the Document Object Model. It's going to parse all the styles and build the CSS Object Model. And whenever it runs into a script tag, whether it's an inline script or an external script, it's going to stop what it's doing and immediately run that script. None of that has anything to do with the event loop. And if that was the only JavaScript that we ever ran on a page, this talk would be very short.

But as it turns out, most of the JavaScript that we write in that initial stage is actually setting up JavaScript that we're going to run later. So, it could be stuff like a timer, which is going to just run a bit of JavaScript in the future. It could be an event listener, which is going to wait for the user to perform some kind of interaction with the page and then run some JavaScript. Or it could be something like a network request where we're going to use fetch or XHR and fetch some resource over the network and then run a callback in response to that. So, the event loop controls when these things get executed.

The Event Loop's Simple Model: A Continuous While Loop4:13

So, this is what it looks like basically. We have these, oh no, I can't see my cursor.

All right. We have these inputs. So, we have like user interactions, we have timers,

and we have network requests. Each of these things is going to run and then once the timer comes back or the network request returns, then the browser is going to find the related callback and it's going to put it in this queue. And the event loop is going to control how this queue is run. At its simplest,

The event loop looks something like this. So, if we were to write it in pseudocode, it's just a while loop that runs forever. Each turn of the loop, it's going to grab the first task off the task queue and then it's going to run that task. If we looked at it with our example from before, we can have some events running,

they're going to go in the queue, and the event loop is just going to run them all in order. Cool. Does that make sense to everybody? Yep. Good.

Event Loop and Rendering Pipeline Interaction5:18

Because it turns out it's a little bit more complicated than that.

So the event loop runs in conjunction with the rendering pipeline. The rendering pipeline is responsible for displaying what you see on the screen. So it's going to take that DOM, it's going to take the CSSOM, it's going to build a render tree. It's going to turn that into a bitmap and display it on the screen. The rendering pipeline likes to run once every time the screen refreshes. So if you've got a 60 hertz monitor, that means it's going to run 60 times a second. So once every 16 milliseconds.

Our event loop works with our rendering pipeline like this.

The way that it works is that we can have events added to the queue. And then the event loop is going to run through those events, one at a time until it's time to render, when the 16 milliseconds is up, and then it's going to go over and do a render. Right, so it's going to run task, task, task and then run a render. The important thing though is that when you're running a task from the task queue, that task is always going to run start to finish. There's no interruption. You're guaranteed that if you have a JavaScript task, it will finish completion before anything else happens. So that means if we're in the middle of a task and the rendering pipeline wants to run, the rendering pipeline has to wait.

So if you're writing JavaScript, it's a good idea to not have extremely long running tasks. If you've got stuff that runs for more than 16 milliseconds, your browser's going to start dropping frames and your animations are going to look a bit janky. So if you do have long running tasks, either split them into smaller tasks, or you can use something like a web worker to move the processing off the main thread.

Multiple Task Queues in the Event Loop7:14

Okay. So now our event loop looks something like this. It's an infinite loop. For every turn of the loop, we're going to take a task off the task queue and run that task. Then if it's time to repaint, we're going to repaint. Everyone's cool with that? Good. Because it turns out, it's a little bit more complicated than that. See, an event loop can have one or more task queues.

This is just like the text out of the spec. And when I was preparing the talk, I thought what I'll do is I'll go and find some code in a browser, like an open source browser, and I'll look at the code and I'll find a browser that has one or more task queues and I'll show you an example of a real world browser that has one or more task queues. The thing is though, browsers are written in C++.

And I'm a front-end developer. And as far as I can tell, C++ is mostly just punctuation and the word delegate. And I don't really know what's going on. So instead, we're going to have a theoretical browser with multiple task queues. This one is actually the example from the spec. So I didn't just make it up, somebody else did. Okay. So in this browser, we have two queues. One queue for user actions and one queue for everything else. So if we have our user actions, they're going to go in one queue. And our timer and Wi-Fi actions will go in the other queue when they get there. This browser prioritizes user interactions. So as long as there's anything in the user interaction queue, it's going to run that before it runs anything in the other queue. And then the rendering pipeline just works the same as normal.

All right. It's not a complete free-for-all with these multiple queues though. There are a few rules. The queues can be executed in any order, so you can pick that yourself. Tasks in the same queue must be executed in the order they arrived. So that just means that the queues are queues. And finally, tasks from the same source have to go in the same queue. So in our case, all of the user interactions went in the same queue. If you're familiar with Node.js, it uses a model like this, and all of the timer events go in one queue, all of the network events go in another queue.

So now, our event loop looks like this.

It's an infinite loop. For every turn of the loop, we're going to pick a queue. Then we're going to take the first task off that queue. And we're going to run that task. Then, if it's time to repaint, we'll repaint. So is everybody good with that? Cool. Because it turns out, it's a little bit more complicated than that.

Microtasks: Tasks Between Tasks (Promises)9:54

We also have something called microtasks. Now a microtask is a task that happens between tasks, which is a bit confusing. They're originally created for dealing with mutation observers. So a mutation observer is an event that gets fired when a particular bit of DOM changes. So if something gets added to it or removed or shuffled around, whatever, you get this event fired. The thing with these events though is that when the DOM changes, you often want to do something else that's also going to change the DOM. So that means that you don't want the rendering pipeline to run in the middle, else your users are going to see that like halfway state between the two things. So microtasks give us a way to ensure that our

callback is going to run before the rendering pipeline runs again. Mutation observers don't get used that much. Something that you might be more familiar with though is promises. So all promise callbacks are microtasks. If you happen to really like microtasks, you can also just create them. Just 𝚠𝚒𝚗𝚍𝚘𝚠.𝚚𝚞𝚎𝚞𝚎𝙼𝚒𝚌𝚛𝚘𝚝𝚊𝚜𝚔 will give you a microtask. So what does that look like? So we have here our two queues from before and our microtask queue just on the beside the rendering pipeline there. And we have our inputs across the top, the ones we had from before, plus the microtask queue.

So say our user runs some tasks, sets some timers, whatever. And one of these callbacks creates a promise.

And that's going to go in the microtask queue.

Now as soon as any task finishes running, the microtask queue is going to run. Even if the rendering pipeline wanted to run, it has to wait until the microtask queue runs. If the microtask queue creates more microtasks, then

all of those are going to run as well. So as you can imagine, you can really stuff up the rendering if you have microtasks that generate microtasks. So if your promise callbacks have really long

callbacks that do a bunch of stuff and run for a really long time, it's really going to bugger up your rendering. So be careful with that.

Okay. So now our event loop looks like this. It's an infinite loop. For each turn of the loop, we're going to pick a queue. Then for each queue, we're going to take the first task off the queue, and we're going to run that task. Then as long as there are tasks in the microtask queue, we're going to run all of those. Then once all of the microtasks are done, then if it's time to repaint, we'll repaint.

Everybody cool with that? Cool. Because it turns out it's a little bit more complicated than that.

Animation Frame Callbacks and 𝚛𝚎𝚚𝚞𝚎𝚜𝚝𝙰𝚗𝚒𝚖𝚊𝚝𝚒𝚘𝚗𝙵𝚛𝚊𝚖𝚎13:04

We also have the animation frame callback queue. Now you can add something to the animation frame callback queue by calling 𝚛𝚎𝚚𝚞𝚎𝚜𝚝𝙰𝚗𝚒𝚖𝚊𝚝𝚒𝚘𝚗𝙵𝚛𝚊𝚖𝚎. Why would you want to do that? Well, you're probably not surprised to learn that it's to do with animating things.

So imagine you have a box, and you want to animate

it so that it moves across the screen in a sine kind of movement.

If we were to code that naively, we might write

something like this. So it's a loop that's going to run as long as the right side of the box hasn't reached the right side of the screen. For each turn of the loop, we're going to calculate how much time has passed since the animation began. And then we're going to pass that into some functions that are going to calculate the new x and y position of the box, and then set the box left and top to those new positions.

If you do this, what you get is this.

The box just appears at the right-hand side of the screen. Doesn't animate at all. The reason for that is that we've basically done this. We've created this massive long task that's going to keep looping through that loop over and over again until it's calculated that the box is at the end of the screen, and then it's going to render. And because we haven't let it render at any of the points in between, we just see the box at the end.

So what we can do instead is use a recursive

function and use 𝚜𝚎𝚝𝚃𝚒𝚖𝚎𝚘𝚞𝚝. So we're just doing the same thing here, calculating the time, getting the box x and y position. And then if the box hasn't reached the edge of the screen, we're going to call 𝚜𝚎𝚝𝚃𝚒𝚖𝚎𝚘𝚞𝚝, which creates a new task, which will be added to the queue, and then that's going to move the box.

Cool. So that looks like this.

We're going to create a task, which creates a new task, which creates a new task, which creates a new task, which creates a new task, and then the rendering pipeline runs. So that's going to happen every 16 milliseconds, and our box is going to get animated across the screen, and everything is going to be delightful. Except for two small things.

First of all, in our application, there's nothing else running. So we know that our new animation task is going to be the first thing on the queue each time. It's going to be okay. In the real world, a bunch of other stuff could be running, which could mean that the animation task isn't going to run in every single frame, which is going to look a bit crap. The other thing is that we're running those tasks over and over again, but we're only really using the one that runs immediately before the rendering pipeline, right? So we're wasting a lot of processing power.

I mean, this particular example doesn't waste that much processing power, but in theory. So what we can do instead is instead of calling 𝚜𝚎𝚝𝚃𝚒𝚖𝚎𝚘𝚞𝚝, we can call 𝚛𝚎𝚚𝚞𝚎𝚜𝚝𝙰𝚗𝚒𝚖𝚊𝚝𝚒𝚘𝚗𝙵𝚛𝚊𝚖𝚎. Which also creates a new kind of task, but it does it in a special way.

So the 𝚛𝚎𝚚𝚞𝚎𝚜𝚝𝙰𝚗𝚒𝚖𝚊𝚝𝚒𝚘𝚗𝙵𝚛𝚊𝚖𝚎 queue is there at the bottom in green. And now if we run some tasks which creates an

animation task, what will happen is that task will sit in that queue, and it won't run until the rendering pipeline is ready to run. And then the animation frame task will run, and then the rendering pipeline will run. So that queue is always going to run just immediately before the rendering pipeline. The other nice thing is that unlike Promises, if we add an animation queue, an animation task,

if we run an animation task, and we add another animation task while that one's running, that one won't run until the next turn of the event loop. So that means that we can create an animation,

like one frame of an animation, and then set up the callback for the next frame, but it won't run until the next time the pipeline runs.

So that results in an animation like this. So the top box is running 𝚛𝚎𝚚𝚞𝚎𝚜𝚝𝙰𝚗𝚒𝚖𝚊𝚝𝚒𝚘𝚗𝙵𝚛𝚊𝚖𝚎, the bottom one is running 𝚜𝚎𝚝𝚃𝚒𝚖𝚎𝚘𝚞𝚝. As you can see, they look pretty identical because nothing else is running on the screen. Importantly though, when we get to the end,

the top one has run 800 times, and the bottom one has run 2,500 times. So we've wasted four-fifths of our computing power, three-fifths of our computing power just on doing nothing.

Why 𝚛𝚎𝚚𝚞𝚎𝚜𝚝𝙰𝚗𝚒𝚖𝚊𝚝𝚒𝚘𝚗𝙵𝚛𝚊𝚖𝚎? Smooth Animations and DOM Manipulation17:59

Can you use 𝚛𝚎𝚚𝚞𝚎𝚜𝚝𝙰𝚗𝚒𝚖𝚊𝚝𝚒𝚘𝚗𝙵𝚛𝚊𝚖𝚎? Absolutely yes. It is available in all browsers back to IE10. It's not supported in Opera Mini, but neither is animating things in general, so that shouldn't be a problem.

So what does our event loop look like now? It's still an infinite loop. For each turn of the loop, we're going to pick a queue. And then we're going to take the first task off that queue, and we're going to run that task. Then, as long as there are tasks in the microtask queue, we're going to run all of the microtasks. Then, if it's time to repaint, we're going to grab the tasks that are currently in the animation queue, and we're going to run just those tasks. And then we'll repaint.

Cool. So is everybody okay with that? Good, because that is actually as complicated as it gets. That's the whole event loop in the browser.

Key Takeaways: Blocking, Microtasks, and 𝚛𝚎𝚚𝚞𝚎𝚜𝚝𝙰𝚗𝚒𝚖𝚊𝚝𝚒𝚘𝚗𝙵𝚛𝚊𝚖𝚎18:55

All right. So what did we learn? First of all, JavaScript can block rendering. So if you have long-running tasks, you need to either split them into smaller tasks or move them off the main thread so that it doesn't stuff up your user experience.

Promises operate via a special kind of task called a microtask. And microtasks have even more power to block rendering than your standard JavaScript tasks. 𝚛𝚎𝚚𝚞𝚎𝚜𝚝𝙰𝚗𝚒𝚖𝚊𝚝𝚒𝚘𝚗𝙵𝚛𝚊𝚖𝚎 is a special kind of task that only runs with the rendering pipeline. And it's really good for creating really nice, smooth animations that you're guaranteed to run every frame. It's also good if you're doing a lot of DOM manipulation. It allows you to batch that all up just into one task just before the rendering runs, which can give you some efficiencies. The other thing that we hopefully understand now is why doing this sometimes fixes things.

Maybe we don't.

Cool. And that's it. That's all I've got. Thank you very much. You can grab the slides and talk me afterwards.

Thank you. Thank you very much, Erin, for sharing your very

insightful and your experience with us for the event loop.