59 comments

[ 3.3 ms ] story [ 79.1 ms ] thread
The other way to deal with this particular problem is to do:

    [].forEach.call(myNodeList, myCallback)
Good tip, but while it works in this specific case, it isn't generally equivalent. "for of" works on anything that is iterable. This will only work on array-like objects.
(comment deleted)
And the major reason I use for-of is because you can `await` within a loop easily.
Awaiting in a loop usually is not a good idea. Map with Promise.all / any is probably much faster.

edit: now reading about for..await..of. Interesting, I never used that.

Even outside of for..await..of you still need to do async things inside a synchronous-like loop.
I get it. It’s roughly 5 times the lines of code and there are better polyfills for IE (sounds like an easy bug to fix in Babel) but what’s the actual performance impact when it runs?
It looks like Typescript also transpiles to a similar try..catch..finally pattern:

https://blog.mariusschulz.com/2017/06/30/typescript-2-3-down...

..with the `--downlevelIteration` flag set and ES5 or ES3 set as the compilation target. I should have clarified.

From the article:

"TypeScript 2.3 introduced a new `--downlevelIteration` flag that adds full support for the ES2015 iteration protocol for ES3 and ES5 targets. `for...of`-loops can now be downlevel-compiled with correct semantics."

I've learned the hard way to check Babel's output for sanity.

Sometimes its fine.

Other times it's madness or pulls in a massive polyfill. That's sort of inherent to making a transpiler: they need to match the spec. I usually just need one specific behavior that could be written in a 3 line helper function.

They have a "loose" mode for most compilations that can benefit from them.

In loose mode they might not follow the letter of the spec, but can cheat for speed or size.

Yep you got that right!

Only thing I can think of (since changing the default to be non-spec would be odd) is just warning on larger code size outputs and saying you could try loose mode in certain plugins/situations. Lastly would be do start creating a tool that does runtime checks to determine how small the compiled output should be

Why would a polyfill of Symbol make this work? If a browser doesn't support Symbol, it won't have a property named [Symbol.iterator] on DOMNodeList, right?
Because the 259kb polyfill also adds iterator support to everything.
My polyfill to support back to ie11 takes less than 100kb, raw. If you're polyfill is 259k, you're probably doing something suboptimal
> but I sometimes wonder if we’re living in a collective delusion that the current toolchain is great when it’s really just morbidly complex.

Epiphany of the year there.

> I’m not sure this is an issue with Babel per-se, but maybe a more meta topic of the cost of polyfilling.

The overarching problem is that the web platform itself is so fragmented, with different levels of support for various features, that polyfilling and transpilation are important. Strictly speaking, if every relevant browser supported for..of natively, there should be no need to transpile.

You could avoid the whole for..of problem by writing ES5 JS code, but for many people that's apparently a worse option than dealing with Babel.

> You could avoid the whole for..of problem by writing ES5 JS code, but for many people that's apparently a worse option than dealing with Babel.

Yes, and I don't understand that at all. I don't have any problems writing plain old ES5 code.

You don't, I do.

TypeScript has been a revolution in the way that I write client side code, been able to write in a superset of a language I already know against existing code in a way that I and my tools can reason about and with faith (backed up by experience) that I can write it once and know it will work basically everywhere is huge.

Is it a perfect tool? by no means, it's output messages can be a bit opaque.

That I can also use features now that won't make it into browsers everywhere for a while and that they'll gradually replace those features as browser support for them increases, ES6 was a good step in the right direction (and you can target it from TS and in many cases there aren't that many differences between clean ES6 and TypeScript's output but I can also set the target to ES5 and use it).

FWIW: https://github.com/Microsoft/TypeScript/pull/12346

https://basarat.gitbooks.io/typescript/content/docs/types/li...

> You could avoid the whole for..of problem by writing ES5 JS code, but for many people that's apparently a worse option than dealing with Babel.

The big reason I'm so excited about web assembly is that now we can escape the albatross of JS/ES. I've started playing with rust targeted to emscripten and it's such a breath of fresh air (both the language and toolchain).

