mishoo / UglifyJS

JavaScript parser / mangler / compressor / beautifier toolkit

Home Page:http://lisperator.net/uglifyjs/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Turn off negate_iife by default as it hurts V8 performance.

krisselden opened this issue · comments

Removing the ( before iife causes V8 to preparse to skip function bodies in top level iifes (when the script is large) then parse it when executing and also affects the hinting for lazy function body compiling.

From the V8 preparser.h:

        // Heuristically try to detect immediately called functions before
        // seeing the call parentheses.
        parenthesized_function_ = (peek() == Token::FUNCTION);

Do you have some actual numbers showing the difference in performance?

I second @rvanvelzen. Can't believe there is any significant difference, or else I'll make fun of @mraleph. >-)

@mishoo I haven't touched V8 source for 3 years now - no need to make fun of me ;)

@mishoo this is on an internal app and I'm focused on android performance issues testing on an S3, I'll pull together the numbers for you but if I do, please don't make fun of anyone or tweet it.

@krisselden you should report the numbers to V8 folks. I am sure they'll be interested. Startup issues caused by overly zealous lazy parsing heuristics have been increasingly on the radar lately AFAIK.

@mraleph in a single page app, they help by deferring components and templates you don't use in initial render is actually pretty nice, it is just a bit touchy because it is double parsing a lot of code that is needed for initial render.

Just kidding. @mraleph is one of the greatest compiler/JIT engineers and V8 rocks.

Any case, can't believe this small thing could cause a big slowdown — if it does then the V8 folks definitely want to know about it.

It is hard to reduce this to a simple example because the issue is deferring work that happens when you use code in the app, just reducing it to a simple example really alters the trade off, I also regret because I think changing the default is a bit alarmist and I was tired and frustrated. It is good enough for me that it is optional.

FWIW I made a benchmark around this, seems to confirm it's a perf boost for V8 and Chakra: rollup/rollup#774 (comment)

they should add this style to there iife detection

I'm also concerned that as we bring attention to this lots of people are going to start wrapping parens around everything forcing the browser to compile stuff that should be lazy.

V8 also recently has improved parse perf

Yeah the tricky part here is that it's not the case that you want to add parens around functions willy-nilly, but that you want to do it everywhere where you know the function will be executed.

The current optimization in Chakra (and I assume for V8 as well) is due to the fact that most JS functions out in the wild are never executed (i.e. web authors ship a lot of unused code), so it's a perf boost to lazy-parse. However when people ship highly-optimized tree-shaken bundles where every function gets immediately executed, it's a perf regression to do the lazy-parse.

Probably the main problem with the current negate_iife default in Uglify is that it prevents bundlers from even implementing such optimizations. Even if they know exactly which modules will be immediately executed, any parens they add will just be dropped. Yes developers can change the default, but in practice nobody does this.

Also worth pointing out: this optimization doesn't just apply to IIFEs - it also applies to any paren-wrapped functions that are immediately executed. So it's not clear that changing the default negate_iife behavior would fully fix this... 😕

OK, so I ran some more benchmarks and have some updated info on this.

When Uglify converts (function(){})()-style IIFEs into !function(){}()-style IIFEs, it's actually not a perf regression in either Chrome or Edge. Yay!

So that just leaves functions enclosed in parens in other situations, such as the original benchmark where we took a UMD bundle (essentially a function passed in to another function, and then executed). In those cases, it's still a perf regression in both Chrome and Edge to remove the parens.

Since this case has nothing to do with negate_iife (i.e. Uglify will remove the parens either way), this particular issue should probably remain closed.

Edit: my bad, I was wrong, see below.

@nolanlawson your benchmarks are bad, at least for Chrome, the heuristics in the top level of a large script (above 1024) follow the preparse rules which are more simplistic.

Also, this option seems to affect unrelated things: https://gist.github.com/krisselden/c43d47be74dcc3e870a8c014c6e68079

Just to clarify, the preparse phase is only for script files and only top level function literals. Lazy parsing still happens for literals below but the heuristic isn't a simple.

@krisselden I'm sorry, I don't understand what you mean; can you clarify? Also my benchmark is open-source, so please feel free to fork to show me what you mean. 😃

Based on that benchmark it seems to me that unwrapped UMD and declare-then-execute are the only two unoptimized ones in either Chrome or Edge, but I may have made a mistake.

After tracing your benchmark, the only reason they are the same is because you wrapped them in a function literal on the top level that went through the preparse phase.

image

image

Both lazy parsed the function immutable() {

having trouble with github's image upload, but you can't wrap your script in a function literal, this is a top level thing with chrome.

Hm OK in that case I need to go back and rewrite the bench without the top-level immutable function. Thanks for the tip, and sorry for derailing the conversation!

@nolanlawson I've trolled myself many times with benchmarks, it is important to check your work, but at least you made it verifiable and public.

Thanks for calling me out @krisselden; here's a corrected benchmark. My results show that Edge is optimized for the !function(){}() style, but Chrome and Firefox are not. Chrome 52 and Firefox 48 seem to optimize for both Crockford and regular-style IIFEs, as well as the wrapped UMD style, but not the unwrapped UMD or declare-then-exec. In all other ways, Edge is the same, except that it also optimizes for the Uglify style. As for Safari, I'm not sure.

commented

On Safari it errors out with:

ReferenceError: Can't find variable: performance
uglify-style.js:1
commented

@nolanlawson I'm seeing different results for Chrome than what you describe.

Chrome on Mac Version 53.0.2785.116 (64-bit)

Run unwrapped UMD: 7.849999999998545ms
Run wrapped UMD: 7.849999999998545ms
Run function func () {}; func() version: 7.670000000001892ms
Run Uglify-style !function () {}() version: 7.849999999998545ms
Run Crockford-style (function () {}()) version: 1.1200000000026193ms
Run regular IIFE version - (function () {})(): 1.1200000000026193ms

Edit: My bad. It's consistent with your observations.

commented

For Firefox 48.0.1 on Mac I'm seeing 2.8ms across all techniques.

You may have to switch to a slower computer to see the difference in Firefox. I tested 48.0 in both Mac and Windows and I see the same perf characteristics as Chrome. Here's a typical run:

Unwrapped UMD: 3.894999999999982ms
Wrapped UMD: 1.4099999999998545ms
function func () {}; func(): 3.8299999999999272ms
Uglify-style: 3.7849999999998545ms
Crockford-style: 1.5750000000007276ms
Regular IIFE: 2.1700000000018917ms

So the slow version is ~3.5-4ms and the fast version is ~1.5-2ms. Perhaps it would be easier to discern if I used a larger library than Immutable.js.

I tested in Safari 10 and can't repro your performance problem. Are you on Safari 8+?

commented

Safari 9.1.3. Is 10 out of beta?

No, I'm on macOS Sierra, but it's still a preview.