Performance and quality
Polling intervals, animation discipline, effect hygiene, behavior at extreme sizes, and a self-check list before you publish.
A widget is not a page someone closes. It runs for days on a desktop that is usually being ignored, next to a dozen others doing the same. The bar is simple to state: no leaks, and no work whose result nobody is looking at.
Poll like a good neighbor
Every timer-driven fetch goes through
dd.poll, and the
interval should match how fast the data actually changes: weather is a
30-minute question, a build status maybe a minute. Error backoff is
automatic; do not stack your own retries on top. Remember that every
instance polls independently and
rate limits are per
instance, so an interval that seems fine once should still seem fine three
times.
Events beat timers
Never rebuild a data source the host already pushes. The one-second clock
timer is the canonical mistake: dd.time.onTick exists, is second-aligned,
and costs nothing extra. The same goes for system vitals, now-playing
state, the audio spectrum, and folder changes; the
event reference is the menu. A widget with no
timers of its own is usually a widget with no drift, no leaks, and no
wasted wakeups.
Animation frames: when to run and when to stop
A requestAnimationFrame loop is a promise to do work every frame, so it
must be tied to something visibly changing:
- Drive canvas drawing from the data stream where possible. The audio spectrum stream, for example, simply stops sending frames when playback goes silent; a visualizer that draws per received frame stops with it, free.
- If you do run your own loop, stop it when the data goes static, and render nothing at all when values did not change.
- State changes (hover, expand, show) animate with CSS transitions, not frame loops, using opacity and transform only.
The low-spec flag
The dd.ready context carries perf.lowSpec, set when the user's machine
or settings ask widgets to go easy. Honor it in canvas-heavy widgets: cap
your frame work around 30 fps and render at 1x device pixels instead of
the display's scale factor. Some streams adapt on their own (the spectrum
drops its rate); your own drawing is your job.
Effect and subscription hygiene
- Every
on*subscription returns an unsubscribe function. If a subscription belongs to a region you rebuild, keep the function and call it when the region goes; the SDK's auto-dispose of bindings is a backstop, not a lifecycle. - Keep effects small: one
element, one concern. An effect that writes a signal another effect
reads is how update storms start; derive with
computedinstead. - Follow the one rule: bindings on built-once structure, no bindings inside rows a re-render discards.
Never block the main thread
The host pings widgets for liveness; a widget that stops answering (a synchronous loop, a giant JSON parse) is declared crashed and replaced with a reload overlay. Chunk heavy work, keep per-event handlers small, and remember that a busy-looping widget also blocks its own reloads until the loop ends. If you need to burn CPU, something upstream of your widget is usually wrong.
The extremes of the grid
Users will make your widget both smaller and larger than you designed it:
- Set
minSizeto the smallest genuinely useful shape, then actually test it there. Truncate honestly (ellipsis, fewer rows) instead of clipping or overlapping. - Test large too. Readouts should scale (the autoscale pattern), and lists should scroll rather than stretch.
- Sizes are grid cells whose pixel size varies per user, so test by resizing, not by trusting numbers.
The pre-publish self-check
Fifteen minutes with your own widget before submitting catches most rejections and most one-star moments:
- Switch themes, including Automatic over a bright and a dark wallpaper. Nothing should stay stuck in your palette.
- Resize to
minSizeand to something huge. Both should look deliberate. - Kill the network. The widget should degrade calmly, say so, and recover on its own when the network returns.
- Let it run for hours. Memory flat, log quiet, no badge.
- Open the log and watch a few minutes of normal operation; a widget that logs on every tick is noise nobody can debug around.
- Re-read your manifest permissions and delete any the widget no longer uses; every extra grant is install friction.
- Run two instances side by side. They should not fight over storage or look identical when configured differently.
- Toggle reduced motion in Windows. Your custom animations should honor it like the house ones do.