Intermediate 12 min

Continuous input and frame pacing

Keep drag, scrub, resize, and other high-frequency work aligned with the display refresh cadence.

FrameCoalesced is Argui’s requestAnimationFrame-shaped input mode

Browsers may produce pointer events faster than they can present frames. GestureDelivery::FrameCoalesced keeps the newest visual state for each gesture stream and delivers at most one Changed event on each available Argui display frame. This has the same scheduling intent as requestAnimationFrame, while remaining portable across browser and native hosts.

The callback cadence is capped by the active display refresh rate—commonly 60, 120, or 144 Hz—and can be lower when the host is busy or backgrounded. It is not a timer and does not promise a fixed frequency.

src/view.rs
Interaction::default().gestures(
    GestureSet::EMPTY.pan(
        PanGesture::default()
            .immediate()
            .capture(GestureCapture::OnPress)
            .delivery(GestureDelivery::FrameCoalesced),
    ),
)

Coalescing preserves the data a visual interaction needs

For a pan, the delivered event uses the latest pointer position, total displacement, and velocity. Its delta is the sum of every raw delta received since the previous frame, so controlled split panes and other incremental consumers remain exactly under the pointer even when several input samples arrive between frames.

The live example updates the card transform, velocity label, and delivered-update counter from the same frame-coalesced callback. No intermediate text layout or model invalidation runs between two presentable frames.

Choose delivery from the workload

Use FrameCoalesced when each sample triggers rendering, layout, text shaping, hit-test changes, or substantial model work. Use Immediate only when every raw sample is itself application data and dropping intermediate samples would change the result.

DeliveryBest forCost model
FrameCoalescedDragging, split resizing, scrubbing, visual sliders, hover previewsAt most one Changed callback per gesture stream and display frame
ImmediateRaw telemetry, input recording, or algorithms that consume every sampleOne callback per input event; application code owns batching

Keep each delivered frame bounded

Frame coalescing prevents redundant callbacks, but it cannot make an expensive callback cheap. Prefer transform or paint changes over layout, retain stable keys, avoid rebuilding unrelated ownership graphs, and move durable work to the final commit or release event.

  • Use total displacement for absolute visual position and accumulated delta for incremental state.
  • Keep velocity smoothing frame-rate independent by scaling it from elapsed frame time.
  • Update dynamic labels in the same coalesced callback so their shaping is also capped to frame cadence.
  • Measure input-to-display latency in Firefox and Chromium rather than counting raw pointer events.

Reference files

These are the implementation and guide files used for this chapter.

docs/performance/optimizations.mdcrates/argui-ui/src/tree/pointer.rscrates/argui-ui/src/gesture/config.rscrates/argui-widget-gallery/src/pages/drag_drop.rs
Compiled example

Drag with position, velocity, and status delivered once per available frame

The Rust file below is imported verbatim by this page and compiled into the WebAssembly application running underneath it.

Open exact source
app_examples/docs-examples/src/examples/performance.rs
use argui::{
    accessibility::{Role, Semantics},
    core::{Color, Point, Transform2D},
    paint::{Border, CornerRadii},
    runtime::{Context, Render},
    text::TextStyle,
    ui::{
        AlignItems, CursorIcon, Element, EventType, FocusPolicy, GestureCapture, GestureDelivery,
        GestureKind, GesturePhase, GestureSet, Interaction, JustifyContent, PanGesture, Sides,
        UiEventKind, UserSelect, length, percent,
    },
    widgets::default_theme,
};

#[derive(Default)]
pub struct Example {
    offset: Point,
    velocity: Point,
    delivered_updates: u64,
    dragging: bool,
}

impl Render for Example {
    fn render(&mut self, cx: &mut Context<Self>) -> Element {
        let themes = default_theme(cx.environment());
        let theme = themes.resolve(cx.environment().color_scheme);
        let orb = Element::row([
            Element::text(if self.dragging { "HOLDING" } else { "DRAG" }).text_style(TextStyle {
                color: Color::WHITE,
                font_size: 11.0,
                line_height: 15.0,
                weight: 760,
                ..TextStyle::default()
            }),
        ])
        .absolute(Sides {
            left: length(220.0),
            right: argui::ui::auto(),
            top: length(72.0),
            bottom: argui::ui::auto(),
        })
        .width(length(88.0))
        .height(length(56.0))
        .align_items(AlignItems::CENTER)
        .justify_content(JustifyContent::CENTER)
        .background(theme.primary)
        .radius(CornerRadii::all(14.0))
        .transform(Transform2D::IDENTITY.translate(self.offset.x, self.offset.y));
        let pad = Element::container([orb])
            .keyed("frame-coalesced-pad")
            .width(percent(1.0))
            .height(length(200.0))
            .background(theme.muted)
            .border(Border::all(1.0, theme.border))
            .radius(CornerRadii::all(14.0))
            .user_select(UserSelect::None)
            .interaction(
                Interaction::default()
                    .focus_policy(FocusPolicy::TabStop)
                    .cursor(if self.dragging {
                        CursorIcon::Grabbing
                    } else {
                        CursorIcon::Grab
                    })
                    .gestures(
                        GestureSet::EMPTY.pan(
                            PanGesture::default()
                                .immediate()
                                .capture(GestureCapture::OnPress)
                                .delivery(GestureDelivery::FrameCoalesced),
                        ),
                    ),
            )
            .semantics(
                Semantics::new(Role::Group)
                    .label("Frame-coalesced drag pad")
                    .description("Drag to observe one visual update per available frame"),
            )
            .on(cx.listener(EventType::Gesture, |example, event, cx| {
                let UiEventKind::Gesture(gesture) = event.kind else {
                    return;
                };
                let GestureKind::Pan {
                    total, velocity, ..
                } = gesture.kind
                else {
                    return;
                };
                if gesture.phase == GesturePhase::Started {
                    example.delivered_updates = 0;
                    example.dragging = true;
                }
                if gesture.phase == GesturePhase::Changed {
                    example.delivered_updates = example.delivered_updates.saturating_add(1);
                }
                example.offset =
                    Point::new(total.x.clamp(-210.0, 210.0), total.y.clamp(-68.0, 68.0));
                example.velocity = velocity;
                if matches!(gesture.phase, GesturePhase::Ended | GesturePhase::Cancelled) {
                    example.dragging = false;
                }
                event.stop_propagation();
                cx.notify();
            }));
        let status = format!(
            "Delivered frame updates: {} · velocity {:.0}, {:.0} px/s",
            self.delivered_updates, self.velocity.x, self.velocity.y
        );

        Element::column([
            Element::text("Frame-coalesced continuous input").text_style(TextStyle {
                color: theme.foreground,
                font_size: 18.0,
                line_height: 24.0,
                weight: 700,
                ..TextStyle::default()
            }),
            Element::text(
                "Move quickly: position, velocity and this status are delivered together at most once per available display frame.",
            )
            .text_style(TextStyle {
                color: theme.muted_foreground,
                ..TextStyle::default()
            }),
            pad,
            Element::text(status.clone())
                .text_style(TextStyle {
                    color: theme.muted_foreground,
                    font_size: 13.0,
                    line_height: 18.0,
                    weight: 550,
                    ..TextStyle::default()
                })
                .semantics(Semantics::new(Role::Status).label(status)),
        ])
        .width(percent(1.0))
        .height(percent(1.0))
        .padding(Sides::length(28.0))
        .gap(14.0)
        .background(theme.background)
    }
}