Flutter: Don’t Fear the Tree Shaking Mechanism
What Tree Shaking is, why Flutter needs Tree Shaking and how it works in Dart
Flutter uses a lot of methods to optimize the app performance. One of them is tree shaking.
What tree shaking is, what role it plays in Dart/Flutter and how tree shaking is implemented in Dart, all of that we are going to cover in today’s article.
What is Tree Shaking?
Tree shaking is an optimization technique used in Dart and many other programming languages, particularly in JavaScript with tools like Webpack and Rollup. It is the process of removing unused code from the final bundle sent to the browser (in web context). This makes the bundle smaller and more efficient, improving load times and application performance.
Here’s how tree shaking works:
- The bundler analyzes the import and export statements in a module-based project. This allows the bundler to understand which parts of the code are being used in the application.
- Based on this analysis, the bundler can then remove code that is never actually imported or used anywhere in the application. This includes simple things like variables and functions, but also entire classes and modules.
- After the unused code is removed, the bundler repackages the main code. The result: A smaller, more efficient bundle that only includes the code needed by the application.
Tree shaking works best with ES2015+ (ES6+) module syntax (import
and export
) because this syntax is statically analyzable, meaning that the imports and exports are defined explicitly in the code and do not change at runtime.
Now you might say: Well, if I only write functions that I use, why should I even bother about tree shaking?
You have likely added at least one (large) library or framework to your project, and you are most likely only using a fraction of this library, not even using all the other available functions.
In this case, because tree shaking also analyzes the dependencies of a project, tree shaking helps in keeping the size of the final delivered code to a minimum.
When Flutter uses Tree Shaking
As you might have guessed by now, tree shaking is unbelievably important in the context of Flutter development.
We are using libraries like Bloc, Riverpod, animation libraries, etc. and are only using a fraction of the features that these packages offer. Delivering them all to the final user would make the app unbelievably slow.
We all know Flutter uses Dart as its programming language.
A “special ability” of Dart is it’s support for both Just-In-Time (JIT) compilation in debug mode for fast development cycles and Ahead-Of-Time (AOT) compilation for producing optimized, native code for release builds.
Tree shaking is part of the AOT compilation process. The Dart compiler analyzes the application code to identify and eliminate any classes, functions, and variables that are not used in the app.
Now you might ask: Why don’t we have tree shaking in Just-In-Time compilation?
JIT compilation is always used in debug mode, which allows quick builds and immediate code changes reflection in the app (we all know it via hot reload). Tree shaking is not applied in this mode because all code needs to be available for immediate execution and debugging. This results in larger app sizes and slower performance compared to release builds, but it is not a concern since debug builds are not distributed to the end user.
Dart’s AOT Compiler and Tree Shaking
Tree shaking in Dart works similarly to tree shaking in other programming languages like JS, but it still has some differences:
- Static Analysis: Like in other programming languages, when you build your Flutter app for release, the Dart AOT compiler performs a static analysis of your entire codebase. It looks at all the Dart files, including dependencies, to determine which classes, functions, and variables are used.
- Elimination of Unused Code: Based on this analysis, the compiler identifies code that is never referenced or called.
Since Dart uses strong typing and the language does not support eval or similar mechanisms that can change the code structure at runtime, this analysis is quite reliable. - Compilation to Native Code: After unused code is identified and removed, the compiler then translates the remaining Dart code into native machine code. Unlike JIT compilation, AOT does not need to retain the entire program in memory or spend runtime resources on compilation, leading to faster startup and execution.
- More Optimization Techniques: In addition to tree shaking, Dart’s AOT compiler applies other optimization techniques like inlining (replacing function calls with the function body itself), dead code elimination (removing code that does not affect the program outcome), and constant folding (reducing expressions to constants at compile time).
What makes the Dart tree shaking mechanism special is its aggressiveness and the option to configure it (to some degree).
The tree shaking process in Dart’s AOT compilation is quite aggressive because it takes advantage of Dart’s static nature, allowing the compiler to confidently identify and eliminate unused code. All dependencies and their usage are known at compile-time, which enables a more detailed “clean up”.
In addition, we can influence tree shaking to some degree through configuration options in the pubspec.yaml
file and through code annotations that can keep code from being removed if necessary. I’m not going to dive deeper into this, because this is not the purpose of this article.
Conclusion
Thank you so much for reading! If you’ve liked this article, please give it a clap. It would mean a lot to me!
Here are some other stories that might interest you: