There are a ton of benefits around using tools to build XPDA's (cross-platform desktop apps). However, the easier the tooling is to use, and the more powerful it is, the larger your distribution package tends to be. NW.js is not exempt from this generality. I've been making desktop apps for over 5 years now. In that time I've found some tricks and best practices for optimizing for the medium of desktop apps, and I'd like to share some of these techniques today.
The four approaches we'll cover:
NW.js, to oversimplify, is a modified Chromium browser with Node.js built in. So as time goes on, Chromium and Node improve, get new features, and become more secure. They also get bigger as a result. Since NW.js is built on top of these technologies, it inherits these improvements, but also their growth in file size.
NW.js is known for having the latest tech. It typically produces a new release within 24 hours of a new Chromium or Node release. A factor that draws many to it. However, for those willing to use older versions of these technologies, this opens the opportunity for some interesting trade-offs.
One solution to reduce file size is to use an older version of NW.js. The go-to version would be
0.14.7, the Long-Term Support (LTS) release. This was the last version released that supported legacy OS's (Windows XP+, OSX 10.6+). Using this older version will mean a major reduction in the total distribution size of your application, however it also means giving up access to newer features and newer developer tools that come with the latest Node/Chromium releases.
Another choice for an older version is
0.12.3. This was the last release of NW.js before it had a major architectural change to allow releases to come out faster and more often. It is not recommended to use this version as its API has subtle differences from the
0.13.0+ releases. Meaning that it will be more difficult to update your app to newer versions of NW.js in the future. If you create your app using
0.13.0 or above, then updating to newer versions of NW.js is completely painless and will almost never require any code changes other than if something was deprecated in Chromium or Node themselves (pretty rare outside of experimental features).
Using the Windows releases as a baseline, the latest versions of NW.js (~0.36.x) are around 189 MB, the LTS (0.14.7) version is 126 MB, and the outdated version (0.12.3) is 103 MB (again, not worth it). This can save a cool 63 MB (or 86MB if you're really crazy). This savings comes at a trade-off of giving up some developer conveniences, newer features, and most importantly security patches. But you get much wider support for older OS's, and a smaller dist. A compromise would be to build this version just for legacy OS's; but use the latest versions of NW.js for everything else. That would require twice the manual testing of the application though.
node_modules folder has a reputation for being a beast. One of the great features of NW.js is the ability to access Node modules directly from the DOM. So it's likely that your app will have some packages it needs shipped with it.
Types of packages:
Things you just need for development -- These should be in your
package.jsonand removed when it comes time to build for distribution. Example: ESLint, Jest, Babel.
Libraries your code uses -- If these are just code libraries, then ideally, they should be in the
devDependenciestoo, and the relevant code pulled in by an automated bundler/tree-shaking tool (Webpack, Rollup, Gulp, etc.). Example: Lodash, Vue, Moment.
Modules with binaries or other unique files that can't be bundled -- These should be listed as
dependencies. Example: Scout-App is a desktop app that processes Sass to CSS, so it needs the
node-sassbinary files shipped with it. Koa11y requires the dependencies of Pa11y and PhantomJS to test accessibility problems on webpages.
Okay, so simply moving everything you can get away with out of
dependencies will do a lot to help. But what about the stuff you can't? Most of the time each Node Module will come with a lot of extra files that your end user does not need shipped to them (
tests folder, etc.). For this I usually end up creating a custom build or post-build script that lists the path to each of these junk files/folders in an array. Then loop over the
junk array to delete them during or after the build.
There is some inherent risk in this, as you are manually selecting the files to be automatically deleted, and these files may change as you update your dependencies in the future. Though it is a small risk, it is one you should be aware of when updating your dependencies. When you updated any dependency, make sure to test your build thoroughly, and to inspect the
node_modules folder after a build to ensure there are no new junk files/folders to add to the list.
The results here are entirely dependent on your application and how you develop it. In general, this type of optimization will give most apps the biggest gains in reduction of distribution size. It is usually measured in the tens to hundreds of megabytes. We'll use Koa11y as a real-world example, it's a XPDA for detecting accessibility problems in webpages.
- Koa11y with dependencies, devDependencies, and extra files: 274MB
- Koa11y with dependencies and extra files: 158 MB
- Koa11y with dependencies and extra files removed: 121 MB
This case study shows how important it is to reduce the amount of dependencies there are in your app. Since each one is shipped to the user, the decision to add a dependency must be handled with care. Just removing the devDependencies in this case saved 116 MB. The post-build script to remove files that are not required for the app to run (junk files), shaved off an additional 37 MB.
When it comes to optimizing web apps, many focus on shaving off bits and bytes. Which, to be honest is a waste of time when dealing with XPDAs made with NW.js. I'll quickly cover the stuff you can skip, and the stuff with potential to save a few megabytes.
Since NW.js uses web technologies to produce desktop apps, it can be tempting to try to apply all the same techniques used by web developers to optimize their apps for the web. While some of the techniques still apply, it's important to understand the medium you are working in. Desktop apps are not the web.
- Don't concatenate - On the web, a lot of attention is placed in reducing the number of files that will be transferred. This is because servers will typically only handle 4 or 5 network requests at a time per user. Thus, concatenating all JS into one file and all CSS into one file can save tremendously on load times. But... this isn't something we need to worry about, as all files are loaded directly off the local disk (assuming you are following the best practice of designing your desktop app to be offline-first). So, concatenation of files is irrelevant. There is no downside to doing it, but there's also no real benefit here.
- Don't uglify - Uglification really does make a difference when loading a page over the network, especially on slower connections and mobile devices. But we aren't dealing with networks and mobile devices. We're shipping a 100+ MB runtime environment with our app, so, things that shave off a few bytes are completely trivial. The user will be doing a one-time, large download. If the download takes 1 extra second, they don't care, they still need to unzip or install it after it finishes downloading anyway. There is no downside to uglifying or minifying, other than debugging in prod being more difficult, but there is also no real benefit unless it's saving megabytes in file size.
- Don't worry about latency - Again, network requests and latency don't matter, they already have all the files locally.
- Don't worry about "Critical CSS" - All styles are already loaded locally off disk. Critical CSS has no benefit at all here.
- Don't offload to the web - Don't use web fonts or CDN's. The use of these means your app can't run without internet access. Almost all desktop apps should be designed to run offline. None of the benefits CDNs offer applies to desktop apps. So, storing the fonts and libraries with the app is a best practice. These types of files are small anyways.
Bundling - This allows you to keep libraries as devDependencies and only pull in the parts your app needs. For example, LoDash is a common tool used by developers. If all you are using it for is
_.cloneDeep, then you can import in just that part and have it bundled with your code. Alternatively, if you packaged all of LoDash, then your dist size would increase by several megabytes. The most popular tool for bundling is WebPack, though alternatives like Parcel do exist.
- Tree-Shaking - This is the process of tracing all of your code and only pulling in the parts that are ever used. So stray functions, or parts of libraries that will never be accessed when running your app are just branches of the tree that are trimmed away. This can also result in saving several MB's in size. WebPack and Rollup are the most popular tools for tree-shaking.
- Media Optimization - Images, Videos, and Audio files can all be compressed to lower file sizes. With videos, tools like HandBrake offer advanced compression options that can produce the same quality videos at smaller files sizes through techniques that take more time to encode. Though ultimately the best techniques for getting lower file sizes in media files will result in a trade-off in quality for greater compression. With images though, there are forms of lossless compression for PNG files that will produce pixel-for-pixel identical images at smaller sizes. For SVG's there is SVGO to optimize them.
Zipping for distribution - There are many ways to package and distribute your app, but most common is to either zip up the files so users just "unzip, double-click", or to package them into an installer (double-click, click next a few times and finish, then click the shortcut). This only impacts the download size of the distribution, not the size when unpacked. For the zipping route, there are tools you can use to automate the zipping, like the Node Module
7zip-bin, or the options in
nsis7z). Or you can use WinRAR's GUI to produce a self-extracting executable that is compressed using it's rar compression (rather than zip, which is typically the smallest installer option you can produce). What you don't want to do is force your app to be unzipped every time it is opened. NW.js allows for this type of packaging, but it is not advised as it just increases the launch time of your app arbitrarily as it must unzip your app files to a temp folder before launching every time.
This is completely dependent on your application. In general though, you will get the smallest saves from optimizing your app directly. However, the packaging of your application with some zip or rar like compression can significantly reduce the size of your app, often to around 60-70 MB, though it can go even lower in some scenarios. This compression is just for the initial download, once unzipped/installed the app's full size is revealed.
When you inspect the files that come with NW.js you will find that some are more important than others. Some of the files are only required if you use certain functionalities.
libEGL.dll is used when your app talks to your video drivers to display 3D models, like when using WebGL. If this file is missing and you try to load a 3D model into the DOM, NW.js will call upon
libEGL.dll, not find it, and then crash! But if your app never calls upon libEGL, then it missing will not have any negative effect. As you may already be surmising, removing files that can cause your app to crash is very risky, and hence why I don't recommend it. But for the sake of education, I'll list the files that NW.js must have in order to run:
/locales/en-US.pak ffmpeg.dll (not all versions require this) icudtl.dat natives_blob.bin node.dll nw.dll nw.exe nw_elf.dll (not all versions require this) resources.pak v8_context_snapshot.bin (not all versions require this)
locales files is unique to the locale your OS is set to. So, removing the extra ones means you are saving space, but the app won't run on computers with different locales set. There are
.pak.info files. Removing the
.pak.info locale files has no negative effect.
If you attempt to remove the extra files, be sure to manually test every aspect of your app. As your app may not crash until a certain interaction in your app occurs like playing a video, viewing a PDF, viewing a 3D Model, etc.
This can actually shave off around 43 MB. Of course, your app may crash randomly, but ya know, go big or go home (just kidding, don't actually do this).
I've covered a few approaches for how to get your dist size down. Many are unique to desktop development. Some are easy wins. Some have trade-offs. Some are more complicated or risky. It's important to know the trade-offs and what all your options are, even if they're not all safe. And it's just as important to know what optimizations are wasted effort.
You'll notice that in this entire write up, I've only measured savings in megabytes. And I want to impress upon the readers that they should do the same. Remember, you're shipping a 70 - 200 MB app to your user. If you are focusing on saving 20KB, you're focusing on the wrong things.
To close this out we'll stick with Koa11y as our real-world example. Let's compare the different approaches.
|NW.js version||devDeps||junk files||NW.js files||Total size|
119 is not too shabby for shipping Chromium, Node.js, and PhantomJS (used by Koa11y).
The official downloads of Koa11y are zipped, taking it down to 59.8 MB on Windows (66.6 MB on OSX, and 78.4 MB on Linux).
To learn more about Cross-Platform Desktop App (XPDA) development:
To get started with NW.js, I've written a beginner friendly tutorial:
If any of the above requires clarification or further explanation you can ask in the comments below.