 And it's my microphone on. It is. Whoo! Your turn, Jake. Amuse me. Well, no promises there. OK. So, Soma, 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, Soma. And so are you now trying to rectify your path? Not bad news. I'm going to do exactly that this episode, because we're going to make this a counter. OK. I mean, that's some senior engineering right there. 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. They're at least inaccurate or suboptimal. It tends to fall into one of those, or both of those. When you say inaccurate, do you mean not frame perfect? No, I mean worse than that. I mean it's displaying the number. And I'll show you why. And how sometimes the most obvious way is going to lead to stuff like that happening. Well, I'm at the edge of my seat. There's stuff 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. Knowing you, you implemented this timer in your JavaScript-based slide framework. Is it using the correct technique, or did you take a shortcut? 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 set interval, 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. I'm going to channel my inner Paul Lewis, who basically, the second you even start using set interval, he will call your code bad. Go on, then. What's wrong? What have I done wrong? Well, both set interval and set timeout. 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 set interval 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. Well, according to the spec, it will call the callback, and then once the callback is called, it will then schedule the next one. Also, it's literally equivalent to a nested set timeout. Oh, absolutely. Yeah, certainly in the spec. You called it right. Drift is the problem here. Like, 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. Yes, now we have crossed the boundary. 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... Does it make its spec... Uncompliant or is there leeway in the spec to do this? There isn't leeway in the spec to do this, so it's non-compliant. Although you could argue it's, I know, right? It might be more what developers expect, but yeah, it's not complying with the spec here. 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, you know, be delayed by some and it could still miss those seconds. Okay, so let's write this solution. Not accurate over time. That's bad, but it does appear to update steadily. Like, so this goes to what you were saying at the start, like 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. Like, it's a visual thing. So updating the DOM when it's not visible, that's bad, right? You're using CPU when you don't have to use CPU, but otherwise CPU usage is pretty good. Well, two out of four, not too bad. 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's 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 and Firefox. 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 hurtful. You would see 70 seconds for a very short amount of time, but you would see it probably. No, because the drift is going to be consistent. You're not, you're unlike, 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, it 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 with Firefox, it's like you're 250 seconds in before you will 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 and more likely to see this as a user. 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 want it 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 two 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. Like it's going to be really hard to debug that. Also, you didn't store the return value of setInterval, so this code doesn't even work, Jake. Look, just gloss over that. We will get to, actually I don't even, 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. All right, so even if extra codes have stopped the timer in the background, we've introduced a new problem. The user is going to see skip 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 we're going to use requestAnimationFrame to do the scheduling. Like this might not feel like an animation, but it is like it's something that visually updates over time. Yeah, this is kind of shooting at birds with cannons or whatever the actual turn of phrase is. Yes, so this is good in some ways because requestAnimationFrame pauses when the page is hidden, so we're getting that feature for free, but gone, what's the problem? I guess about 984 milliseconds CPU usage are wasted because there's nothing to update on the UI. Yeah. So yeah, 59 frames, you have nothing to updates for the one frame where you have something to update. RequestAnimationFrame pushes, like it runs as fast as updates are pushed to the monitor, which is usually, as you say, 60 times a second. Some modern phones run at 90, 144 times a second is popular on gaming monitors. 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. 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. Might even show up in your battery drain. It will absolutely show up in your battery drain. And in some cases, it will turn fans on on your machine. It's something you'll notice. So when I tried to solve this, I tried to be smart. I felt like... Don't do that, Jake. We've tried this before. Well, as you'll see, I failed. So it's like all of the other times I try and be smart. Like 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 call 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. That seems very sensible. Absolutely. If you've used request animation frame 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, I'm 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 one second minus how far we're currently through the second. So that's what we're using the modules for there. And that's our drift correction. Ah, okay. A bit like what Chrome does with a set interval. And then we call frame again when the animation is done using the frame events like the finish event. So using the on frame thing there. I thought you were going to go with a step function or something, but this works. Okay. Yeah. So I felt very clever. Doesn't work though, does it? The CPU usage is about the same as request animation frame. And that's in Firefox, Safari and in Chrome. It turns out that browsers are using something very similar to request animation frame internally to queue this up. Thanks browsers. I really thought I was on to something here. 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 raft to check if the on finish needs to get invoked, then you're not saving much. Maybe a tiny bit, but yeah. Saving a little bit on the DOM update, but yeah, it uses about the same CPU. Yeah. So not great. I mean, you could actually do this without JavaScript at all. Like here is a two digit timer with pure 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. 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. 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, Irma. 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 set-interval 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. I guess I'd make it a hack then. But now, yeah, unless browsers can optimize this. So what now? We need to go back to the plain JavaScript timers, but let's include some of the stuff that we learned from animations. As before, we're gonna use the current time. It's an animation time. This is an animation. We're also gonna 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 gonna schedule the next frame. We're gonna use 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 gonna provide us the time for the next frame as part of our callback. Great. For the delay, we're gonna 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. Really? And 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 while I drift correction, schedule is another callback almost immediately. So you get this pattern, like boom, boom, boom, boom, boom, like it looks fine to the user because we're using, you know, not date.now. Interesting. So it actually, yeah, because it's getting too early, the actual second you were trying to complete hasn't quite completed yet and you schedule another one immediately almost. Yes, exactly that. It's doing... So is this a case for the double RAF? Doesn't help. Or you end up... Oh! Maybe double RAF would help but then you're pushing things out by a frame and like, I did experiment with just putting like 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. I mean, 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. I mean, saying this is not good CPU usage is also technically correct but also kind of unfair compared to how wasteful the other solutions are. 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 gonna fix this properly. Yeah, you're better. 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 like code in here. Can you spot them? Well, there's the floor, but definitely floor. Yes, I was hoping you would get that one. I'm guessing current time is also implicitly rounded or floored at the start, maybe? 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. So I guess the set timeout parameter is also implicitly rounded to milliseconds at the very least? Yeah, well done, actually. I'm impressed you got any more of these. You're right, set timeout, 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 up to 100. And the only last one I can think of would be the parameter frame which is given to your by-request animation frame. Now, 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 99.99 when it's actually one millisecond till the next second. So it's not strictly flooring, but it's always counting back. Right. Oh, fine. I'll give you that one flooring-like. Yeah, but that's one of those things that we would look at at some point. And yes, the set timeout is the other one. Okay, 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 around. There we go. So you're only using what we're trying to just form a floor into around. 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. We're going to use it to schedule things. Here we go. Oh! 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 one, turn it 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 set timeout, request animation frame, but the target time, it's going to take our delay minus whatever time it is now. And that's a big difference, because now we're using performance.now, which takes the current time. Literally, when is this line of JavaScript executed, not when was the last frame shipped, which is what current time is? Yes, and that's because that's what set timeout uses. When you give it 100, it's 100 from the time it's called, 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, you know? And we have no double frame anymore. This is literally your shepherd frame exactly when you need to on the second. Yes. Incredible. And I will include a link to a version in the description that can schedule for intervals other than seconds. So, you know, if you wanted to do something every, you know, 12 times a second, which, you know, 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. Like this should have worked or the web animation API. Like, it really bugged me that this performed badly in browsers. So what I did is I wanted other kinds of animation that are badly optimized like this. So I went and started testing them. Other kinds of web animation that have like infrequent changes. So we've seen empty web animations. You can also do this with CSS. Not sure why you'd want to, but you can. Performs badly in all browsers. They run internal code every frame, you know, 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 happen, you know, nothing happens before the animation kicks in. Chrome and Safari optimized for this, but Firefox doesn't. And that's a shame because that could have been my workaround for the web animation thing. Like I could have used a delay and a zero duration animation. Anyway, never mind. Okay. Here's a 10 second animation, but you mentioned steps before. It's only going to have two frames. Safari optimizes for this, but Firefox and Chrome do not. Oh, that's interesting. 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 like most of the animation is just the thing sitting there. Opacity one. I actually use this on the Chrome Dev Summit website, like when the conference was live, I had like a little red circle, like a record. The record light. Every now and then it would sort of pulse using this. No browser optimizes for it. That's interesting. It's kind of sad. In these examples, at least it seems fairly easy to do this analysis, whether there's, you know, basically dead time in the animation, but I'm guessing it will become increasingly hard when you have more complex animations. Yeah. And then I 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 the timer, I think they're going to fix that first. 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 this lasted, like a 45-minute episode on creating a JavaScript counter. Just, yeah, just count up, but do it efficiently. Oh, yes, good. It's good coughing. Oh, yes. Oh, just do the rest of the episode in this voice. Oh, sexy, sexy JavaScript.