perf(ui-dioxus): 6-step color fade without CSS transition engine (~0% CPU)

This commit is contained in:
Hibryda 2026-03-19 07:28:38 +01:00
parent 8b5a4daf72
commit 3e6307ffd0
2 changed files with 36 additions and 26 deletions

View file

@ -1,12 +1,11 @@
/// PulsingDot — smooth pulse via background-color transition in Blitz.
/// PulsingDot — smooth pulse via 6 manual color steps. Zero CSS animation overhead.
///
/// Toggles between bright (green/peach) and dim (surface2) every 800ms.
/// CSS `transition: background-color 0.4s ease` smooths the switch.
/// The transition runs for 400ms (~24 repaints), then stops until next toggle.
/// Net cost: ~24 repaints per second spread across two 400ms bursts = ~2% CPU.
/// 6 pre-computed color classes cycled every 200ms = 1.2s full pulse.
/// Each step: 1 class change → 1 Blitz repaint. No CSS transition engine.
/// Cost: 5 repaints/sec × 1 element = unmeasurable CPU.
use dioxus::prelude::*;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::Arc;
use std::time::Duration;
@ -19,27 +18,30 @@ pub enum DotState {
Error,
}
const STEP_MS: u64 = 200;
const STEPS: u8 = 6;
#[component]
pub fn PulsingDot(state: DotState, size: Option<String>) -> Element {
let should_pulse = matches!(state, DotState::Running | DotState::Stalled);
let is_bright = use_hook(|| Arc::new(AtomicBool::new(true)));
let step = use_hook(|| Arc::new(AtomicU8::new(0)));
if should_pulse {
let flag = is_bright.clone();
let s = step.clone();
let updater = use_hook(|| dioxus::dioxus_core::schedule_update());
use_hook(move || {
std::thread::spawn(move || {
loop {
std::thread::sleep(Duration::from_millis(800));
flag.fetch_xor(true, Ordering::Relaxed);
std::thread::sleep(Duration::from_millis(STEP_MS));
s.store((s.load(Ordering::Relaxed) + 1) % STEPS, Ordering::Relaxed);
updater();
}
});
});
}
let base_class = match state {
let base = match state {
DotState::Running => "dot-running",
DotState::Idle => "dot-idle",
DotState::Stalled => "dot-stalled",
@ -47,17 +49,20 @@ pub fn PulsingDot(state: DotState, size: Option<String>) -> Element {
DotState::Error => "dot-error",
};
let brightness = if should_pulse {
if is_bright.load(Ordering::Relaxed) { "dot-bright" } else { "dot-dim" }
let step_class = if should_pulse {
match step.load(Ordering::Relaxed) {
0 => "dot-s0", 1 => "dot-s1", 2 => "dot-s2",
3 => "dot-s3", 4 => "dot-s4", _ => "dot-s5",
}
} else {
"dot-bright"
"dot-s0"
};
let sz = size.unwrap_or_else(|| "8px".to_string());
rsx! {
span {
class: "pulsing-dot {base_class} {brightness}",
class: "pulsing-dot {base} {step_class}",
style: "width: {sz}; height: {sz};",
}
}