Optimizing Haxe Builds with Dead Code Elimination: A Practical Design Guide
Learn how Haxe’s dead‑code elimination flag trims output size, the assumptions behind it, and how to verify that only the needed code remains.
18 Sept 2025, 03:30 UTC

Problem Statement
When compiling to JavaScript, Flash, or native targets, the generated bundle can grow quickly. Unused classes, methods, and fields inflate the size, increase download time, and slow runtime startup. Haxe’s -dce flag offers a way to prune unreachable code, but its effectiveness depends on how the compiler perceives reachability.
Design Requirements
- Remove all code that the application can never execute at runtime.
- Preserve APIs that may be accessed via reflection, serialization, or external libraries.
- Keep the build process deterministic and reproducible.
- Provide a verification path so developers can confirm that DCE behaved as expected.
- Allow a fallback to a more aggressive mode for very small targets.
Minimal Design Using Haxe DCE
The core of the design is a single compiler flag: -dce std. This instructs the Haxe compiler to start from all main entry points, traverse the call graph, and mark every reachable field, method, class, and extern. Anything not marked is dropped from the emitted output.
To make the design robust, we add two optional layers:
@:keepmetadata on identifiers that must survive DCE.- A build‑time macro that sets
-dce wholefor release builds when size is critical.
Trust and Data Boundaries
Haxe’s DCE operates under the assumption that the only entry points are the functions explicitly declared as main (or those referenced by haxe.macro.Compiler.define). The compiler trusts that any code reachable through static calls, constructor chains, or type references is safe to keep.
Boundaries are defined by:
- Reflection: Calls to
Type.getInstanceFieldsorType.getClassFieldsare treated as dynamic; the compiler cannot prove which fields are used, so it conservatively keeps all fields of the referenced class unless@:keepis applied. - Externs: External JavaScript definitions may reference Haxe symbols by name. If the externs are not annotated with
@:keep, DCE may remove the symbol, causing runtime errors. - Macros: Compile‑time code that generates new classes or methods can influence reachability. If a macro creates a class that is never referenced statically, DCE will remove it unless the macro emits a
@:keepannotation.
Operational Checks
After enabling DCE, a developer should perform the following checks:
- Size Comparison: Compile once with
-dce stdand once without. Theout.jsfile should be noticeably smaller when DCE is active.haxe -main Main -js out.js # baseline haxe -main Main -dce std -js out.js - Keep/Remove Report: Use the
-Dce-checkflag to have the compiler print a list of kept and removed identifiers.
The output will look like:haxe -main Main -dce std -Dce-check -js out.jsKept: 42 classes, 123 methods Removed: 8 classes, 56 methods - Runtime Validation: Run the application and confirm that all expected features work. If a feature that relies on reflection fails, inspect the DCE report for missing symbols and add
@:keepwhere necessary.
Failure Modes and Mitigation
- Missing Reflected Symbols: If code is accessed via
Type.getClassor string‑based lookup, DCE will remove it unless@:keepis present. Mitigation: add@:keepto any field or method that may be looked up dynamically. - Extern Mismatch: Extern definitions that reference Haxe symbols can break if those symbols are removed. Mitigation: annotate extern classes or fields with
@:keepor exclude them via a build script. - Macro‑Generated Code Removed: A macro that emits a class not referenced statically will be pruned. Mitigation: emit
@:keepmetadata in the macro or add a dummy static reference. - Over‑aggressive DCE: Using
-dce wholecan strip code that is only referenced from externs or JavaScript. Mitigation: test the release build thoroughly and consider falling back to-dce stdif failures occur.
When to Switch to Aggressive DCE
For very small targets such as embedded microcontrollers or WebAssembly modules where every kilobyte counts, -dce whole can be justified. However, it should only be used when:
- All external interfaces are known and static.
- The project has no dynamic reflection usage.
- The build passes the
-Dce-checkreport with no missing symbols.
Practical Verification Steps
- Baseline Build:
haxe -main Main -js out.js - DCE Build:
haxe -main Main -dce std -js out.js - Compare File Sizes:
ls -lh out.jsbefore and after. A reduction of at least 10% is typical for projects with unused utility classes. - Check DCE Report:
haxe -main Main -dce std -Dce-check -js out.jsand review the console output. - Run Tests: Execute the unit test suite. Any failures related to missing classes or methods indicate a DCE oversight.
Example Build Script
Below is a minimal build.hxml that demonstrates the whole flow. Replace <TARGET> with your desired output target (e.g., js, cpp, java).
# build.hxml
-main Main
-dce std
-Dce-check
-<TARGET> out/<TARGET>.out
# Optional: keep a reflection‑based API
-keep ClassWithDynamicApi
# For release builds, switch to whole DCE
# -dce whole
In this script:
- The
-keepline is an example of how to preserve a class that may be accessed viaType.getClass. - Commented
-dce wholeshows how to enable aggressive pruning when the project is free of dynamic features.
Limitations
- DCE cannot analyze runtime string‑based class names. Developers must manually annotate or exclude such code.
- Macros that generate unused code will be removed; keep metadata must be added at macro generation time.
- Externs that reference Haxe symbols need careful handling; otherwise, runtime errors will occur.
Conclusion
Haxe’s dead‑code elimination is a powerful tool for shrinking binary size, but it requires a clear understanding of reachability, reflection, and extern boundaries. By following the design outlined above—using -dce std, guarding dynamic APIs with @:keep, and verifying with -Dce-check—developers can confidently reduce output size while maintaining correctness. When targeting extremely constrained environments, consider -dce whole only after exhaustive testing and a clean DCE report.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.