JavaScript counters the hard way - HTTP 203
Вставка
- Опубліковано 24 сер 2024
- You’ve seen loads of counter tutorials online, but they’re all a bit wrong… or at least most of them are. Jake and Surma dissect different techniques and identify how to make the counter work accurately and efficiently across browsers.
The optimal reusable solution → goo.gle/2LY9GNI
Browser bugs for suboptimal CSS animations:
Chrome (empty JS anim) → goo.gle/3qS3vt7
Chrome (other animations) → goo.gle/39izDjS
WebKit → goo.gle/2Mnkt3F
Mozilla → goo.gle/2Nt4T74
Other videos in the series → goo.gle/2wneQLl
Subscribe to Google Chrome Developers here → goo.gle/Chrome...
Also, if you enjoyed this, you might like the HTTP203 podcast! → goo.gle/2y0I5Uo
Hey, you cared enough to scroll down to the comments! At this point you might as well subscribe.
How d'you think I got here?
@spYro double subscribe! I hear you need to SMASH the button
Yeah, we're going to need an else clause. We programmers generally spend much time with interpreters and compilers and are therefore fastidious.
My UA-cam recommendations sent me here, even the algorithms looks broken for some people.
THE perfect way to use the pinned comment!
This is the only channel, where a seasoned javascript developer can still learn something in a basic counter tutorial. Very well done, and so incredibly informative.
Thank you! Glad it's interesting & useful
Taking simple things and making them complicated - exactly why I'm here. I don't just want to get stuff done. I want to get it done right, and understand why it's right 👍
Kudos for making such excellent content!
If I ever get asked to implement a counter during a job interview I'm hitting them with this bad boy.
🤣🤣🤣🤣
😂😂😂😂😂
You might not be getting the billions of views, but the videos you do make are always great!
Though work like this does end up *affecting* billions of views of webpages when it becomes standard in libraries, Frameworks and tutorials.
Fun fact: you can clear a setInterval using clearTimeout.
Because, as Surma said, setInterval is literally setTimeout that schedules itself.
Thanks for complicating. Without it, I wouldn't have known about CSS steps() function being a thing.
Beginners tutorial: how to build a counter
Advanced tutorial: how to (not?) build a counter
This is the hardest thing to do with a seemingly simple task I've ever seen, congratulations. I loved it
Now I know how should I build a counter.
Thank you.
@Jake Archibald, Great episode. Thank you so much for making things complicated ;). Please make a video on how you debug things behind the scene. How do you approach this kind of issue. I think everyone wants to see the pain went behind the scene as well.
11:54 Nitpick:
Technically the `%` operator is the "remainder" operator - not *modulo*
tc39.es/ecma262/#sec-numeric-types-number-remainder
The _remainder_ operator calculates the remainder of division rounded towards *zero*, i.e.:
dividend - divisor * Math.trunc(dividend / divisor);
_modulo_ calculates the remainder of division rounded towards *negative infinity*, i.e.:
dividend - divisor * Math.floor(dividend / divisor)
Both produce identical results when both operands have the same sign but produce different results when the operands have different signs.
const modulo = (dividend, divisor) =>
((dividend % divisor) + divisor) % divisor;
const tests = [
{ dividend: 5, divisor: 3, mod: 2, rem: 2 },
{ dividend: 5, divisor: -3, mod: -1, rem: 2 },
{ dividend: -5, divisor: 3, mod: 1, rem: -2 },
{ dividend: -5, divisor: -3, mod: -2, rem: -2 },
];
for (const { dividend, divisor, mod, rem } of tests) {
console.assert(
dividend % divisor === rem,
`${dividend} % ${divisor} is not ${rem}`
);
console.assert(
modulo(dividend, divisor) === mod,
`${dividend} mod ${divisor} is not ${mod}`
);
}
So in this use case it doesn't matter but when you're doing "clock arithmetic" you want *modulo*, not remainder.
although not pulling in millions of views, these videos are gold for the more experienced developer that wants still wants to learn something new. and the teaching style is great. it never feels like im learning, rather im discovering a topic with the both of you
Gracias
This is super helpful for me! I've been working on a web player for a proprietary bitmap animation format from a Nintendo DS app, and getting the frames to render at the right interval has been a really annoying challenge for me. Gonna give this a whirl!
Excellent, thorough assessment of problems implementing time accuracy for js animation
I ran into these issues and had to figure them out while I was making a drum machine. Wish I'd had this video to hand back then :D. Even if your eyes would never detect the drift you can definitely hear it, especially when it's inconsistent.
Great video, thoroughly enjoy these scenic-route explanations.
That intro segment is exactly why I watch these videos XD
You guys are great presenters, and provide tremendous value with your videos.
Big thank you
Thank you for this!
Making simple things hard - is the best of this channel)) because it is our frontend life, here is absolutely no simple things, but LOTS of details!
Hey, thanks for the explanation, I never know about document timeline :)
My favorite UA-cam channel! Love you guys
I learned so much about document.timeline. thanks for the complication!
Great episode. Enjoyed watching
I actually really loved this. Where to learn complex stuff better than in simple problems.
Also: never heard of steps in CSS before, thanks for that, too :)
My usual 10 min morning coffee got extended to 24:08. Riveting geekery 👍
Hey Jake
Please put more time in explaining what code is written, would be more helpful then
Loved the video, thanks
Well done! Another mind-blowing episode
I always enjoy your tinkering videos!
I was wrapping my head around where time arg comes from. It turns out it's passed by raf - "passed a single argument, a DOMHighResTimeStamp, which indicates the current time"
Oh danm, I got to know this program last epsode, but I'm confident in saying that's one of the best shows/video series I've seen in a long while. Better than the shows I've been watching on netflix, but probably that on my poor ability to choose series ahha
Good work digging there and
SAVE CPU WHENEVER YOU CAN!
Very clever solution with targetNext.
this was just amazing. Thank you guys
Why would anyone dislike it
The requestAnimationFrame example could be improved...the animation frame callback gets a timestamp given to it, so you can use that in place of date.now()
I literally hate Chrome timing. Implementing JSMoo, a currently 4-system emulator, getting timing to happen in any consistent way is a serious challenge.
Great episode, thanks!
One thing that I think is wrong in the last solution is that it does not account for some browsers (safari) rounding performance.now() so it can actually be smaller than requestAnimationFrame argument. So the scheduling is not great, but rounding will take care of this.
Thank you for doing the deep work
Fascinating!
This is gold
I like this video concept.
love ya guys!
Love this one!!
Thanks for the great videos..
As a developer who has written a lot of css animations... I can confirm they're pretty wonky.
Thank you
so great thanks
Well done.
Thank you for this rich content!
This is exactly why I stay away from the front-end
This was BRILLIANT! :)
Regarding Chrome's rAF bug I did open crbug.com/1018120 you might want to check.
Also, an other mean to have a timer is to use the Web Audio API and its AudioScheduledSourceNode. These won't tick the "Avoids running in background" case, but on the contrary they'll prevent the browser from throttling the tab's timers at all. So not great for the trees, though I didn't measure overall CPU usage, but in some cases (e.g stackoverflow.com/questions/40687010/40691112#40691112 ), we want our code to run no matter what.
What did you mean by the "double wrath"? (16:23)
It's "double raf", as in call requestAnimationFrame within requestAnimationFrame
requestAnimationFrame(()=>{
requestAnimationFrame(callback)
}
)
Interesting!
❤️🤓 thank you guys, this is premium web-nerd food 🍱
Amazing!
Very nice job. These two perspectives are just super useful in this context, and I hope it appeals to ‘the kids’ too :)
But what if we schedule the frame function in the video.requestVideoFrameCallback method on a looped, 1 frame per second and 1x1 resolution video?
Ohhhhh that's a great idea!
(I mean, it might be a terrible idea for all sorts of reasons, but I like your thinking)
Ah, it seems to suffer from drifting (~10ms per frame). But I'm not sure if the video framerate is precisely 1fps or if chrome does not render it precisely at 1fps.
codepen.io/wesselkroos/pen/MWbjypz
Also, with a ogv video the videoFrameCallback is only called once per ~11 frames...
@@wezelkrozum ah well, thanks for giving it a go
Wow! Excellent interview question. So many gotchas. I liked the way you presented the drift. Thanks for teaching us something new. How did you benchmark the CPU usage for each approach though? Is fair to say high CPU usage => high main thread usage for rAF, body.animate and CSS keyframes approaches?
I just watched the task manager to measure the CPU. Not very scientific, but the difference was extreme enough to see.
@@jakearchibald Got ya. Is fair to say high CPU usage => high main thread usage for rAF, body.animate and CSS keyframes approaches?
Just trying to understand why higher CPU usage is bad if it doesn't block the main thread
@@NitinPasumarthy even off the main thread, CPU usage uses battery
@@jakearchibald Totally makes sense. Thank you.
In general, which of rAF, body.animate and CSS keyframes approaches you shared, block the main thread and potentially impact user experience when used in other complex counters? Say, in my case, CPU & battery are less of a problem, than interactivity.
@@jakearchibald It's visible in the task manager? I was sure the difference would be too small to show up.
Thats good job google
Is there an example why UA-cams counter on ads are wrong and take 1 extra second 🤷♂️ yeah I notice...
This should be the video that auto plays after every js counter tutorial.
Is this a GameOfThrones Whiskey on the background?))
Yep! Someone bought me it, and it was surprisingly nice
I like complicated stuff. So thanks. :)
Time to refactor all my timers!
Count efficiently!!!
What is your recommendation to handle longer timeouts that would be handled well over computer sleep? As an example, one minute timeout/interval to show time (hours and minutes) does not work very well over sleep.
Lol I used to do the basic thing of checking the difference of setTimeouts and the date time and calling it a day. Then I started using millisecond level clocks and stopped caring :D
Can't help but think this should be renamed "How to implement the tag". The counting part is not really relevant to the video and excellent insights.
Since you are talking about animation, It would be interesting if you explain how requestAnimationFrame can be used with canvas in an worker thread.
Although we don't have an episode about this, there's developers.google.com/web/updates/2018/08/offscreen-canvas
Browsers that have OffscreenCanvas also have requestAnimationFrame in a Worker context
@@dassurma Interesting. Is that available in all major browsers? Last time I experimented with multi thread webGL rendering was a few years ago and offscreen canvas was still experimental
I feel like using `document.timeline` should immediately disqualify this as the "right" solution given that it's still in working draft, regardless if browsers technically implement it or not.
That's a bad take 😀. The HTML spec is a living spec, somewhat equivalent to a working draft. I guess you refuse to use HTML too?
@@jakearchibald Maybe I'm just jaded by Google's premature use of the Shadow DOM v0 spec but there is an obvious difference between an accepted standard and one still labeled as experimental. Google in particular has been entirely too sanguine with considering its own implementation of a fledgling spec as ready for wide adoption. That's a "bad take" not unlike using the HTML living standard in an apples to oranges comparison to a Web API which isn't in living standard status. The "bad take" would be forgetting the Google privilege that if you shoot for the moon on a bad spec you have an army of developers to change direction (See: Polymer v2) which 99% of the web world does not have. Foisting potentially unstable tech on teams which can't afford the maintenance cost should the rug be pulled out from under them is a "bad take."
@@ShawnBiddle the web components thing was bad, agreed. However, the animation API I'm using here isn't Google only, it's in all browser engines, so the comparison to web components is apples/oranges.
Also, check out the list of editors in the spec. Mozilla + Apple + Google.
@@jakearchibald Totally fair enough, like I said I'm just jaded :D Though given how many things you went over in this video that are further along than WD in the spec that just doesn't work how you'd expect or explicitly ignores the spec I think we can both also agree that there's a big gray area between "use everything" and "use it after it's been stable for 5 years." Smaller companies are necessarily more conservative and a lot of the time companies with dev bandwidth they take for granted signal prematurely the readiness of feature XYZ. Google certainly isn't the only culprit nor is the web the only susceptible platform.
Surprised safari is the one to have those optimizations. I just wish they also kept up with implementing new js/css features 😅
Animations are one of apples jam
is there a way to stop it without AbortController?
You can adapt the solution to do it however you want 😀
No other series on web development will make you so sad to be a web developer. Guaranteed!
Might make some "HTTP 203: bringing the sadness" t-shirts
@@jakearchibald Maybe it says something about me that I make sure to watch every episode you guys make. Keep up the good fight!
Thanks!!!!
What about a simple requestAnimationFrame loop where you floor the time every time, but only update the UI if it's floored value changed?
That still performs badly (similar to the web animation example). It's the 60hz timer that uses most of the CPU, not the DOM update.
@@jakearchibald Interesting
currentTime seems nice.
I used to combine the elapsed time calculated with Date.now in setTimeout with requestAnimationFrame and tried to hit the next true second elapsing.
So far this works perfectly fine for me and doesn't show any drift.
Now I would wonder what happens if NTP syncs time throughout the timer running..
I remember that double hit with setTimeout and I also remember that I solved it.
I should re read through my timer code.. I'm confident about it.
I made it like.. 10 years ago before requestAnimationFrame existed and extended it with requestAnimationFrame as soon as it shipped xD
Ok a quick look led me there:
let timeout = now % 1000;
timeout = timeout !== 0 ? 1000 - timeout : 1000;
What if you simply took the negative deltas and made them zero? (thus avoiding the double-render)
It isn't clear that they're a negative delta
Can you send us the article of Paul Luis who says setinterval is bad. I want to see what he uses instead.
We want to redo all of our timers in the app as it’s causing too much of a slowdown for other things
If you need to run something every frame, use requestAnimationFrame. If it's a low-frequency animation, use the solution in this video. If you're using timers for other things, consider using something else developer.chrome.com/blog/timer-throttling-in-chrome-88/ 😀
@@jakearchibald thank you so much. I’ll start looking into all of these!
How did you got to know about drift??
I think I first encountered it by implementing a timer and seeing it glitch. However, I now know about it in more detail by reading the spec.
@@jakearchibald is there any way to check if particular website counter is drifting or not (through code) like if I want to check of any chess website.
@@rajatgupta08 You could record performance.now() at the start, then compare it to performance.now() at the end and see if the timer is showing the right thing
I ended up with a similar solution when I needed a way to schedule cron-like actions on a website (some run every second, some at 8pm every day, some every hour, etc)
github.com/creationix/family-dash/blob/main/modules/schedule.js
I do not quite understand how that is supposed to work consistently. Can't you still get too much drift from setTimeout?
E.g. going from underlying time difference of 16.4999 to 17.5001, yielding a jump in the UI from 16 to 18.
I actually implemented a countdown just a few weeks ago. I think i settled for using a time difference while updating the UI a few times every second, but not every frame.
So when the drift hits a boundary you get one interval that is slightly off but no numbers are skipped.
It'd have to drift over 1000ms to get the wrong number, and the drift correction should stop it getting anywhere near that
Just curious...what earphones are you guys using?
...and microphones
Thanksssssssa
I did a similar kind of optimization when I wrote a FPS calculation library, it tried to predict when the next update should happen and bails all the calculations prematurely to save CPU making it a simple conditional integer branch.
But in my case the scheduling mechanism is up to the consumer.
github.com/victornpb/micro-fps
In "real" schedulers you generally have both a "run after/delay by" function and a "run at/delay until" function. Does JS / DOM need the latter? There's still a teeny inaccuracy between the now when you call setTimeout and when it actually executes, which might be important to someone, I guess.
Feels like the web animation API should be the way to do the more accurate timing, right?
@@jakearchibald call me a weirdo for thinking web animation should be for animations! More seriously I would be suspicious about it not running my code if it didn't feel like it, (background) etc., but I suppose that's all spec-able.
@@SimonBuchanNz ahh yeah, if you want it to run in the background, that's different. Generally we're trying to cut down on pages running stuff in the background, and instead looking at features that eg show a notification at a particular time
@@jakearchibald thinking of valid use cases is pretty tricky, to be fair: keeping a server synchronised, polling input, computing processing/usage statistics over time, but most of those are either not that time sensitive or should be parametrized by the time that's passed. I'm mostly just anxious about apis that imply a missing feature.
Seems to me the spec is wrong and was designed very lazily.
Which spec?
I really want to know if step easing in the css solution would have avoided the CPU usage issue
We cover all of that at the end of the video. Only Safari optimises for step(), and it doesn't support animating content with CSS, so no 😀
@@jakearchibald yeah, I had paused the video to write that but I did watch to the end and really enjoyed it. Many thanks for doing this research and sharing the results!
thanks a lot for first Q answer => it's true
I just need to know what the in-ear monitors you're both using are...
Mine are Jabra 75t, Sumra's are Shures. IMO the Shures produce better sound & isolation, and it's what I used to use when commuting, but I find the Jabras easier to put in & take out, so they're better for calls.
When did Chrome stop drifting?
Or is it CPU architecture dependant?
I ran into this trying to work on an embedded ARM system with Chromium and it started to drift really badly after a few weeks of running.
Ended up using an external process for the events and just taking into account the delay in communication.
Ohh it's possible things change after weeks of running. Like I said, a random blocking task will cause a major drift (in all browsers)
You're going to get actual physical clock drifts at that time scale across separate hardware, it's why physical communication protocols have to have a clock signal. There's no software fix, except for something synchronizing them like NTP.
I can't help but wonder if some of the code was inspired by a StackOverflow post that I participated in.
/questions/37187504/javascript-second-counter
@@JonathanGray89 nah, it wasn't based on anything particular from stackoverflow
@@jakearchibald Ahh the second example is extremely close to my answer there... Thanks for replying, and awesome video btw!
สวัสดีครับ
The first method, even if the person starts the count (say go for exercise), comes back, and checks how long he has exercised for, if the computer did go into sleep mode, then it will not have counted the time when the computer was sleeping. The second method... so I think the concern is, since it is flooring the number, there can be a gap... (but 16 seconds? Even if it is off by 33ms, it take 30 seconds to go from floor(33.990), which is 33, to floor(35.023), which is 35 to show the gap (of missing 34)... so 16 seconds? meaning about 60ms slower each time?) So why not just make the interval 33 or 16 ms instead of 1000ms? If we are off by 0.033 seconds when we see the time, it is ok... but actually, when we floor the number, we can click start, and if we click "Stop" and it is 0.93s, we still see 0 and we are off by close to 1 second. So it is better to display 1 or 2 decimal places and know we can be off by that amount. Some time ago I checked, 33 ms was about the least amount of time between the handler being invoked in older browsers (IE8?), but now I do a `let start = Date.now(); void setTimeout(() => console.log(Date.now() - start), 0);` in Google Chrome, and it either prints out 1 or 2 on the MacBook Air M1, so it is 1/1000 or 2/1000 of a second
> So why not just make the interval 33 or 16 ms instead of 1000ms?
Then you're using 30/60x the amount of CPU required. Basically the same reason requestAnimationFrame is a bad solution (which on a 60hz screen will be running every 16ms).
@@jakearchibald ok, I got to read up on requestAnimationFrame() and the rest of your solution... often I thought other webpages are doing tons of stuff underneath including showing ads and eating up 300MB to even 1GB just for one page sometimes anyway
@@jakearchibald I just saw the use case of UA-cam broadcast count down from 2:34 to 0:00... it wouldn't look good if it is jumps from 2:12 to 2:10 directly... I wonder what if it is a solution such as updating every 160ms, so about 6 times per second, not too heavy on the CPU. If it is 100ms, then some second may appear as 0.9s and some appear as 1.1s but I guess not a big deal
@@winterheat why not use the solution I came up with?
@@winterheat the ideal solution in this video is only a few lines
How come Houdini didn’t come up in this chat?
Why should it have? What difference does it make?
I'm the billionth viewer
I don't believe I've watched that
Wow -- next level. Performance.now() and document.timeline.currentTime *seem* to refer to the same thing --- time since document loading -- but they are....slightly different. Different enough to be used distinctly (non-interchangeably) within your code. Jake describes document.timeline.currentTime as "like performance.now(), *but* [is] the time of the current *frame*." What is this distinction, exactly? I can't any reference to frames in MDN documentation for document.timelines ....
performance.now() will give a different answer each time you call it, even in the same synchronous JS execution, but document.timeline.currentTime will only change when the animation timeline current time changes, which only changes every frame.
Strap in:
drafts.csswg.org/web-animations-1/#dom-animationtimeline-currenttime - the definition of currentTime
drafts.csswg.org/web-animations-1/#ref-for-timeline-current-time%E2%91%A4 - where the current time of document timelines is defined
drafts.csswg.org/web-animations-1/#update-animations-and-send-events - the current time of document timelines is updated when this algorithm is called
html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model - that algorithm is called in step 11.10 of the event loop. Step 11 is all to do with updating the display, which happens when we need to create a frame.