JavaScript counters the hard way - HTTP 203

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
SURMA: Is my microphone on? It is. Woo! Your turn, Jake. Amuse me. JAKE: Well, no promises there. So Surma, I've been having a look at other web tech YouTube videos and channels and how they do things. And I've noticed that the popular ones, what they do is they take tricky topics and make them simple and easy to digest for people. Whereas what we do, or I guess mostly what I do, is I take something simple and make it really complicated. And I think that's why we don't get the billions of views, Surma. SURMA: And so are you now trying to rectify your path? JAKE: No, bad news. I'm going to do exactly that this episode because we're going to make this. It's a counter. SURMA: OK. JAKE: Yeah. SURMA: That's some senior engineering right there. JAKE: Absolutely. It's the kind of thing you'll see in hundreds of tutorials. You'll see loads of framework components that are doing this kind of thing. And they're all wrong, or the vast majority of them are. Or they're at least inaccurate or sub-optimal. It tends to fall into one of those or both of those. SURMA: When you say inaccurate, do you mean not frame-perfect? JAKE: No, I mean worse than that. I mean, like, it's displaying the wrong number. SURMA: Oh, god. JAKE: And I'll show you why and how sometimes the most obvious way is going to lead to stuff like that happening. SURMA: Mm-hmm. Well, I'm at the edge of my seat. JAKE: But there's something I'm going to look at. It's not just going to apply to timers. It's going to apply to all kinds of low-frequency animation from blinking cursors to drawn animation, that kind of thing. I'm going to cover how animation scheduling works as well and some interesting browser bugs because it turns out a lot is involved in making a simple counter. But there you go, counter, millions of views. SURMA: Knowing you, you implemented this timer in your JavaScript-based slide framework. Is it using the correct technique? Or did you take a shortcut? JAKE: It's the correct technique, absolutely. The correct technique is actually quite small. It's not many lines of code. But there's a lot to it and a lot of gotchas along the way. So I'm going to start with a simple version. There you go. It's setInterval. And that's going to call this callback. Well, we've told it, every 1,000 milliseconds or every second, as humans would call it. SURMA: I'm going to channel my inner Paul Lewis, who basically the second you even start using setInterval he will call your code bad. JAKE: Go on then. What's wrong? What have I done wrong? SURMA: Well, both setInterval and setTimeouts, basically what they do is they schedule something, put it into the event queue after a period of time. But that doesn't mean that it will actually run after the period of time. If I block the main thread for two hours, this will never run. Seconds will not get increased. And so this could drift any which way. And setInterval, specifically, will schedule a task every second with this callback function, regardless of whether the previous scheduled task from the same interval has already run. And that's often not what you want. In this case, it might actually be what you want. But in most cases, it's not. JAKE: Well, according to the spec, it will call the callback. And then once the callback is called, it will then schedule the next one. But you-- SURMA: Oh, so it's literally equivalent to, in essence, a timeout? JAKE: Oh, absolutely. Yeah. SURMA: Ah. JAKE: Certainly in the spec. You called it right. Drift is the problem here. Even in your best case, Safari will drift about half a millisecond with every call. So everything starts out fine. But as we go on and on, like 125 seconds later, we're now half a second late. And by the time we get to 256 seconds-- but at this point, we're now displaying the wrong time. SURMA: Yes. Now we have crossed the boundary. JAKE: And that is best case in Firefox. It will take longer in Safari in best case. But as you said, if something blocks the main thread for a bit, those drifts are going to be exaggerated. Also, if you put a tab in the background, those drifts are massively exaggerated as well. So things are going to get inaccurate really fast. Interestingly, Chrome doesn't drift. It actually auto-corrects, even though that is not in the spec. You could say that's what developers might expect. But-- SURMA: Does it make a spec uncompliant, or is there leeway in the spec to do this? JAKE: There isn't leeway in the spec to do this. So it's non-compliant. SURMA: [GASPS] JAKE: Although, you could argue it's-- I know, right? It might be more what developers expect. But, yeah, it's not complaint with the spec here. SURMA: Dun-dun-dun. JAKE: But again, if it's in the background, it's going to be throttled. If there's a lot of main thread work, it's going to be delayed by some. And it could still miss those seconds. So let's rate this solution. Not accurate over time-- that's bad. But it does appear to update steadily. So this goes to what you were saying at the start, the frame accurate thing. I don't care so much about that. If it appears to update every second, that's good enough for me. So it passes this one, even though it's slightly out. To the human eye, it seems good enough. It also runs code in the background. It's a visual thing. So updating the dom when it's not visible, that's bad, right? You're using the CPU when you don't have to use CPU. But otherwise CPU usage is pretty good. SURMA: Well, two out of four, not too bad. JAKE: Not too bad. Let's see if we can do better than that. Next solution-- so here I'm taking the time from date.now. So that's going to be accurate over time. And that's really the difference here. But we still have that drift. And that is still a problem. So here's the drift again. I've exaggerated the drift to make it easier to see in this diagram. But watch what happens. So now with the drift, we've reached a point where we've crossed that second boundary. So what the user is actually going to see is 15, 16, 18. It won't happen in Chrome because it doesn't have that drift. But this will happen in Safari, in Firefox. SURMA: Well, this is more than drift. This is more about it actually is longer than exactly 1,000 milliseconds until the task gets run. Because if it was exactly 1,000 milliseconds, the drift wouldn't be that hurtfull. We would see 17 seconds for a very short amount of time. But we would see it probably. JAKE: No, because the drift is going to be consistent. You're only-- because it's going to be, like, 1,000 milliseconds plus a bit, right? That's your drift. So that's what we see here, is it displays 16, but in reality, it's mostly-- it's almost 17. So then the next tick is actually 18. So 15, 16, 18, each of those is going to appear like it lasts a second to the user. But it's slightly longer, which is why it ticks over to the next second. And like we saw in Firefox, it's like you're 250 seconds in before you would see this happen. But that's best case. If people are moving between tabs or there's a lot of main thread work, you are more likely to see this as a user. And it looks weird. But let's say you add some code to stop the timer running in the background, because that's one of the things we wanted to fix. When that timer is reactivated, there's a good chance it might be towards the end of that second boundary anyway. So you're more likely to see that skip happen in Firefox and Safari. But with Chrome's drift correction, it is possible to end up with this, where Chrome ends up correcting back and forth around the boundary of a second. So the user is going to see 21, 23, 25, each for around 2 seconds, because it's back and forth on that second boundary. I mean, you'd have to be pretty unlucky to [? land ?] with this. But if you've got thousands of users and they're changing tabs and whatever, some user is going to report this timer is doing weird things. And you are not going to know why. It's going to be really hard to debug that. SURMA: Also, you didn't store the return value of setInterval. So this code doesn't even work, Jake. JAKE: Look, just gloss over that. We will get to-- actually, I don't-- the actual solution that I'm going to put in the description of this video, it will be cancelable. But yes, so I glossed over that. I glossed over that. It's fine. So even if extra codes will stop the timer in the background, we've introduced a new problem. The user is going to see skipped numbers or uneven numbers or whatever. It's not good. Back to the drawing board. So this is pretty similar to our previous example. But I'm going to use requestAnimationFrame to do the scheduling. This might not feel like an animation, but it is. It's something that visually updates over time. SURMA: Yeah, this is kind of shooting at birds with cannons or whatever the actual turn of phrase is. JAKE: Yes. So this is good in some ways because, of course, the animation frame pauses when the page is hidden. So we're getting that feature for free. But go on, what's the problem? SURMA: I guess about 984 milliseconds CPU usage are wasted because there's nothing to update on the UI. JAKE: Yeah. SURMA: So, yeah, 59 frames, you have nothing to update for the 1 frame that you have something to update. JAKE: RequestAnimationFrame pushes-- it runs as fast as updates are pushed to the monitor, which is usually, as you say, 60 times a second. So modern phones run at 90. 144 times a second is popular on gaming monitors. SURMA: My iPad runs at 120. Oh, no, RequestAnimationFrame doesn't. The screen is 120, but Safari doesn't skip. That's another story for another time. JAKE: But there are VR screens and all of that sort of stuff, which are going to be running at that kind of rate, if not higher. We're running code 60 times more than we need to is the point. And that shows up. That shows up in your task manager or your activity monitor or whatever. It's bad. We've swapped one problem for another. SURMA: Might even show up in your battery drain. JAKE: It will absolutely show up in your battery drain. And in some cases, it will turn fans on in your machine. It's something you will notice. So the way I tried to solve this, I tried to be smart. I felt-- SURMA: Ah, don't do that, Jake. We've tried this before. JAKE: Well, as you'll see, I failed. So it's like all of the other times I try and be smart. Look, animations felt like the right thing, because it's the right part of the event loop and all that stuff. So I thought, can I make an animation that is every second rather than 60 times a second? So I started with this. So this is a bit like performance.now, in that it's the time since the creation of the documents. But it's the time for the current frame. Like, in a tight loop, if you called date.now or performance.now, you'll get different values, which is useful in some ways. Whereas current time is always going to give you the same number within the same frame. And this is how animation scheduling works on the web. Because if you start multiple animations in the same task or part of the same frame, they will all start in sync because they get their start time from the current time. SURMA: That seems very sensible. JAKE: Absolutely. If you've used RequestAnimationFrame before, you'll notice it actually passes you in a time to your callback. It's the same thing. It's the current time of the frame. So I've got my frame callback, as before, calculate which second we're in, as before, just using current time instead. But to schedule a frame, going to use the web animation API with no keyframes. I'm using the body element just because any element will do. No keyframes, so I'm using null. And it's got a duration of 1 second minus how far we're currently through the second. So that's what we're using the modulus for there. And that's our drift correction. SURMA: Ah. OK. JAKE: A bit like what Chrome does with setInterval. And then we call frame again, when the animation is done, using the frame events, [INAUDIBLE] the finish event. So using the on-frame thing that-- SURMA: I thought you were going to go with a step function or something. But this works. OK. JAKE: Yeah. So I felt very clever. Doesn't work, though, does it? The CPU usage is about the same as RequestAnimationFrame. And that's in Firefox, Safari, and in Chrome. It turns out that browsers are using something very similar to RequestAnimationFrame internally to queue this up. Thanks, browsers. I really thought I was onto something here. SURMA: Yeah, I'm actually surprised, because I figured at very least, your code like the code that you wrote would at least be less frequent. But I guess if the browser is under the hood still doing a RAF to check if the unfinished needs to get invoked, then you're not saving much, maybe a tiny bit, but, yeah, very interesting. JAKE: Saving a little bit on the dom update, but, yeah, it uses about the same CPU. Yeah, so not great. You can actually do this without JavaScript at all. Here is a two-digit timer with [INAUDIBLE] CSS. But it has the same problem. Chrome, Safari, Firefox, all have high CPU usage here. Also, it doesn't work in Safari. You just get the CPU usage. It doesn't actually change the display. They are actually working on that. Fair enough. SURMA: I'm surprised you went with linear and not a step function. I feel like that's what it was made-- I mean, content probably just flips over at 50%. But with step function it would have probably been a bit more intuitive. JAKE: Yes. So with what they call a discrete value animation, it flips over at 50%, which is why I've used an animation delay here to sort of correct for that 50% switch. And you're right. I could have used step. When you use steps, which is an easing function, you can also specify is it in the middle, at the start, or at the end. Yeah. So I could have used that. I was trying not to complicate it. But thanks, Surma. We've complicated it. I don't know why I didn't try and complicate it because this whole episode is basically just an over complication of something really simple. Fine. It's fine. But yes, this uses way more CPU than our setInterval version, which surprises me because I usually have this thing in my head that if you can do something without JavaScript and it's not a total hack, then that's probably the best way to do it. But that's not the case here. SURMA: I guess that makes it a hack then. JAKE: For now, yeah, unless browsers can optimize it. So what now? We need to go back to plain jobs four timeres. But let's include some of the stuff that we learned from animations. As before, we're going to use the current time. It's in animation time. This is an animation. We're also going to pass that into our frame function for the first call. This bit, same as before, figure out the second, display it. But here's how we're going to schedule the next frame. We're going to setTimeout, which is a bit like setInterval, but it only happens once. When that calls back, we're going to use RequestAnimationFrame as well. And that gives us the-- well, it synchronizes with other animations, but it's also going to stop it running when the page is in the background. And it's also going to provide us the time for the next frame as part of our callback. Great. For the delay we're going to do similar to what we did before, wait a second, but reduce that by the amount of second that's already passed. But we're using setTimeout this time. This is good in Firefox and Safari but not in Chrome. SURMA: Really? JAKE: I don't know if it's a bug or if it's expected behavior. This is what we want, something like this, one frame per second. But due to synchronization issues between setTimeout and the current frame time, it undershoots a bit. It actually happens before the second boundary. So our drift correction schedules another callback almost immediately. So you get this pattern like, boom, boom-boom, boom-boom, boom-boom. It looks fine to the user because we're using not date.now. SURMA: Interesting. So it actually-- yeah, because it schedules too early, the actual second you were trying to complete hasn't quite completed yet, and you schedule another one immediately almost. JAKE: Yes, exactly that. It's doing-- SURMA: So this is a case for the double RAF? JAKE: Doesn't help. Or you end-- SURMA: Oh! JAKE: Maybe double RAF would help, but then you're pushing things out by a frame. And I did experiment with just putting an extra delay of, like, some milliseconds onto this. But it's not always accurate. Yeah. So it felt like a bit of a hack to do something like that. It's not the end of the world. We're running code twice as much as we need to. And that's only in Chrome. SURMA: Saying this is not good CPU usage is also technically correct, but also kind of unfair compared to how wasteful the others solutions were. JAKE: Yeah. We were 60 times the amount of work before. We're now twice the amount of work. And that's a big difference. But this is actually two or three. So we're going to fix this properly. SURMA: Yeah, you better. JAKE: When you've got an inaccuracy, that can happen in both directions, positive and negative. The solution is to round rather than floor. There's actually three bits of flooring code in here. Can you spot them? SURMA: Well, there's the floor. That definitely floors. JAKE: Yes, I was hoping you would get that one. SURMA: I'm guessing current time is also implicitly rounded or floored at the start, maybe? JAKE: It's actually not, although that can depend on the browser. And that comes from performance.now is rounded in some browsers and not in some others. And that can also depend on high-resolution timers and stuff, anyway. SURMA: So I guess the set timer parameter is also implicitly rounded to milliseconds, at the very least? JAKE: Yeah, well, done actually. I'm impressed you got any more of these. You're right. SetTimeout, what it will do is floor whatever you give to it. And that's as per the spec. So if you pass it, like, 99.99, it's going to come down to 99 rather than go to 100. SURMA: And the only last one I can think of would be the parameter frame, which is given to your by RequestAnimationFrame. JAKE: No, the other one is the modulus operator, which is a flooring-like operator because it's giving you the amount that you've overshot the second. It's not giving you the distance to the next second. So this could give you 999 when it's actually 1 millisecond till the next second. So it's not strictly flooring, but it's going-- it's always counting back. SURMA: Right. Oh, fine. I'll give you that one, flooring and the like. JAKE: Yeah. But it's one of those things that we would look at, at some point. And, yes, the setTimeout is the other one. So let's get to work. I'm going to get rid of that scheduler we had before. And I'm going to swap that floor for a round. There we go. SURMA: So you're only using the-- well, if we're you're trying just a floor into a round, that is only used for the seconds variable, which we only use to update the UI. So it doesn't really affect how we schedule things. JAKE: We're going to use it to schedule things. Here we go. SURMA: Oh! JAKE: We're going to figure out what time the next frame should be. So that's the second that we are displaying, which we've rounded plus 1 turned into milliseconds and add it to the start time. So that's when we want the next frame to happen. And then similar to before, we're using setTimeout, RequestAnimationFrame. But the target time, it's going to take our delay minus whatever time it is now. SURMA: And that's a big difference because now we're using performance.now, which takes the current time, like, literally, when is this line of JavaScript executed not when was the last frame shipped, which is what current time is. JAKE: Yes. And that's because that's what setTimeout uses. When you give it 100, it's 100 from the time it's called not the time of the last frame. So, yes, it makes sense to use performance.now here. We could round this as well. But it's not necessary. Rounding the seconds is enough. And that's it. That's how you do a simple timer that achieves all of these things. SURMA: And we have no double frame anymore. This is literally you ship a frame exactly when you need to on the second. JAKE: Yes. SURMA: Incredible. JAKE: And I will include a link to a version in the description that can schedule for intervals over the seconds. So if you wanted to do something every 12 times a second, which some animations, drawn animations like that, you can have that, and it's cancelable with an abort controller as well. But I'm not done yet. I know this is a long episode. But I'm not done yet. Something was keeping me up at night. and it's like, this should have been the answer. This should have worked, all the web animation API. It really bugged me that this performed badly in browsers. So what I did is I wondered are there other kinds of animation that are badly optimized like this. So I went and started testing them, other kinds of web animation that have infrequent changes. So we've seen empty web animations. You can also do this with CSS. Not sure why you would want to, but you can. It performs badly in all browsers. They run internal code every frame, 60 times a second or whatever. Animations can have a delay where nothing happens for a bit. So this is a five-second animation, but 10 seconds happened-- nothing happens before the animation kicks in. Chrome and Safari optimize for this. But Firefox doesn't. And that's a shame because that could have been my workaround for the web animation thing. I could have used a delay in a zero-duration animation. Anyway, never mind. Here's a 10-second animation. But you mentioned steps before. It's only going to have two frames. Safari optimizers for this, but Firefox and Chrome do not. SURMA: Oh, that's interesting. JAKE: So well done, Safari, here. And then finally this one. So here we've got something. It fades out a bit and then fades back in. And then it waits. So most of the animation is just the thing sitting there, opacity 1. I actually use this on the Chrome [? decimate ?] website. Like, when the conference was live, I had a little red circle, like a record. SURMA: The record light, yeah. JAKE: And every now and then it would sort of pulse, using this. No browser optimizes for it. It's-- SURMA: That's Interesting. JAKE: It's kind of sad. SURMA: In these examples, at least, it seems fairly easy to do this analysis, whether there's basically dead time in the animation. But I'm guessing it will become increasingly hard when you have more complex animations. JAKE: Yeah. And fair play to Safari for optimizing more cases than Firefox and Chrome. But there are still real-world improvements that can be made. So I'll add them to the description, because I filed bugs with all of the browsers about this. So you can track when they start fixing these things. I think the empty animation, which is the one that was important to me for this, the timer, I think they're going to fix that first. It'll probably be Safari that fixes it first, which is great. But, yeah, you can follow along for that. And finally, that is all I had. That's all I've got. And so hopefully this episode is going to get the millions of views because everyone's desperate to watch a-- I don't even know how long it lasted-- like, a 45-minute episode on creating a JavaScript counter. SURMA: Just-- yeah, just count up, but do it efficiently. JAKE: [CLEARS THROAT] Oh, yes, good. That's good coffee. Mm. [CLEARS THROAT] (DEEP SCRATCHY VOICE) Oh, yes. Oh, I'll just do the rest of the episode in this voice. Ooh. Sexy. Sexy, sexy JavaScript.
Info
Channel: Google Chrome Developers
Views: 31,048
Rating: 4.9534636 out of 5
Keywords: GDS: Yes, counter, timer, javascript counter, web counter, counter animation, accurate digital counter, how to make a digital counter, how to code a digital counter, digital counter tutorial, digital counter, web timer, counter tutorial, setTimeout, setInterval, requestAnimationFrame, animation scheduling, new videos from Chrome, Chrome, Chrome Developers, Chrome devs, developers, videos for developers, css, javascript, new in tech, new in web, new in chrome, HTTP203, Jake and Surma
Id: MCi6AZMkxcU
Channel Id: undefined
Length: 24min 8sec (1448 seconds)
Published: Tue Feb 09 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.