Trimming JavaScript Bundles with Haxe Dead Code Elimination and Externs
Learn how Haxe’s -dce full flag and custom externs can shrink your JavaScript bundle by removing unused code, with a lodash.debounce example and practical verification steps.
03 Sept 2025, 09:35 UTC

Problem: Large JS output from Haxe libraries
When you compile a Haxe project to JavaScript, the generated file often contains every class and function from the Haxe standard library, even if your code only uses a handful of them. For web applications this inflates download size and slows page load.
Thesis: Enabling full dead‑code elimination (‑dce full) together with well‑written externs lets the Haxe compiler strip away everything that is not statically reachable, producing a much smaller bundle while keeping the code you actually call.
How DCE works in Haxe
The Haxe compiler builds a call graph starting from your entry point (usually main). With -dce none it keeps everything; with -dce full it removes any class, function, field, or macro that the graph cannot reach. This analysis is purely static, so it cannot know about code that is invoked only through strings, eval, or reflection.
Why externs matter
Externs describe the shape of external JavaScript APIs to Haxe. They give the compiler enough information to type‑check calls and, crucially, to treat an extern symbol as "potentially used" when it appears in your code. Without an extern, the compiler would see the symbol as unknown and could either discard it (breaking the call) or conservatively keep it (inflating the bundle).
Worked example: exposing lodash.debounce
Suppose you want to use lodash’s _.debounce function in a Haxe web app and nothing else from lodash.
Install lodash in your project (npm or yarn).
npm install lodashCreate an extern file
lodash.debounce.hx:@:extern @:keep class LodashDebounce { public static function debounce(func: Dynamic -> Void, wait: Float, ?options: Dynamic): Dynamic -> Void; }The
@:keepannotation prevents the extern class itself from being stripped, ensuring the generated JS retains the reference to lodash.Write a small Haxe module that uses the extern:
import lodash.debounce.LodashDebounce; class Main { static function main() { var traced = function() { trace('debounced'); }; var debounced = LodashDebounce.debounce(traced, 300); // Simulate rapid calls for (i in 0...10) debounced(); } }Compile with full DCE:
haxe -main Main -js output.js -dce full -lib lodashIf you omit
-dce fullor replace it with-dce none, the generatedoutput.jswill contain the entire lodash library (hundreds of kilobytes). With the flag set, the output should only include thedebouncefunction and the tiny runtime helpers it depends on.Verify the result:
- Check file size:
ls -lh output.js(or use a bundle analyzer likesource-map-explorer). - Inspect the file: search for
debounceand confirm that other lodash helpers such as_.mapor_.filterare absent. - Run the script in Node or a browser and ensure the trace appears after the debounce delay, proving the function was not removed incorrectly.
- Check file size:
Trade‑off and limitation
Full DCE relies on static reachability. If your code calls a function through a string (window['someFunc']()) or uses Haxe’s Type.createInstance with a runtime‑determined class name, the compiler cannot see that call and may remove the target. In those cases you must add a manual @:keep annotation to the affected extern or class, or wrap the dynamic call in a function that is itself referenced statically.
Enabling -dce full also increases compile time because the analyzer must traverse the whole codebase. For large projects this can add several seconds to each build, so you may want to keep -dce none during rapid iteration and switch to full only for release builds.
Actionable closing
To start benefitting from smaller JavaScript bundles today:
- Audit the external libraries you actually use and write minimal externs for the needed symbols (many are already available in
haxelibor community repos). - Add
-dce fullto your release build command. - Verify the build size and run a smoke test to ensure no needed code was stripped.
- If you encounter missing symbols, add
@:keepto the relevant extern or class and rebuild.
By combining precise externs with Haxe’s dead‑code elimination you get type‑safe access to JavaScript APIs while keeping the final bundle as small as the code you truly need.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.