I was just thinking the same thing while reading this article but, upon thinking about it, you'll likely have to ship the stdlib of whatever language you're targeting as well as your code (although I'm sure some interesting omissions of the stdlib can be automated determined based on the actual use). That's traditionally why Rust binaries are large (they statically compile in their standard library).

Statically compiling a simple hello world C program results in a 700+ KB executable. By researching And adding various options to gcc to reduce size, I was able to get the binary down to just under 650 KB (277 KB gzipped), so progress! :/

There are undoubtedly smaller libc implementations[1], but I think I've illustrated the problem adequately. Having an entire runtime in the browser gives JS a fairly large advantage as I see it, at least currently.

1: https://en.wikipedia.org/wiki/Dietlibc

Rust binaries are also large because they copy some of C++'s bad decisions:

1) Generics are instantiated and compiled separately with types for which the generics have identical behavior.

2) Functional abstraction is encouraged (not a bad thing!) and inlining heuristics are overly aggressive to compensate.

3) By default, almost every function call has to deal with the possibility that the callee might throw an exception. This is smaller than 1) and 2), but it still matters.

Except that rust doesn't have exceptions.
Rust actually does have exceptions. Rust unwinding is implemented using LLVM/Dwarf exceptions, and can be caught using std::panic::catch_unwind:

https://doc.rust-lang.org/std/panic/fn.catch_unwind.html

Idiomatic Rust doesn't use them for error handling, and they can be disabled entirely with a command-line option, but the implementation still has to deal with all of the issues that come with having exceptions.

