Are you telling me a readonly property is wrecking my performance?(shub.club)
shub.club
Are you telling me a readonly property is wrecking my performance?
https://shub.club/writings/2026/july/check-your-scrollheight/
37 comments
Even without this performance hit, an often used way for implementing 'auto scroll to top/bottom' is to first check if there's no other stuff coming in before starting to actually scroll. This goes unnoticed by the user but drastically reduces the number of updates (and in this case, number of calls to scrollHeight) needed when a lot of data is coming in in batches. Principle is like: receive message, add to buffer and start timer of like 50ms. Upon timer tick copy everything from buffer (which in the meantime can have accumulated more data) to rendering and only then update scroll.
IME, this absolutely doesn't go unnoticed, except in trivial cases.
I was talking message logs for example; is that trivial? I.e. what do you mean exactly? Do you notice the difference between 1 line of text being added every 16.6ms vs 3 lines every 50ms? And if so is that a problem?
Drives me nuts whenever I reach the bottom of a scroll window and I can feel the network call happening before stuff starts loading again. It's 8ms from me to the local CDN and I've got gigabit, but it feels like every dang site is slow as hell these days.
Preloading certainly does seem to be a lost art at times, yeah. It's just so complicated, we might never climb back to such heights.
Indeed. It's the same principle[0] the other way too - start a timer from user typing for an autocomplete function and only refetch after the timer expires.
[0] https://developer.mozilla.org/en-US/docs/Glossary/Debounce
[0] https://developer.mozilla.org/en-US/docs/Glossary/Debounce
Yup. Anything layout-related could be costly to read, and could force a layout to occur if modified since the last read (even within the same synchronous javascript code). Most things are not deferred until the next layout pass, which is one of the reasons virtual DOM got popular: it batches changes for you.
If scrollheight is not a performant property, than it shouldn't be a property. It should have been a method called calculateScrollHeight() or something to indicate that it is not cheap.
Element.scrollHeight first appeared in the DOM API with IE6 in 2001 as far as I can tell. That browser was the first really modern one, with javascript and CSS that could drive a dynamic layout and they innovated with things like AJAX and vector graphics. The web browser had matured from a document model with live elements to a programmable operating environment.
The API implementation was klunky though, where DOM Elements attached to the DOM became 'live' objects. Reading some of the properties would necessarily require reflowing the page in order to calculate the value. It's interesting to think about all the ways the DOM thread can stall for synchronous API's. The Paul Irish gist is always the top hit when I search:
https://gist.github.com/paulirish/5d52fb081b3570c81e3a
The mouse event ones always get me.
I'm always impressed by how old sins like element.scrollHeight can get papered over with newer API's. You can use ResizeObserver or requestAnimationFrame() to time your reads of the property and hopefully get the reflows for cheap or free.
The API implementation was klunky though, where DOM Elements attached to the DOM became 'live' objects. Reading some of the properties would necessarily require reflowing the page in order to calculate the value. It's interesting to think about all the ways the DOM thread can stall for synchronous API's. The Paul Irish gist is always the top hit when I search:
https://gist.github.com/paulirish/5d52fb081b3570c81e3a
The mouse event ones always get me.
I'm always impressed by how old sins like element.scrollHeight can get papered over with newer API's. You can use ResizeObserver or requestAnimationFrame() to time your reads of the property and hopefully get the reflows for cheap or free.
This API dates back to 1999 if not earlier.* Lots of APIs back then weren't designed very carefully, and now we're stuck with them.
* According to https://github.com/mdn/browser-compat-data/blob/v8.0.6/api/E..., Internet Explorer 5 had it. Unfortunately, I don't know of any way to look up which Netscape versions did.
* According to https://github.com/mdn/browser-compat-data/blob/v8.0.6/api/E..., Internet Explorer 5 had it. Unfortunately, I don't know of any way to look up which Netscape versions did.
Why? Plenty of web APIs have been redesigned since then. Nobody is going to stop the browser makers from introducing an alternative and deprecating the old one for removal 25 years from now.
If they can remove <blink> and <marquee>, they can provide an alternative to this.
If they can remove <blink> and <marquee>, they can provide an alternative to this.
Agree, but also easy to say after it been in use for 20 years or what it is. Doing this change today would be a monumental migration, unless you provide fallbacks, and then what's the point?
Properties are to allow dynamic actions for what appear to be simple variable accesses. They don't magically make those as fast as accessing a variable; they are a syntactic convenience to allow assignment and using the value implicitly rather than having to invoke a function/method. They could have cached the value and kept a dirty flag, but then everything that affected the value would have to be sure to mark it as dirty or result in subtle bugs.
The biggest performance bomb you can have in your code is a loop that does something like
for (...) {
el.style.height = `${something}px`;
whatever.value = el.style.offsetHeight;
}
This forces the browser to recalculate layout multiple times in a single frame. Separating layout changing code from measurement code will help a lot here (most frameworks out there have solved this so we don't have to be too concerned about it though).Or as Andreas Kling (jokingly) put it[1]: “STOP INTERLEAVING STYLE CHANGES AND LAYOUT METRIC QUERIES ASSHOLE”.
[1] https://nitter.net/awesomekling/status/2021932080742142056
[1] https://nitter.net/awesomekling/status/2021932080742142056
This is true, but a much more common reason is that you have a self growing textarea and Firefox didn't support field-sizing: content until recently, so you had to let JS resize the textarea on every keypress.
I miss Firefox Quantum that effort should have continued, stuff like this makes for great candidates for that initiative to resolve.
I had a severe performance issue with streaming messages in a custom chatbot UI. I was able to resolve by enqueuing the streamed token chunks and processing them in batches at requestAnimationFrame boundaries. Forcing an immediate repaint on every received chunk seems to break down in a non-linear way. For small conversations with few messages you almost can't tell the difference.
Layout-triggering properties used to be common knowledge for frontend development. Now you just try the next npm library that promises to be a little faster…
> I just assumed that readonly properties will always be pretty performant in general.
This is also the case for variables. var is faster than const, because for const (and let) the engine must do additional work (related to enforcing block-level scope, if I am not mistaken).
This is also the case for variables. var is faster than const, because for const (and let) the engine must do additional work (related to enforcing block-level scope, if I am not mistaken).
To my understanding today it depends on the JS engine and more importantly what JIT stage that code is in. JITs optimize const/let in ways that they can't optimize var now.
Which is to say that if someone tells you that you should in general never use const/let for "performance" they are probably wrong, but yes there probably are still edge cases and microbenchmarks that make var look faster on some engines.
Which is to say that if someone tells you that you should in general never use const/let for "performance" they are probably wrong, but yes there probably are still edge cases and microbenchmarks that make var look faster on some engines.
Which is why Zig's top feature on its webpage is "No hidden control flow."
Ironically, the 'no hidden control flow' was one of the arguments against Zig in the recent Bun rewrite to Rust.
https://bun.sh/blog/bun-in-rust
So sometimes hidden control flow is needed.
https://bun.sh/blog/bun-in-rust
So sometimes hidden control flow is needed.
> Ironically, the 'no hidden control flow' was one of the arguments against Zig in the recent Bun rewrite to Rust.
Yeah, but that makes sense: if you want "hidden everything", which they appear to want due to now having a code base that has never been read by a human, then a subset of "want everything hidden" is "want control flow hidden".
Yeah, but that makes sense: if you want "hidden everything", which they appear to want due to now having a code base that has never been read by a human, then a subset of "want everything hidden" is "want control flow hidden".
Honestly I am pretty sure AI would be better at zig than rust since zig has no hidden control flow which means that AI has the full context of any given function without having to find traits. The rewrite to rust from bun is as much of as PR stunt as fustration with DX of zig.
Every time I want to interact with zig code I just have the AI do it since I honestly can't be asked to change 3 lines and around 3 to 5 characters when it's a single keybind in every other language which in turn has lead me to experiment quite a lot 'writing' in zig. It's rather pleasant to look at, however, I wouldn't want to write code myself.
Rust is by far the most enjoyable dx experience since everything usually 'just works' across machines and even architectures while having a strong sense of assurance that compiled applications will work as expected before ever running them once. node/bun/whatever is probably the worst here since compilation means nothing for runtime as undefined symbols will happily 'compile' (transpile?).
Every time I want to interact with zig code I just have the AI do it since I honestly can't be asked to change 3 lines and around 3 to 5 characters when it's a single keybind in every other language which in turn has lead me to experiment quite a lot 'writing' in zig. It's rather pleasant to look at, however, I wouldn't want to write code myself.
Rust is by far the most enjoyable dx experience since everything usually 'just works' across machines and even architectures while having a strong sense of assurance that compiled applications will work as expected before ever running them once. node/bun/whatever is probably the worst here since compilation means nothing for runtime as undefined symbols will happily 'compile' (transpile?).
> Honestly I am pretty sure AI would be better at zig than rust since zig has no hidden control flow which means that AI has the full context of any given function without having to find traits.
You say that as if having to find traits was a problem ... for a bot.
You say that as if having to find traits was a problem ... for a bot.
Traits are harder to find since multiple traits can apply, but the one strictest one wins. They also have to be followed from generic types which adds a whole other layer of complexity, without an LSP it's a lost cause.
> Traits are harder to find
Sure, but what matters is whether that's too hard for an "AI".
Sure, but what matters is whether that's too hard for an "AI".
1 read vs realizing that lsp exists and using it correctly, even if ai is smart that's more token burn and it is slower making zig 'better'.
It also has to realize that "Drop" exists and know what it does since there's nothing in code that clearly says "this object is automatically dropped by the compiler on scope end.
It also has to realize that "Drop" exists and know what it does since there's nothing in code that clearly says "this object is automatically dropped by the compiler on scope end.
That tells you more about the people writing Bun than it does Zig or what's "needed" for people actually writing and reading code. Bun dev is not just hiding the control flow, their goal is to hide all of the code so that no human reads it, ever.
I mean, there's an easier way of doing that than rewriting to another language, just make the repo private.
Sometimes I think about this: using readonly is generally recommended at the architectural level. But it's technically difficult to choose the right trade-off when you need to remove that abstraction. The reason readonly is recommended is because it's a form of encapsulation, and using it means you're promising immutability for that state.
Sometimes you break down abstractions to reduce actual costs, but that can end up being bad for the overall structure. So you say you're breaking it down because the cost ruined the perceived performance, but it's always difficult to decide whether it's better to keep the overall beauty or to break the abstraction locally.
Speed and user UX are important, but if it's a screen the user is constantly watching, you might remove the abstraction. However, if it's something like a waiting screen after payment, you'd probably keep it. In the end, what matters is the user flow
Speed and user UX are important, but if it's a screen the user is constantly watching, you might remove the abstraction. However, if it's something like a waiting screen after payment, you'd probably keep it. In the end, what matters is the user flow
> readonly property
Um, what readonly property?
Um, what readonly property?
The scrollHeight property: https://developer.mozilla.org/en-US/docs/Web/API/Element/scr...
Afaik let in JavaScript also has a sizable penalty too, if not quite as bad as const. Var on and on.
Let and const have a "temporal dead zone" before initialisation that distinguishes them from var, which is just undefined:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
Runtime TDZ checks can be performance issues:
https://vincentrolfs.dev/blog/ts-var
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
Runtime TDZ checks can be performance issues:
https://vincentrolfs.dev/blog/ts-var