Making it feel native

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 computed instead.
  • 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 minSize to 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:

  1. Switch themes, including Automatic over a bright and a dark wallpaper. Nothing should stay stuck in your palette.
  2. Resize to minSize and to something huge. Both should look deliberate.
  3. Kill the network. The widget should degrade calmly, say so, and recover on its own when the network returns.
  4. Let it run for hours. Memory flat, log quiet, no badge.
  5. Open the log and watch a few minutes of normal operation; a widget that logs on every tick is noise nobody can debug around.
  6. Re-read your manifest permissions and delete any the widget no longer uses; every extra grant is install friction.
  7. Run two instances side by side. They should not fight over storage or look identical when configured differently.
  8. Toggle reduced motion in Windows. Your custom animations should honor it like the house ones do.