Created attachment 401421 [details]
ES2015 Time Profile Instruments output
We have have recently begun evaluating directly shipping ES2015 code in our application, instead of down-compiling it down to ES5. We have observed a significant performance regression when doing this in Safari.
Here is a somewhat reduced test case, that displays the execution time of the critical part our application (the time to load/render a document):
ES5 version: http://persistent.info/webkit/test-cases/es2015-slowness/?es5 - takes around 80ms
ES2015 version: http://persistent.info/webkit/test-cases/es2015-slowness/?es2015 - takes around 312ms
This has been observed in both Safari 13.1.1 and Safari Technology Preview 107 on macOS 10.15.5
A timeline recording shows significant time spent in the root "(program)" part of the script, which does not shed much light.
We thus also ran the Web Content process under Instruments, and compared the top functions for the ES5 and ES2015 versions. The latter seemed to spend a lot of time in JSC::BytecodeGenerator::getVariablesUnderTDZ() and creating/populating hash tables that back JSC::CompactVariableEnvironment (see attached es2015.trace file).
Given this clue, we generated a variant of the ES2015 output where all `let`s and `const`s were replaced with `var`s, and that yielded better results, though still not as fast as the ES5 version:
http://persistent.info/webkit/test-cases/es2015-slowness/?es2015-var - takes around 155ms
In case it helps, the original code is in TypeScript (using ES207 features including async/await). The TypeScript compiler handles the down-leveling to ES2015 or ES5. Rollup is then used as a bundler, it generates the IIFE that the entire script is wrapped in. I'm happy to try to generate an even smaller benchmark if it proves helpful.
I have observed the same time difference in the current WebKit nightly build (r262775)
As a further experiment, I generated a variant of the ES2015 output that did not wrap the output in an IIFE, and that had comparable performance to the ES5 output that did:
http://persistent.info/webkit/test-cases/es2015-slowness/?es2015-no-iife - takes around 91ms
However, that pollutes the global namespace (and exposes symbols that we would prefer not to be accessible). If we take the script and load it with <script type="module">, then we're back to where we started from:
http://persistent.info/webkit/test-cases/es2015-slowness/?es2015-no-iife-module - takes around 330ms
So taking off the IIFE and using module scripts is not a feasible workaround for us.
These are the sources for the variants used:
Saam: It looks like you've looked into IIFE and TDZ-related performance issues in the past, any chance you could see if there's anything that could be done from the JSC side?
I think you are seeing the same issue that came up in https://bugs.webkit.org/show_bug.cgi?id=199866. It seems like this is a more widespread problem that we should solve. I'll try to find time to work on it soon! Although, I no longer remember what the solution I had in mind back then was... 🤦♂️
Closing in favor of bug 199866 -- it's the same underlying issue and that one has more traction.
*** This bug has been marked as a duplicate of bug 199866 ***