Hi all, Some of you have probably been wondering about my changes to the compiler over the last year or more. I apologize again for occasionally breaking things for short periods. It's been quite a challenge getting this stuff working, but I'm excited to finally be able to report some real improvements that pretty much anyone should be able to take advantage of when building a Royale app.
First some background. A while back, Harbs asked me to look into ways of reducing the file size of release builds. As you may know, we use Google's Closure compiler to optimize our generated JavaScript. Closure can be very aggressive in its optimizations, by renaming symbols (things like variable and function names) and removing "dead code" that is detected as never being called. Closure's optimizations are good, but they also require developers to be very careful about how they write their JavaScript code. When you enable Closure's full optimizations, you are not allowed to use certain JavaScript features because Closure cannot analyze them properly. For instance, consider the following code: var propName= "myProp"; var value = obj[propName]; When you dynamically access a property with a string, Closure cannot reliably know that the property exists and will be accessed at runtime. It may decide to rename or remove that property, which would break things at runtime. ActionScript supports many of the same restricted dynamic features too, so if you want to support the entire AS3 language, we can't let Closure do its full optimization. Luckily, Closure also provides a bit of a backdoor: it allows you to "export" symbols, which means that they won't be renamed and they won't be removed as dead code. Traditionally, we have made heavy use of this exporting feature in Royale. Harbs wanted to know if we absolutely needed to export everything that we currently export, and if we could potentially allow developers to turn off exporting entirely, as long as they follow the stricter rules required by Closure. I won't go into all of the details, but over the last several months, I've been changing the compiler to give developers more control over release builds. In particular, control over which symbols get exported, but also the ability to block Closure from renaming symbols that haven't been exported. Now, for some of the results. I'm going to share the output file size of the release build for several Royale projects with various different compiler options. For the example projects included with Royale, I built royale-asjs commit 94f12ed0e564b0b443834400dc2fc06d61b90a8a from October 26, 2020. If you want to try building these examples yourself, the file sizes of release builds may be slightly different, if you use a different commit. SpectrumBrowser is a project developed by Harbs and his team. I used commit d25a3def972b15ec029ae838f1a8a677d2d158bd from October 20 for the results below. Repo: https://github.com/unhurdle/spectrum-royale/ To establish a baseline, I built all of these projects with the older Royale 0.9.7 compiler first. ========== Baseline: royale-compiler 0.9.7 ========== HelloWorld: 68 KB ASDoc: 231 KB TourDeJewel: 1074 KB SpectrumBrowser: 900 KB Again, I am building the same AS3/MXML code every time, but these first numbers are from building with the older compiler. All apps build and run successfully. ----- The rest of the results are built with royale-compiler commit df8bd9f686f1bbf89539e545377b2797c646172c from November 3. All results below include the difference in KB and %. These values are always in comparison to the baseline numbers above. ========== Result 1: 0.9.8 default options ========== HelloWorld: 84 KB (+10 KB / +24%) ASDoc: 254 KB (+23 KB / +10%) TourDeJewel: 1105 KB (+31 KB / +3%) SpectrumBrowser: 936 KB (+36 KB / +4%) These examples are slightly larger when built with the newer compiler. That's expected. It's not ideal, but in the process of testing a multitude of things to be sure that nothing had broken after my compiler changes, I discovered some cases where exporting a symbol didn't actually work correctly in 0.9.7! To properly fix the bug and export these symbols, there was no choice but to make the file size a bit larger. ========== Result 2: Disable export ========== HelloWorld: 74 KB (+6 KB / +9%) ASDoc: 227 KB (-4 KB / -2%) TourDeJewel: 942 KB (-132 KB / -12%) SpectrumBrowser: 882 KB (-18 KB / -2%) In this round, I added the *-export-public-symbols=false* compiler option. You may recall that I said earlier that I also modified the compiler to allow a symbol not to be exported, but still prevent it from being renamed. With that in mind, -export-public-symbols=false basically tells the compiler that it still can't rename things, but it is allowed to remove what it perceives as dead code. HelloWorld is still slightly larger than 0.9.7, but the three other examples are now slightly smaller than 0.9.7. Most developers should be able to safely add -export-public-symbols=false to their compiler options when building a Royale app. The only time that you might still want this exporting is if you have external JavaScript in your page that isn't part of your Royale app, but it needs to call functions/classes in your Royale app. ========== Result 3: Allow non-public things to be renamed ========== HelloWorld: 72 KB (+4 KB / +6%) ASDoc: 221 KB (-10 KB / -4%) TourDeJewel: 918 KB (-156 KB / -15%) SpectrumBrowser: 861 KB (-39 KB / -4%) In this round, I used the following compiler options: -export-public-symbols=false *-prevent-rename-protected-symbols=false-prevent-rename-internal-symbols=false* The two new options allow Closure compiler to rename protected and internal symbols. Once again, HelloWorld is still slightly larger than 0.9.7, but the other three examples have gotten smaller again. While -prevent-rename-public-symbols=false exists too, we cannot use it. The examples would not work correctly at runtime. This option would probably work in a pure AS3 app, but our implementation of MXML in Royale uses dynamic language features that Closure restricts. Unless that is fixed, we need to avoid renaming certain public symbols. Again, most developers should be able to add -prevent-rename-protected-symbols=false and -prevent-rename-internal-symbols=false to their Royale app's compiler options. You might need to prevent renaming of protected/internal symbols if you access them dynamically. However, in my experience, people are much more likely to access public symbols dynamically. ----- ========== Result 4: Allow public methods to be renamed ========== HelloWorld: 64 KB (-4 KB / -6%) ASDoc: 206 KB (-25 KB / -11%) TourDeJewel: 881 KB (-193 KB / -18%) SpectrumBrowser: 828 KB (-72 KB / -8%) In this round, I used the following compiler options: -export-public-symbols=false -prevent-rename-protected-symbols=false -prevent-rename-internal-symbols=false *-prevent-rename-public-static-methods=false-prevent-rename-public-instance-methods=false * The two new options allow Closure to rename methods that are public. Now, all four examples are smaller than 0.9.7, and the file size difference is getting even more dramatic. Once again, -prevent-rename-public-static-methods=false and -prevent-rename-public-instance-methods=false should be safe for most developers to enable when compiling their Royale app. In my experience, calling methods dynamically is rare. ========== More new compiler options ========== There are some additional new compiler options available, but using them is likely to break most Royale apps. -prevent-rename-public-static-variables=false -prevent-rename-public-instance-variables=false -prevent-rename-public-static-accessors=false -prevent-rename-public-instance-accessors=false These options control whether Closure allows variables or accessors (getters and setters) to be renamed. There are also similarly-named options for protected and internal symbols, if you want more control over those too, instead of using -prevent-rename-protected-symbols=false and -prevent-rename-internal-symbols=false. Unfortunately, renaming public variables/accessors is usually not possible without breaking the app at runtime. In some apps, you might be able to allow public static members to be renamed. However, in my experience, binding to static constants is pretty common, and renaming breaks those bindings. ========== Next Steps ========== Ideally, I'd like to make it possible for developers to be able to tell Closure that it's allowed to rename all symbols, including public ones. I believe that we could see even more file size savings in release builds if Closure works with full optimizations for all symbols. Obviously, ActionScript developers would be required to strictly follow Closure's rules, if they opt into renaming of public symbols, but that's a choice that they should be allowed to make. As I mentioned above, our implementation of MXML and binding uses dynamic access, which is not compatible with Closure's full optimizations. To support those optimizations, I will need to explore changes to how we generate JS for MXML, and how it gets parsed at runtime. We previously discussed this subject a bit in this older thread from January 2020: https://lists.apache.org/thread.html/r843e55252e37967b71b1430a2a904719791d698f3e5e2a79de74e493%40%3Cdev.royale.apache.org%3E At the time, I tried out some ideas that we came up with while brainstorming, but all had various downsides that didn't make for an obvious winner. In the end, I decided to set further investigation aside and first focus on exporting/renaming stuff. Now, I'm ready to take a second look with a fresh perspective, and maybe we'll have some new ideas to try. ----- That was really long, so thank you for reading, if you made it to the end! TL;DR: By enabling certain, new compiler options, most Royale developers can make their app release builds smaller. Additionally, I plan to keep investigating, and I expect to find more ways to reduce file size in the future. -- Josh Tynjala Bowler Hat LLC <https://bowlerhat.dev>