A minimalist, ready-to-use template for building high-performance graphical applications in Rust using the egui library. This kit is designed to compile seamlessly for both Native (Windows, macOS, Linux) and WebAssembly (WASM).
- Cross-Platform: Single codebase for Desktop (via
eframe) and Web (via WASM). - Immediate Mode: Reactive and easy-to-code user interface.
- Dark/Light Support: Built-in egui native themes.
- State Persistence: Automatic app state saving (optional/configurable).
- Optimized Workflow: Ready-made configuration for web deployment.
Before you begin, ensure you have the following installed:
- Rust: https://rustup.rs/
- WASM Target (for web builds):
rustup target add wasm32-unknown-unknown
- Trunk (the build tool for web):
cargo install --locked trunk
To launch the application on your system (Linux, macOS, Windows):
cargo run --releaseTo compile and serve the application in your browser:
trunk serveThen, open your browser at: http://127.0.0.1:8080
.
├── assets
│ └── icon.png
│── src
│ ├── app.rs
│ └── main.rs
│── .gitignore
├── Cargo.toml
├── index.html
└── README.md
2 directories, 7 files
assets/: Folder for icons, fonts, and images.src/main.rs: Entry point for the native binary.src/app.rs: This is where your UI code and application state live..gitignore: Avoid to push on GitHub some files and directories.index.html: Boilerplate for web rendering.
To generate static files ready for production (GitHub Pages, Netlify, Vercel, etc.):
trunk build --releaseThe output files will be located in the dist/ directory.
egui uses getBoundingClientRect() to calculate the mouse position. If the canvas has padding, a border, margins, or a CSS transform, the coordinates will be skewed.
CSS Fix: Ensure the canvas has no unintentional offsets:
canvas {
display: block; /* Prevents inline space below the canvas */
margin: 0;
padding: 0;
border: none;
/* Avoid transform: translate(...) */
}
body {
margin: 0;
padding: 0;
overflow: hidden;
}eframe can sometimes enter a resizing loop where it confuses logical pixels (CSS) and physical pixels (device), causing coordinate offsets.
The egui canvas has two sizes:
canvas.width/height→ Physical pixels (rendering resolution)canvas.style.width/height→ CSS pixels (displayed size)
If these two dimensions do not match correctly via the devicePixelRatio, mouse coordinates (which are always in CSS pixels) will be misinterpreted.
HTML Index Fix: Force the canvas to occupy exactly the expected CSS space:
<style>
html, body {
margin: 0;
padding: 0;
overflow: hidden;
height: 100%;
}
canvas {
display: block;
width: 100%;
height: 100%;
}
</style>When the canvas is embedded within a larger page and has a border or padding, egui uses getBoundingClientRect() but might not correctly subtract these values. This bug was fixed in recent versions of eframe, ensure you are using eframe ≥ 0.28.
Firefox with privacy.resistFingerprinting=true (often enabled by "Enhanced Tracking Protection") sends spoofed mouse coordinates to the application, causing this exact offset. The user-side solution is to disable this setting for the site or set privacy.resistFingerprinting to false in about:config.
This specific case cannot be fixed within the egui code itself.
If the user has zoomed in/out in their browser, the devicePixelRatio changes. This can cause offsets if egui doesn't recalibrate correctly. While eframe usually handles this automatically, ensure you are not manually overriding pixels_per_point with a fixed value:
// ❌ Avoid this if the zoom level can vary
// ctx.set_pixels_per_point(2.0);
// ✅ Let eframe handle it automaticallyUsing mimalloc (developed by Microsoft) with egui is a common and excellent choice for Rust desktop applications. In an "Immediate Mode" GUI like egui, the UI is rebuilt every frame, leading to frequent memory allocations. A performance-oriented allocator can help keep the frame rate stable.
- Lower Latency: mimalloc is designed to minimize "stop-the-world" moments and fragmentation, which helps prevent micro-stutters in your 60+ FPS UI loop.
- Immediate Mode Friendly: egui constantly allocates and deallocates small objects (vertex buffers, strings, layout shapes). mimalloc handles these small, short-lived allocations much faster than the default system allocator (especially on Windows).
- Efficiency: It generally offers a smaller memory footprint over time due to better fragmentation management.
Integration is straightforward and only takes a few lines of code.
Add this to your Cargo.toml:
[dependencies]
mimalloc = "0.1"In your main.rs (or lib.rs), declare it as the global allocator. This must be done at the root of the file, outside of any function.
use mimalloc::MiMalloc;
#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;
fn main() {
let native_options = eframe::NativeOptions::default();
eframe::run_native(
"egui App with mimalloc",
native_options,
Box::new(|cc| Box::new(MyApp::new(cc))),
).expect("Failed to run app");
}Requirements
A C compiler is required for building mimalloc with cargo.
Using secure mode adds guard pages, randomized allocation, encrypted free lists, etc. The performance penalty is usually around 10% according to mimalloc own benchmarks.
To enable secure mode, put in Cargo.toml:
[dependencies]
mimalloc = { version = "*", features = ["secure"] }By default this library uses mimalloc v2. To enable v3, put in Cargo.toml:
[dependencies]
mimalloc = { version = "*", features = ["v3"] }Do not use mimalloc for the WASM target. WASM environments manage memory differently, and mimalloc either won't compile or won't provide any benefit. You should use conditional compilation to keep it desktop-only:
#[cfg(not(target_arch = "wasm32"))]
use mimalloc::MiMalloc;
#[cfg(not(target_arch = "wasm32"))]
#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;For web applications, the size of the .wasm file is a critical performance metric (Load Time).
- Adding
mimalloc(a large C library) significantly increases the binary size. - In the WASM community, the trend is actually toward "tiny" allocators like
wee_alloc(though it is now unmaintained, it was designed to be the opposite of mimalloc: prioritizing size over speed).
While mimalloc is fast, it isn't a "magic wand" for performance:
- CPU usage: You might see a 5% to 15% reduction in time spent on memory tasks. 6 Frame Consistency: The biggest win is usually the reduction of frame-time spikes (jitter), making the scrolling and animations feel smoother.
jemalloc is another popular alternative, often used in heavy Linux server environments. However, for cross-platform desktop apps (Windows/macOS/Linux), mimalloc is generally preferred because it is easier to link and performant across all three.
[package]
name = "my_egui_application"
version = "0.1.0"
edition = "2021"
[dependencies]
# Disable unnecessary default features to reduce dependencies
eframe = { version = "0.27", default-features = false, features = [
"accesskit", # Accessibility
"default_fonts", # Basic fonts (essential)
"glow", # Rendering via OpenGL (lighter than WGPU natively)
"wayland", # For Linux
"x11", # For Linux
] }
[profile.release]
# 's' is often a better trade-off than 'z' for GUIs
# because 'z' can slow down graphical rendering too much.
opt-level = "s"
lto = true
codegen-units = 1
panic = "abort"
strip = true
# CRUCIAL OPTIMIZATION: Optimize dependencies as much as possible
# even in debug mode or if the main profile is set to 's' or 'z'.
[profile.release.package."*"]
opt-level = 3egui and eframe come with default features (like extra fonts or image formats) that you might not use. You can disable them to save space.
[dependencies]
egui = { version = "0.27", default-features = false, features = ["default_fonts"] }
eframe = { version = "0.27", default-features = false, features = ["wgpu", "glow"] }To compile an egui application (generally using eframe) into a native binary with a minimal output profile, you need to be a bit more cautious. Unlike a command-line utility, a graphical application depends on heavy system libraries and font/image management.
Below is a suggested Cargo.toml optimized for native builds, balancing binary size and rendering performance (as a GUI must remain fluid).
[package]
name = "my_egui_app"
version = "0.1.0"
edition = "2021"
[dependencies]
# Disable unnecessary default features to reduce dependencies
eframe = { version = "0.27", default-features = false, features = [
"accesskit", # Accessibility
"default_fonts", # Basic fonts (essential)
"glow", # Rendering via OpenGL (lighter than WGPU for native)
"wayland", # For Linux
"x11", # For Linux
] }
[profile.release]
# 's' is often a better compromise than 'z' for GUIs
# because 'z' can slow down graphical rendering too much.
opt-level = "s"
lto = true
codegen-units = 1
panic = "abort"
strip = true
# CRUCIAL OPTIMIZATION: Maximize optimization for dependencies
# even if the main profile is set to 's' or 'z'.
[profile.release.package."*"]
opt-level = 3Choosing opt-level = "s" instead of "z"
For a graphical application, smoothness (60 FPS) is the priority. The "z" optimization can sometimes break critical loop optimizations required for pixel rendering. "s" seeks a compromise: reducing size without brutally sacrificing execution speed.
The [profile.release.package."*"] trick
This is the "secret" for complex Rust projects. It tells Cargo: "Optimize my own functions for size, but compile all external libraries (like the graphical rendering engine) with maximum optimization (3)." This keeps the interface ultra-responsive while reducing the weight of your business logic.
Feature selection in eframe
By default, eframe often includes wgpu for rendering. It is powerful but very heavy in terms of binary size (as it includes complex shader compilers).
- By using
glow(OpenGL), your binary will be significantly lighter. - Remember to disable
default-featuresto keep only what is strictly necessary for your target platform.
Caution with panic = "abort" and windows
Using panic = "abort" is excellent for size, but keep in mind that in the event of a crash, the application will close instantly without leaving console logs or a proper error window. For native apps, this is often acceptable.
A final tip for image weight
If you display images in your egui application, use the WebP format or ensure you compress your assets before compilation, as they are often included directly in the binary via include_bytes!.
The most significant gains come from telling the compiler to prioritize binary size.
[profile.release]
# Optimize for size ('z' is more aggressive than 's')
opt-level = "z"
# Enable Link Time Optimization (LTO) to remove dead code across crates
lto = true
# Reduce parallel compilation to allow deeper optimization
codegen-units = 1
# Strip symbols and debug info from the binary
strip = true
# Immediately panic without stack unwinding (saves space)
panic = "abort"wasm-opt is part of the Binaryen toolkit. It performs passes on the generated WASM file that the Rust compiler cannot do. It can often reduce the size by another 20% to 40%.
Command:
wasm-opt -Oz -o output_optimized.wasm input.wasmSee: Binaryen (wasm-opt) on GitHub
This is the most effective way to reduce transfer size. WASM files are highly compressible. A 5MB file can often be served at around 1.2MB using Brotli.
- Brotli: Best compression ratio for web assets.
- Gzip: Faster but slightly larger than Brotli.
- Reference: MDN - Content-Encoding
If your file is still too large, use twiggy to find out exactly which functions or libraries are taking up the most space.
cargo install twiggy
twiggy top -n 20 your_file.wasmReference: Twiggy Documentation
Enabling wasm-opt in Trunk
Trunk has built-in support for wasm-opt. If you have the tool installed on your system (or if Trunk downloads it automatically), it will run as part of the --release build.
In your Trunk.toml (or as command line flags): You don't usually need to change anything if you run with the release flag, but you can verify it:
trunk build --releaseTrunk will look for wasm-opt in your path. If it's missing, you can install it via your package manager (e.g., brew install binaryen or sudo apt install binaryen).
- Reference: Trunk Documentation - Tools
Trunk Asset Pipeline (Hashing and Minification)
Trunk automatically handles cache busting by adding hashes to your .wasm filenames. This allows you to set long-term "Immutable" cache headers on your server, which improves perceived performance for returning users.
If you have a index.html file, ensure your link to the WASM/JS is handled by Trunk:
<link rel="rust" data-bin="my_app" data-wasm-opt="z" />The data-wasm-opt="z" attribute tells Trunk specifically which optimization level to pass to the optimizer.
Automatic Brotli/Gzip with trunk serve
When you use trunk serve, it doesn't necessarily compress files (as it's meant for local dev). However, for production, you should use the output of trunk build --release (the dist/ folder) and serve it with a web server that supports compression.
Pro-Tip: GitHub Pages / Vercel / Netlify If you deploy your dist/ folder to these platforms, they automatically apply Gzip or Brotli compression to .wasm files. You don't have to do anything!
Final Cargo.toml for Trunk Users
To make sure Trunk has the best "raw material" to work with, your Cargo.toml should look exactly like this for production:
[profile.release]
opt-level = "z" # Optimize for size
lto = true # Link Time Optimization
codegen-units = 1 # Maximum optimization potential
panic = "abort" # Remove stack unwinding code
strip = true # Remove all symbols/debug info- Reference: The Rust Wasm Book - Shrinking .wasm Size
- On desktop, the state is saved in the RON format within the system configuration file (
~/.local/share/<app_name>/app.ronon Linux). - On the web, eframe uses the browser's
localStorage.
Tools:
Tutorials:
WASM:
mimalloc:
- The Power of jemalloc and mimalloc in Rust — and When to Use Them
- crates.io: Rust Package Registry
- mimalloc 0.1.48 - Docs.rs
- GitHub - mi-malloc: mi-malloc
- microsoft/mimalloc: mimalloc is a compact general purpose allocator with excellent performance.
- Link with -lrt for older glibc by jserv · Pull Request #140 · microsoft/mimalloc ⚠
misc...
Contributions are welcome ! Feel free to open an issue or submit a pull request to improve this starter kit.
Developed with ❤️ using Rust.