Using const/let instead of var can make JavaScript code run 10× slower in Webkit
github.com
github.com
As an aside, I’ve been seeing a number of articles show up that basically go like “Chrome implements x efficiently but semantically equivalent y poorly, you should use always use x”. I really hope we don’t end up with JavaScript code being written to be optimized well on one particular implementation of a JavaScript engine…it’s interesting to see this in this area specifically because other VMs (e.g. Java) have knobs you can turn yourself but with JavaScript your code runs without much input on your side, so methods to influence the VM are much more specific…
Hopefully they're always treated like bugs and not something we need to work around.
The problem is that when implementing improvements it's easy to forget every mostly-equivalent thing and so miss obvious things (often the entire fix in the end is "if (canDoItFast) doTheExistingFastThing()"
I mean I'm sure they have a solution, since this seems like a rare occurrence.
I will note though that const/let are not mostly equivalent to var. They're often used interchangeably, but they work pretty differently as far as a JS engine is concerned.
Hopefully this changes, even slowly, but it'll probably take a while since Chrome's got so much share.
for (var i = 0, len = arr.length; i < len; ++i) { ... }
Hell, the whole premise for React (the virtual DOM) is based on outdated performance advice. Chrome has used a dirty-bit for DOM manipulations since a year or two after React came out; manipulating the DOM is within a factor of ~2-4 of setting a property on a JS object (and much faster than constructing & copying whole new JS objects, which incurs a GC cost), it's just that you really want to avoid interspersed manipulations & queries, which force a page reflow: parent.appendChild(document.createElement('div')); // Fast; ~50 us
let w = parent.innerWidth; // Slow, forces reflow; ~20 ms
let h = parent.innerHeight; // Fast again; no page modifications
parent.appendChild(document.createElement('div')); // Fast; just sets dirty bit
parent.appendChild(document.createElement('div')); // Still fast; dirty bit already set
But that's the nature of a lot of technical rules of thumb. They get stale as the underlying stack beneath them changes.It doesn't really matter nowadays anyway, because now I write my for-each loops like:
for (let elem : arr) { ... }
or arr.foreach(elem => { ... });
(Well, technically now I write Android & C++ code and do leadership/communication stuff, but I brushed up on my ES6 before getting the most recent job.) arr.foreach(elem => { ... });
is still always much slower than a for loop, having the callback call, isn't it?Do you mean:
for (let elem of arr) { ... }
Unless this is a joke about writing in Java & C++ (for which the colon syntax is valid) instead of javascript.I suspect the length access is just so fast that the difference between hoisting it out and not is immeasurable.
for (let elem of arr) {}
is actually quite a bit slower than for (let i = 0; i < arr.length; i++) {}
because the people who built the JS spec decided that there should be a brand new heap object created every iteration. At the time, there was thought that escape analysis would let them optimize away this object, but from what I can tell, ten years later, engines are really bad at it. Escape analysis is a heuristic, and it needs to be conservative.And yes, this isn't a micro-benchmark. At least in my application, performance is mostly bounded from GC pauses and collection, not slow code execution. Anything to reduce your GC pressure is going to be a good improvement... but note that modern frameworks like React are already basically trashing your heap, so changing out your loops in an already GC-heavy codebase won't really do much.
A good compiler will make it so that they are largely equivalent. In C-based languages this is one of the first things a compiler will do, and I am sure that every JavaScript engine does this kind of thing too when possible (actually, it may even have an easier time doing it because it may be able to skip pointer analysis).
let index = arr.length;
do {
index -= 1;
console.log(arr[index]);
} while (index > 0);
As a side note whether it takes longer to access a variable or object property is largely superficial depending upon the size of object because it implies creating a new variable on which to store that object property. There is time involved to invoke a new variable just as there is time involved to access an object's property.As an added bit of trivia in the 1970s a software developer named Paul Heckel, known for Heckel Diff algorithm, discovered that access to object properties is faster than accessing array indexes half the time. That was in C language, but it holds true in JavaScript.
for(var i=0,e;e=a[i++];){ ... }
Could result in problems if the array contained falsey values, but we just didn't do that.Nowadays, like I mentioned above, I'd just do
arr.foreach((elem, i) => { ... });
Which last time I checked was significantly slower than the for-loop, but I've learned my lesson about trying to optimize for browser quirks that may disappear in a year or two. :-)What are these object properties in C?
I do not, mostly because I came online at a time that Internet Explorer was already uncool and Mozilla was clearly better, and also I had no idea what JavaScript was ;) But I can only imagine what the browser monoculture of the time was like, viewing the echo of it many years later with Blink.
Web components are primarily about trying to add new virtual HTML tags to "extend the platform".
React and other frameworks are about trying to build interactive applications that require larger-scale UI management, efficiently, on top of the DOM, by defining pieces of that UI as a tree of reusable components (and using techniques that web components don't have available to them).
If I want to add a color picker to an otherwise static HTML page, I might use a color picker web component.
If I want to build a meaningful-sized app, I'd reach for React.
React is awesome because it allows feature-aligned separation of concerns (each component has a single job - render everything about a specific element - which is usually a well defined part of a specific use case).
Jsx is the best UI system ever in terms of productivity - speaking from experience: I’ve implemented production apps using dozens of UI frameworks/platforms - Html, WYSIWYG, Flash, WindowsForms, WebForms, Ajax, Asp.Net Mvc, Razor, WPF, Xaml, Silverlight, Knockout, Handlebars, PhoneGap, Ionic, Bootstrap, MaterialUI, Angular2, React w/ Class Components, React w/ Mobx, React w/ Hooks
I can tell you pros/cons of each of those. But at the end of the day I can develop an entire app in days in React+Hooks which would take me weeks in most any other.
Previously we had been doing retained-mode rendering. We would modify & shuffle around the pieces of the page.
React's components were there to let you re-render the app quickly. When state changed, a new render happened, with new elements emitted. You didn't think of what elements used to be there.
React was about the virtual dom. It was about creating an abstraction to let us not have to regard what was on the page, when we were deciding what is on the page. Incidentally, imo, that involved components, but components, while comprising numerous html elements, serve a very similar function to html elements (especially to web components), and were not, imo, a particularly novel part of React. Yes, there was a lot to their creation & implementation, there was a lot of tech work that went into making Components a thing. But components, to me, are far outshadowed by the vdom, by the performance & speed of a data-system designed to go diff & en-act desired state into the live DOM tree.
In kubernetes world, we'd call the vdom a controller. It reads the canonical state, the component tree, and insures the target DOM machinery is kept up to date & reflects this desired state.
The vdom is interesting, but the right history isn't starting there. The premise was the ability to build bigger, better, more reliable, easier to understand/maintain/refactor apps. Components with declarative render + top down data flow enable that, vdom is just the only way to make it work without being too slow.
Everything interesting & notable about components relates to the fact that they are immediate-mode things. Nothing else is particularly notable or interesting or important about them, has parity with what HTML Elements did/do.
Regardless of intent or deliberation, this, to me, is the clear & obvious technical difference that underpins whatever goals the team thought they were shooting for. It's the major characterization of how React was different from other webdev we'd tried. Everything else is downstream of that specific choice, for how to "draw" HTML: immediate-mode.
Immediate mode has a specific meaning that isn't really what React is doing. There's no concept of avoiding double buffering, immediate mode re-renders everything every frame while vdom avoids as much work as possible and updates are only triggered by actions.
But there's more interesting to them than just being declarative. A pure render function, a standardized prop boundary, top down data flow, and lifecycles (further improved by hooks, which are basically algebraic effects, which make real hot reloading work), are just as important, and all of those fall under "components".
Enyo, the WebOS framework, actually had a great declarative component model without vdom many years before React.
> Immediate mode has a specific meaning that isn't really what React is doing. There's no concept of avoiding double buffering, immediate mode re-renders everything every frame while vdom avoids as much work as possible and updates are only triggered by actions.
I've shown above others with similar framing to my own. I think you are over-focusing & refusing to see a similarity that is quite present. From a programmer perspective, react is about calling React.render(myJsx, domElement), again and again and again. How much more immediate mode does it get? "Redraw the world" is the premise.
As for double buffering, that's pretty much what the vdom is doing! There's the current buffer, there's the new world, and the vdom machinery is pushing the new buffer onto the old buffer once the render completes.
> But there's more interesting to them than just being declarative. A pure render function, a standardized prop boundary, top down data flow, and lifecycles (further improved by hooks, which are basically algebraic effects, which make real hot reloading work), are just as important, and all of those fall under "components".
These are all good characteristics of React, and part of it's total package that defines it. Interesting, yes! I think I am under-attributing the different feel of components versus where the web was before. A lof of this, feels, to me, like an incident discovery to a bigger phase change, from retained to immediate. I see a lot of those finger prints in other immediate mode rendering places. But it's very new to the web, best I can tell. I need to go back & re-review Enyo. Been a while. Ahh the heady days of two way data-binding!!
To hash up some specific points? Declarative is only part of it. The DOM is declarative. But the DOM is not immediate mode rendering, it's a retained system. The declarativeness feels normal?
Pure render functions are neat to see on the web, yeah. A lot of other immediate mode systems have this, geometry shaders being a large class of systems that often are pure functions.
The prop boundary seems like something the DOM already has & used a lot, if not quite so heavily bounded. This is just properties on elements, only expressed in a slightly different way: that distinction doesn't draw any major note for me, is an interesting twist, but just a re-embodiment of what was. That it's a harder boundary now doesn't do a ton for me- elements have been powered by their properties since DOM1 & it's very normal on Custom Elements.
Top down data-flow again feels like something relatively naturally emergent most scene-graph rendering systems, not unique to React. The web itself is a big top down renderer, always has been. It's weird because I both see tons of parallels between templates (which often nest or accept children or slots) and React components, but also I see that the feel is quite different, that components somehow are different, although I struggle to characterize how they really are and how this has changed webdev.
Not sure how I feel about lifecycle either. Willing to give this one to componenents. Definitely not a very immediate-mode idea, more like something we'd see in a scene-graph though.
This mentality sorta falls into the exact pitfall GP mentioned (i.e. what you really want is for IE/Edge to be significantly faster bringing speed gains for everyone else along the way, not for Chrome to be microscopically faster at the expense of every other browser)
There are real and significant costs in things like random access of .childNodes or bottom-up DOM construction in some browsers. At the height of the whole virtual dom thing, there was a lot of super browser-specific micro-optimizations going on, a lot of it w/ a heavy focus on Chrome, with IRHydra and stuff.
It got a bit ridiculous when I figured out that you could make a significant dent in one of the popular benchmarks at the time not by tweaking JS anymore, but by removing unused CSS rules from bootstrap.css...
But as you said, engines change quickly and I'm not sure it's really worth to be chasing micro-optimizations anymore. We're getting to a stage where the only way to get faster is to simply run less code. Some frameworks are getting the idea (svelte, marko).
I was a big fan of Polymer/WebComponents in 2014, but they kinda dropped the ball. In hindsight I wish they'd just implemented the React API directly in the browser. (Though depending on how Google vs. Oracle goes in the Supreme Court, maybe that'll become illegal, sigh.)
But overall, I agree with the idea of incorporating the ideas that are working into the platform. There have been various discussion over the years about how virtual dom could work as a native browser API, but unfortunately, the needle hasn't moved there at all.
Just in case you haven't been watching the space at all:
- https://43081j.com/2018/08/future-of-polymer
- https://www.polymer-project.org/blog/2020-09-22-lit-element-...
Also, I've seen the maintainer of lit-element flaming people in comment threads. It doesn't inspire confidence.
I was already on the fence about what to try next, and Svelte is at the top of the list if I have any problems with lit-element... lit-element just seems a bit lighter and more standards-positive so I wanted to give it a go.
These days though, I'm basically not considering investing in any libraries/frameworks that don't offer SSR with competent hydration. IMO it's the closest we get to the holy grail in frontend -- separation from the backend (which I argue is a benefit), and the SEO-friendliness, nojs-compatability, and speed of server side rendering.
lit-element doesn't have a good SSR story just yet (it's experimental[0]), and Svelte has sapper and ElderJS[2], so it's already ahead there...
[0]: https://github.com/PolymerLabs/lit-ssr
We should build templating and DOM updates into the browser, but we should do better than React.
The benefit of React (if any; it has a lot more overhead than vanilla JS+DOM) is that you can't hit that specific pitfall of forcing layout interspersed with DOM or style manipulation.
Could you explain why?
Is the following true even when the parent is already in the DOM?
parent.appendChild(document.createElement('div')); // Fast; ~50 us
let w = parent.innerWidth; // Slow, forces reflow; ~20 msBut let's imagine instead that the node added to parent was a div with a text child. Or, slightly less obviously, the div is getting added in a document with a style rule of `div { width: 1000px;} In that case, the width could change. So the browser engine's options are:
1. Return a stale value from parent.innerWidth for now, and just lazily update style at the next event loop iteration. 2. Synchronously update style and layout (note, this update is not as expensive as a from-scratch layout), and return the up-to-date value of parent.innerWidth
It turns out that, historically, the earliest browsers with scripting did option (2), and websites came to depend on it. So browsers had to keep on doing it, and so forth. Many folks in the web standards world would like to find away out of this dilemma, where DOM mutation doesn't risk this kind of performance hazard.
You could also imagine an extra bad option: 3. Every time the DOM (or the CSSOM) is mutated, synchronously update layout.
This is super expensive in the face of repeated DOM mutations. Repeated DOM mutations (e.g. adding multiple elements, setting multiple attributes) are way more common than repeatedly getting style/layout-dependent attributes. (3) has the same observable functional behavior as (2), but it's a lot slower, because it will do a lot of unnecessary layouts.
I'm not totally sure if this explains everything you were wondering about, but I hope it helps some.
So browsers normally optimize the first call, but are forced to update at the second one.
As a related note, are you considering the content-visibility property for webkit?
This isn't true though, useLayoutEffects that perform a read/write littered through your code is going to quite easily induce layout thrashing and there's no way of splitting this throughout a tree
You're late by many years, most frontend developers have been explicitly optimizing for Chrome for a long time now.
> I think I can reproduce this behavior on Figma's code base too. I've never noticed because we use Chrome pretty much exclusively for development.
We're already there. Chrome dominates so much of the dev market, so naturally v8 is what people are optimising towards.
Because of a perf bug in WebKit they added a flag to generate less correct code that is fast in WebKit.
If anything, it's a case of optimizing towards WebKit.
But really, it's a case of performance bug in WebKit and them adding a work-around.
But, I think a more relevant example might be people who write code prefixed with comments like "Destructure this manually. At -O2 on GCC this allows the first field to be placed in a register" which is putting waaay too much dependence on the internals of a specific implementation, beyond even whatever extensions a particular compiler claims to support.
Because I have seen those kind of discussions too many often, even back on Usenet days, like the example you point out.
Well we know pizlonator hangs around on HN from time to time :) Especially on topic of JS and Webkit.
I'm afraid that ship has already sailed.
This seems bad.
I mostly switch to Chrome when debugging PWA. Other then that Firefox gives me better UX.
We tend to think that newer is better and faster, but it's also wise to think that old apis had to be fast on old hardware. Hardware that is not the target of new APIs.
IE 8 got querySelector before IE 9 got getElementsByClassName, so it’s not really an old vs. new thing, and I don’t think anyone expects querySelector to be faster than getElementById.
Also, block-scoping is a language feature that's fundamental to virtually every other modern language out there. This isn't some high-falutin' new API. This is basic.
> I think I can reproduce this behavior on Figma's code base too. I've never noticed because we use Chrome pretty much exclusively for development.
I'm more and more concerned that the monoculture of chrome for developer tooling is making the web a worse place...
while true {
let hello = "world";
}
The scope would keep getting freed then re-allocated, which will be slower than simply continuously setting an existing variable's memory location over and over again.`let` can be changed so cannot be optimized out this way (at least not without doing more complex analysis and proving that the variable is not being changed).
node let.js 7.00s user 1.35s system 51% cpu 16.272 total
node var.js 7.21s user 1.22s system 55% cpu 15.183 total
go run main.go 0.80s user 0.94s system 17% cpu 10.064 total
I feel like that is about the best we can do without harming backwards compatibility. They are there when needed, but we are stuck with the ambiguous "number" type in general.
It's kind of pointless to even try and optimize for things like this because there's a very good chance that it can change in the news few weeks/months/years.
It also allows me to build sites which work in Netscape 3.0 and up without any special compatibility modes. :)
For instance, "var" may seem easier, but "const" is definitely less complicated. "var" has unintuitive edge cases with closures that "const" does not. `var` is also scoped to the nearest function rather than the nearest block, a "feature" that has tripped up many a new JS developer. `const` has none of these surprises.
Well, you can redefine it by editing its source code file on disk. (Unless the file is read-only.) But it's a lot faster to be able to redefine it interactively inside the DevTools console and I wish there was a "dev" setting that allowed this. Also, you said "you declared it" but usually it wasn't me who declared it constant. Rather, some other programmer thought `width=800` or `numColumns=80` should be declared constant. "Variables aren't; constants won't be." No coder is an island: one programmer's bad `const` can't make life harder for other coders. Related: One reason I use Chromium browsers instead of Firefox is because in Firefox DevTools console once you declare `class A {}` you can't interactively change it to, say, add a constructor like `class A { constructor (foo) { this.foo = foo } }` During learning and experimentation this is lousy; you can't interactively iterate at the console prompt like a proper dynamic language. You must instead put your code in an external file and set up a "save file and re-run everything" system just like static languages like C++ require.
You can replace A with: A = class {...}
This works with any browser.
BTW. In Firefox there is a multi line editor. With code completion. I actually find more convenient to test large code fragments in fox because of that.
class A {}
has a huge difference from code written like A = class {}
In classical JavaScript consider how small the difference is between function foo () {}
and foo = function () {}
Function hoisting only works for the "function foo(){}" form so there's a difference but they intuitively work essentially the same and either form works well when experimenting in the console. If enough rules like "class A {} is very different from A = class {}" are piled onto JavaScript it's going to become like C++, a language so complex that people would rather learn a simpler language so they can focus on the complexities of their actual problem space rather than the complexities of their language. A new language called Zig is now trending upward because a growing number of programmers would rather have a language they can 100% understand after a reasonable time investment rather than a language like C++ where even after a 20 year career with it and implementing portions of a compiler for it you still get painfully surprised by some obscure rule interaction you never dreamed to imagine possible.Would love to see what semantics are making let/const abysmal.
(I don't care if I get a million down votes, I really don't. You're all misusing const and you know it. It's a shitty hack turned into a fad by someone who didn't understand the language.)
const foo = {};
foo = 42; //not allowed
foo.bar = 'baz'; //this is fineconst x = {immutableValue}
That tells me the thing is fully immutable. Whether it’s a global variable or not, that’s still nice to know when reading the code! Likewise:
let x = {immutableValue}
const x = {mutableValue}
These tell me different things. The first tells me I should look out for re-assignment, the second that I should look out for mutation of the value.
In contrast, this:
let x = {mutableValue}
Clearly has the most cognitive load. Anything could happen with this. If it’s a bigger, more complex function/whatever, “what happens to x, it could be anything” is just one more thing to fit in my head as I try to figure out what this code does. Enough little uncertainties like this, and I have to give up understanding by reading entirely, and resort to a debugger. Had the author limited the scope of what was possible, it’d be a bit easier to understand, with really no drawbacks. It’s not the end of the world, but “small, simple, easy thing I can do to make the code easier to understand for future readers” ... why not do that?
Likewise, I'm saying that a minority is vocal because reading a rant from a OSS celebrity either reaffirms their preconceptions or sways them through aggressiveness (both of which are objectively true, as I have witnessed cases of both), but you're accusing me of projecting (presumably because you think that I'm making a claim about you personally - which is not true).
Consider that some of the words you use are weasel words ("overusing", "fad"), which, IMHO imply a tautology (i.e. "I think X, therefore X", as opposed to "The facts are X, therefore Y"). I originally said that the spec is clear about what const and let are. Implying that following the spec is a fad is needlessly derogatory and doesn't address the double standard with regards to the confusing-ness of const vs let.
It’s the same behaviour in many other languages.
Why would rebinding variables ever be a good thing?
const fixed the pointer but don't prevent user change the value in the storage it point to.
It is possible to define a pointer to an object whose contents may not be changed. This is done with ‘const * type object’.
If you want a pointer whose value must not change, but do not care about the contents of the pointed to object, you would write ‘type * const object’.
Of course, in C++ there are const references as well, and in this case const also means you can't modify the referenced object (in any case in C++ you can never re-bind a reference).
This is a pointless discussion.
const foo = #{};
foo = 42; //not allowed
foo.bar = 'baz'; //not allowed
https://2ality.com/2020/05/records-tuples-first-look.htmlYou're welcome to go use another language that doesn't have a formal spec, if you're so inclined to disagree with what the spec for this one says.
Also, in general, using the most limited constructs that fit your use case makes a tonne of sense for code cleanliness, in any language. For example, in a language supporting private/protected/public properties and methods, you should always default to private, as that’s most limited and easiest to reason about. Then make it more public as necessary. Same goes for const vs. let - const is more limited, it’s a better default choice, only use let if you actually need to re-assign it. Using const means there’s one less thing for readers of the code to worry about, it’s a variable that CAN’T be reassigned, the same way you don’t have to worry about a private method being called externally.
This is, essentially, the “principle of least power.”
Also, if you’re talking about leaving mutability like this in the code long term, this really only applies to GLOBAL mutable state, and global mutable state is a terrible thing for code maintainability in any language. If it’s not global, you can’t fiddle with it later in the console anyways. Littering your program with global mutable state just because “someday, someone may want to fiddle with this in the console, and this makes that slightly easier”, that’s just not a reasonable argument.
const improves readability, and the ability to reason about code. It lessens the need to exhaustively scan the code looking for other assignments to the variable.