> you'll likely have to ship the stdlib of whatever language you're targeting as well as your code (alt

True. However from my POV the code bloat is outweighed by the fact that rust tooling is simply that good. Javascript is, IMO, a nightmare to develop for both due to poor language choices and due to the really mediocre tooling.

My new favorite JSism (thanks HN!) was this:

['1','2', '3'].map(parseInt)

In a better world, the browser-borne language with access to DOM and other features would be small, stable, and the "transpiling" would be plain compilation.

Unfortunately, both Netscape and MS wanted control, and then it was too late.

This wouldn't be the right solution if you wanted to avoid the problem in the article. For-of has subtle semantics around exceptions. When you have to spell them out explicitly at every call site, which is what would happen with a small VM, you get the code bloat described in this article.
I'd rather have a language which is not specific to the browser, and browsers offering a compilation target, like LLVM / CLR / JVM. A simpler compilation target would hopefully lack semantic subtleties.

Now we can have this, too, of course, and e.g. run Clojure or Elm in a browser, compiled down to a safer / better supported subset of JS, or use ES7 with polyfills. But the intention of browser vendors is currently different; WebAssembly, while a small and well-defined VM, is not intended as a JS peer, let alone replacement, though.

> I'd rather have a language which is not specific to the browser, and browsers offering a compilation target, like LLVM / CLR / JVM. A simpler compilation target would hopefully lack semantic subtleties.

I would rather claim that all common VM targets, such as LLVM, CLR, JVM etc. are so full of semantic subtleties that the subtile parts of JavaScript are quite harmless against them.

(comment deleted)
I also noticed that Babel's transpiled ES6 classes suffer from abstraction leaks quite a lot. This is especially bad if you have two classes in the same class hierarchy, where one class resides in a babelified module, and another one in a native module.
Try out dart or typescript.
But those just transpile to JS as well and likely suffer the same problems.
Go one step further, eliminate the polyfill, and just write a traditional for loop.

Several years ago, I decided to try out the then-newish E4X for-each syntax. It was only implemented in SpiderMonkey, but that was okay because not only am I not a web developer, this project wasn't meant to run on the web, either—it just happened to be in JS. I knew for a fact that this code wouldn't be running anywhere except SpiderMonkey, so I dug in. After some research, though, I discovered some problem. Some subtlety that made it a non-starter. So I kept writing traditional for loops.

A couple years later, I was starting work on a kind-of-new codebase; it was essentially greenfield, but I knew I was going to end up cribbing a bunch from an existing codebase. When my fingers began typing out the familiar `for (`, I paused. This would be the first for loop introduced in the project, and I should maybe make a decision to get things right from the beginning. By now, for-of had arrived, and it was being standardized in ESx proper. Should I maybe do another survey of the lands and look into giving the new iteration syntax a shot? It was a simple iteration: a straight pass through a list of items, one after another. And in addition to being able to address items by index, they all had the equivalent of a "next" pointer for the item that followed. So I deferred; I went with a while loop instead, chasing "next" pointers to avoid having to do some in-depth research on the then-current state of `for`. I got pretty far doing this, actually, and was able to go the next few days writing all my loops in this way, just to avoid tacitly endorsing either for-of or traditional for loops.

After several days, however, that ran out, and I came to point where I would not be able to sidestep it and needed to make a decision. So I stopped, and dug in again. Another problem surfaced: for-of was only implemented in the newest releases of SpiderMonkey, and the only enhanced iteration syntax available in previous releases that this code would need to run on was the now-deprecated for-each. I thought really hard about this. Was there some part of the language that could be exploited to find some clever way around this? No. Maybe consider a transpiler? After all, for-of would be standard, so eventually support is going to be widespread enough that even the lowest common denominator will have native support...

But then I stopped. Because the whole thing is stupid. Just write a damn for loop. What was the real problem? Trying to save time? Except how much time was already sunk into trying to shave off, what? Like one second of typing? Maybe two? And the tradeoff for that development time gain is (a) a runtime performance penalty; (b) having to either throw everything older than version x by the wayside or spend all those extra seconds and more to put some transpiler crud in place; and (c) code that's harder to understand for anyone who doesn't fetishize staying on top of the newest language features. The latter also involves people who typically work in other languages and would probably immediately understand `i = 0; i < n; i++` (even if it's not idiomatic for their preferred ecosystem), but likely wouldn't immediately grasp the nuances of the new iteration protocol.

What's wrong with `i = 0; i < n; i++`, anyway? It's not even that verbose. What it is, is predictable. Which means your eyes get used to it, just like any other pattern. Except traditional for loops are powerful, too, because you might want to do something, say, for every fourth item. Or maybe skip the first. Or iterate backwards. And you can. In each case, anything not fitting the traditional pattern will stick out.

Anyway, I still write traditional for loops today. I don't feel like I'm missing out on anything, and my thoughts very much echo the sentiments in an essay published a few years ago on a similar topic: "Java for Everything"...

One point towards using Array prototype functions (map, filter, reduce, etc.), is that they are often much more readable.

For loops are harder to skim.

Still, I don't understand the for loop hate. I have seen people argue against 'pointless optimizations', in the name of readability. And while yeah, arr.filter().map() is probably faster to comprehend, you're still iterating through the array twice. Pointless optimizations, not so much when the array is thousands of elements long.

Iterating a long array twice to do two simple things can often be faster than iterating once to do a more complicated thing. See for example https://stackoverflow.com/questions/8547778/why-are-elementw... .

The wonders of modern processors and compilers.

That link is more about the pitfalls of page alignment. Caches being a very dangerous topic.

Getting the data aligned in a cache friendly way returns those loops to the expected behavior of one iteration being faster than two.

I’ll never understand this. JavaScript’s threading model (none) makes it ridiculously easy to implement coroutines/iterables so that map, filter, and co can be efficiently written.

How did Microsoft manage to realize the necessity of coroutined iterables in a threaded, impure, multiprocess-capable language like C# (without breaking backwards compatibility, might I add) with Linq, and yet JavaScript couldn’t?

(If I may be permitted to go slightly off topic here, I also think it’s a damn shame Rust devs were so eager to release v1.0 of their shiny, new language to attract the masses that Rust - the language, not stdlib - shipped without native concepts to accommodate iterables and async/await. Unthinkable this side of 2010 for a language that aimed to be the second coming of C++).

Sometimes I think that it might actually be fine to just stick to JQuery and ES5 after all.
Developer: Non-moving standards, motherfucker, do you speak it?

Web: YES!

Developer: Describe what well-established platform looks like!

Web: W-what?

> First stop, I looked at babel-polyfill.js but was surprised to see it is 259kb unminified/uncompressed. I’m sure it does a lot, but the entire JavaScript bundle for the whole site is only 93kb unminified/uncompressed. It’s hard for me to recommed a +300% increase.

I just tested it, and the minified and compressed babel-polyfill is 34K. Going from 12K of JS to 46K of JS just to make modern loops work may be a little unsettling, but those are both really small numbers these days. The blog post is about 10x bigger. It's also worth noting that babel-polyfill adds lots of other useful stuff; there are plenty of similar problems that you'll never have to think about if you're willing to drop in the whole babel-polyfill.

I think that for most use cases, bundle size is a distraction and it's best to just focus on building things. You should still keep an eye on bundle size in case it gets out of hand, but I don't think that's the case here.

Agreed, you can always optimize for size later... And conditionally load the polyfill, etc.

All that said: dropping all the fancy logic and just going with plain JS has some nice simplicity to it. In the JS world you can spend a lot of time on tooling.

I agreed with you until I had to be wary of performance on mobile devices where the user may have to pay 10s of milliseconds just on boot per every 15kb of javascript you add.

At the end of the day you ask yourself "do I really need to add 34kb for 'for..of' or can I get away with a small concession?" like the blog post asked. You may find that it might not cost you much to pay attention to your trade-offs.

You can set babel to `loose` mode to avoid having the transpiled output use `Symbol`. What I would love though is a "super-loose" mode which takes for-of and transpiles it directly to for-in -- i.e. excluding any support for iterators and just functioning on indexable collections with a defined `.length`. That would be enough to get concise output in most cases if you're confident you're always iterating over static collections.
for..of works with any iterator which is essentially the interface { next() }, thus arbitrary. If you limit it to only indexable things, then I'm not sure why you would make it a point to use for..of.
One upon a time web developers had to wait for browsers to support new features. Using Babel does not automatically mean that this no longer true.
The problem is that IE11 is not eol until the end of 2020, and management can be loathe to drop support for it while it still makes a blip in the traffic, even a tiny one.

It's one thing to wait for the actually supported browsers to all catch up on a new feature, but IE11 is abandonware that can't be abandoned, and the situation just keeps getting worse.

You know its a good language when you have this much trouble writing a simple for loop.
You're confusing the myriad of client runtime environments with the language.

Whining about the former is to just whine about history, like how 'referer' is misspelled.

What is iterator.return()? That seems non-standard?
The transpile issue on mobile is greatly overstated. The vast majority of mobile browsers (by market share) have very good es6 support. Instead of one version, websites need one with minimal transpiling and one with full translation for older browsers.

Sure you need a little more setup, but it's not only better for clients, but it's also much better for development because your source maps, breakpoints, and actual variables are much closer to what the browser is actually executing (this is especially important for generators because they compile to case statements that aren't easy to debug).

For loops are the single biggest reason we still use Coffee Script. They're the most efficient and most compatible of the alternatives. The syntax of the loops is very clean and they all compile to simple "for( ; ; )" or "for( in )" loops.

And a few months ago that we considered moving to something else, Coffee Script 2 was released with all that was missing for us.

The author has huge gaps in his understanding the language works, and these tools work, and he seems to have zero interest in correcting that. It sucks that he had a bad experience. And, yes, it's a lot to understand. But he blames Babel, the authors of polyfill code, and Twitter without putting any attempt into filling the gaps in his understanding.

The Babel homepage literally says what it does.

> These plugins allow you to use new syntax, right now without waiting for browser support.

When a Twitter user tried to help him by asking how a syntax transform could polyfill `forEach` on a `NodeList` -- something that would be impossible in JS -- he was completely dismissive.

> Science? ‍️ I dunno. Maybe if the object was previously queryselected? [...] I'm way past that problem now.

Polyfills can be heavy because they have to be forward compatible and implement behavior to spec. If they don't then you end up shipping code that breaks when browsers are updates and the polyfill becomes redundant. That's how you break the web.