Skip to content

How my Mac live wallpaper idles at 0.04% CPU

My live wallpaper idles at 0.04% CPU on a Mac. How it draws one picture of the sky a minute, stops when you can't see it, and what ate its memory instead.

By Published 12 min read

On my M6 Mac mini, Orbit, the live sky wallpaper I make, averaged 0.04% of one CPU core over five idle minutes with the desktop in view, in the optimised (Release) build, and held 22 MB of memory. Mostly it gets there by doing nothing on purpose: one still picture of the sky a minute, drawn off the main thread, and nothing at all while nobody can see the desktop. The one thing that moves, the space station during a pass, is handed to Core Animation in one go, so the app sleeps while it crosses.

Orbit: Live Sky Desktop is my first Mac app. The measuring scripts and memory hunts below happened in my Claude Code sessions (Claude is co-author on every commit in the repo); I set the condition at the start and make the calls on looks, from rendered pictures. Every measurement comes from one machine, an Apple M6 Mac mini on macOS 27.0 driving one 1920 x 1080 display at 1x. It has no battery, so for battery life and an Activity Monitor checklist for any wallpaper, see does live wallpaper drain battery on a MacBook.

On 27 September I said build it, on one condition: it must never slow my Mac down, because I work on other projects on it all day. 22 MB turned out to be about a fifteenth of one percent of my Mac mini's 32 GB. Here are the limits I set, against what I measured:

The limitWhat I measured
Under 1% CPU when idle0.04% over 5 minutes, optimised build (0.60% in the busiest 5 seconds)
Under 60 MB of memory22 MB idle; 33 MB with the briefing card open, 38 MB the highest seen
Animation only when something real moves, 30 frames a second at mostonly the station during a pass; it asks for 30, and the app can't read the real rate
A full stop when the desktop is hidden0.00% CPU and 19 MB, tested behind the lock screen
No GPU work when idlenot measured yet
Measured on one Apple M6 Mac mini, macOS 27.0, one 1920 x 1080 display at 1x, 1 to 5 October 2026. Debug builds except the idle CPU figure. CPU is a share of one core; memory is the footprint reading.

How do live wallpapers work on a Mac?

With a window, because macOS has no live-wallpaper API. The one official call, setDesktopImageURL(_:for:options:), sets the wallpaper to an image file, and nothing in it plays.

So the usual way, and Orbit's, is a borderless window (one with no title bar or edges) over each display. Orbit's sits one level below the level Apple gives desktop icons, so the icons stay on top and the menu bar and Dock sit far above. A few lines in DesktopWindow.swift do most of the rest:

collectionBehavior = [.canJoinAllSpaces, .stationary, .ignoresCycle, .fullScreenNone]
ignoresMouseEvents = true
colorSpace = .sRGB

canJoinAllSpaces puts the window on every Space, like the menu bar. stationary keeps it put when Mission Control shuffles everything else. ignoresMouseEvents lets clicks pass straight through to the desktop beneath, as if Orbit weren't there. ignoresCycle keeps it out of Command-backtick window switching, and fullScreenNone means it can't be made full screen.

The window can never become the key window (the one your typing goes to), and it stays off screen until its first picture is ready, so the desktop never flashes an empty sky.

Quit Orbit and your own wallpaper is back. To set one up rather than build one, see how to get a live wallpaper on a Mac.

How often does a live wallpaper need to redraw?

For a sky, once a minute. On Orbit's 120-degree view the sky creeps about 4 pixels a minute across a 1080p screen (two pictures an hour apart show it), so more redraws would buy nothing you could see. At that pace you notice the sky has moved by looking up after an hour, not by watching.

There is one clock in the whole app, MinuteClock, and its only repeating timer fires a second after each whole minute:

let timer = Timer(fire: Date(timeIntervalSinceReferenceDate: nextMinute + 1), interval: 60, repeats: true) { [weak self] _ in
    MainActor.assumeIsolated { self?.onTick?() }
}
timer.tolerance = 10

The tolerance lets macOS fire it up to ten seconds late, so the wake-up can ride along with others the system already has planned. Apple's Energy Efficiency Guide for Mac Apps suggests at least 10% of the interval for a repeating timer, and ten seconds in sixty is about 17%. The only other ticks are one-offs, for the second a pass begins or ends and the end of a pass preview.

What happens each minute0 checked
  1. Up to ten seconds late, by design.
  2. Each window carries a stamp, the minute plus a counter that goes up when a setting changes. Only windows that are out of date (a new minute, a changed setting or display, a pass starting or ending) and can be seen get redrawn.
  3. Off the main thread, at low priority (higher when you've just changed a setting), into one CoreGraphics bitmap in sRGB per display.
  4. The main thread sets it as the layer's contents with animations off, so nothing crossfades, then does nothing until next time.

Tap a box as you go. Checks reset when you leave this page.

A 1080p sky takes 7 to 8 ms at full priority in the optimised build. Orbit asks for the low "utility" priority, though, so its drawing queues behind your own work. In my Debug builds the same code averaged 18 ms one morning, 29 ms that afternoon and 66 to 117 ms behind the lock screen. Same renderer, file for file; what moves the number is when macOS gets round to low-priority work.

Does a live wallpaper keep running behind full-screen apps?

Orbit is built not to. Each desktop window asks AppKit for its occlusionState, which says whether any part of it can be seen:

var isShowingDesktop: Bool { occlusionState.contains(.visible) }

The same Energy Efficiency Guide is blunt about what to do with it: when none of a window can be seen, "your app should halt all work performed in that window."

That one signal covers a full-screen app's Space, the lock screen, the screen saver and a sleeping display. It also listens for screensDidSleepNotification, and for power-source changes, which IOKit sends through IOPSNotificationCreateRunLoopSource, so nothing polls the battery. On battery below 20%, the clock stops. The 20% isn't a setting.

When any of those hold, the clock throws its timer away. Until the pause ends nothing redraws, and the briefing waits too unless you open the card. Then Orbit draws once. I've only tested the pause behind the lock screen (my Mac mini has no battery for the last case), and there it read 0.00% CPU over 150 seconds and 19 MB.

Moving the ISS with Core Animation keyframes

During a visible pass the space station crosses the desktop as a small layer above the still sky: two tiny images, a white-hot point in a cyan glow and a tapering trail. I haven't watched a live one on my real desktop yet; I've seen passes only in log lines and in captures with the clock faked (for a nudge before a real one, see ISS pass alerts on a Mac). Orbit works out where the station will be each second of the pass and hands the whole list to Core Animation in one go:

keyframes("position", path.points.map { NSValue(point: $0) }, on: layer)
keyframes("transform.rotation.z", path.headings, on: layer)
keyframes("opacity", path.brightness, on: layer)
keyframes("transform.scale.x", path.trails.map { max(0.001, $0 / Self.trailLength) }, on: trail)

A CAKeyframeAnimation fills in the frames between those values, and WindowServer, the macOS process that puts every window on screen, does the drawing. Orbit's own process isn't woken to move the station, and the sky underneath isn't redrawn for it. I never needed a display link (a callback on every frame) or a per-frame timer. Each animation asks for at most 30 frames a second, though the app has no way to read the rate WindowServer really composites at:

static let frameRate = CAFrameRateRange(minimum: 15, maximum: 30, preferred: 30)

A pass already under way gets a beginTime in the past, so the station starts from wherever it has got to. The sky itself redraws once as a pass begins and once as it ends, turning to face the pass and back again if the direction you picked doesn't already show it.

Close-up of a small white point in a cyan glow with a thin cyan trail fading up and to the right, beside faint constellation lines and a star labelled Regulus, on a dark blue sky that lightens towards the bottom.
Seven minutes into the space station's predicted pass over Berlin, 6:37 AM on 21 October 2026, cropped from a sky facing east with labels and constellation lines on. Computed on 20 October; pass times can shift as the orbit data updates. Drawn by Orbit for Mac 1.0.
A closer look

Close-up of a small white point in a cyan glow with a thin cyan trail fading up and to the right, beside faint constellation lines and a star labelled Regulus, on a dark blue sky that lightens towards the bottom.

Over a 617-second pass, with the app's clock set forward to the evening, the screen locked and drawing forced on, Orbit read 0.0% CPU in every 5-second sample except the minute's redraw (2.3%), and 22 MB throughout. It kept time to a tenth of a second: 616.9 seconds by the wall clock for a 617-second pass. It was that evening's pass, played behind my lock screen at a quarter to three in the afternoon.

WindowServer was harder to pin down, and my own measuring kit was the problem. The Claude session running the test redrew its window busily enough to hold WindowServer at 41 to 51%, with or without Orbit. Against that, the 12-second pass preview made no step you could see: 47.4% before, 47.7% during, 46.7% after. Whatever the station costs WindowServer is lost in a few points of noise.

How much memory does a live wallpaper need on a Retina or 5K display?

Each display gets a picture sized to its points times its scale, up to 2x, and all the displays together are capped at 10 million pixels (though never below 1x):

static let pixels = 10_000_000.0

A sky picture is four bytes a pixel and stays in memory while it's shown, so the cap is 40 MB. Past it, every display's picture is drawn coarser by the same factor and the layer scales it up.

DisplaysFull-sharpness pictureWhat Orbit drawsSky pictures in memory
One 1080p display at 1x1920 x 1080 (2.1 million pixels)the same, at 1xabout 8 MB
14-inch MacBook Pro3024 x 1964 (5.9 million)the same, at 2xabout 24 MB
4K display set to look like 1920 x 10803840 x 2160 (8.3 million)the same, at 2xabout 33 MB
Studio Display (5K)5120 x 2880 (14.7 million)4216 x 2372, at 1.65x40 MB
14-inch MacBook Pro plus a Studio Display20.7 million togetherboth at 1.39x40 MB
Two Studio Displays29.5 million togetherboth at 1.16x40 MB
What Orbit 1.0's render budget draws for some common setups, computed from RenderBudget.swift on 20 October 2026. Four bytes a pixel; each Retina display taken at the mode where its points times 2 equal its native pixels.

So my 22 MB is the small-screen figure, and I haven't had a Retina or 5K display to measure the whole app on.

How to see what's eating a Mac app's memory (footprint)

Every memory figure here is a reading from footprint, Apple's command-line tool for the memory the system counts against a process. Plain footprint breaks it down by kind, footprint -v lists every memory region, and heap and malloc_history (run with MallocStackLogging=1) say who allocated what.

The colour space that copied every image

The first real sky measured 45 MB, with a 60 MB peak, against 14 MB for the placeholder before it. An opaque pixel format was the first suspect and wasn't the cause. Instead, Core Animation was re-rendering every sky picture into a full-size copy of its own, because the window was in the display's colour space and the picture was in sRGB. ColorSync had built 27 lookup tables of 272 KB each for that and for the Moon icon drawn into AppKit's own contexts. And OrbitKit's cached star catalogue was sitting in memory.

Together the fixes took it from 45 MB down to 19 MB (27 MB peak): the window declared sRGB (that colorSpace line), the icons drawn into sRGB bitmaps, and the catalogue loaded without its cache.

If you're hunting the same thing, a full-size "CoreAnimation" region in footprint, or "CG Raster Data" counted more than once per display, means an image is being copied. Blocks of 272 KB in multiples of three in heap are ColorSync tables.

NSPopover memory usage on macOS 27

The briefing card under the menu bar Moon first lived in an NSPopover, the standard speech bubble that hangs from a menu bar icon. Every open and every close added about 40 MB of graphics memory at once, and macOS took about fifteen seconds to hand it back.

footprint -v showed the spike as IOSurfaces tagged RenderBox, 420 x 542 in half-float RGBA. That's wider than the 344-point card, so it was the popover's own chrome (its frame and arrow). A popover holding one empty AppKit view and a label spiked just the same. On macOS 27 the popover draws its chrome in the app's process through Metal, and nothing you put inside it changes that.

The fix is CardPanel, a borderless, non-activating NSPanel of Orbit's own, hung under the Moon. The same card sat at 28 MB open, with no spike. A panel has to close itself, though, and my first one had square shadow "ears" poking out of its rounded corners, visible only in a capture of the real window. The before-and-after numbers are in how my first Mac app got through App Review.

Orbit's briefing card for Berlin, Tuesday 20 October: a starry evening sky across the top, then tiles for sunrise at 7:42 AM with dark by 7:15 PM, the Moon 69% lit, waxing gibbous and up 18° in the southeast, an ISS pass at 6:30 AM tomorrow lasting 11 minutes, west to east at 66°, Mars, Jupiter and Saturn with their directions, the next launch in 6h 11m, and Kp 1.0, Quiet.
Orbit's briefing card for Berlin at 7:00 PM on 20 October 2026, with the data it had cached that day. The ISS tile's 6:30 AM pass is the one in the close-up above, seven minutes in. Drawn by Orbit for Mac 1.0.
A closer look

Orbit's briefing card for Berlin, Tuesday 20 October: a starry evening sky across the top, then tiles for sunrise at 7:42 AM with dark by 7:15 PM, the Moon 69% lit, waxing gibbous and up 18° in the southeast, an ISS pass at 6:30 AM tomorrow lasting 11 minutes, west to east at 66°, Mars, Jupiter and Saturn with their directions, the next launch in 6h 11m, and Kp 1.0, Quiet.

SwiftUI Canvas performance: about 45 MB for a few rings

Four days later the redesigned card got faint background rings, drawn in a SwiftUI Canvas. Opening the card took the footprint from 23 MB to 69 MB; the same rings as plain Circle shapes came to 31 MB. The numbers point to a Canvas drawing through the same renderer as the popover's chrome. That's my inference, not anything Apple documents.

To be fair to Apple, its Canvas page pitches it as possibly faster for a complex drawing with dynamic data. For a few rings in a card that opens and closes all day, it cost me memory instead. The popover put a rule in the project's notes: nothing that draws through Metal inside Orbit (Metal itself, a display link, an NSPopover, a glass effect) goes in without being measured first. The rings put the Canvas on that list.

What I haven't measured yet

The GPU, for a start. powermetrics needs sudo, the measuring ran in Claude Code sessions that don't use it, and I haven't run it myself yet. Neither the still sky nor the pass needed Metal, but WindowServer putting them on screen is GPU work Orbit can't see from inside. I also owe a WindowServer reading from a quiet desktop.

Then there are the 122 wake-ups. In one five-minute run on 5 October, while I was typing and moving windows, some notification from macOS woke Orbit about every two and a half seconds. CPU stayed at 0.11%, and quiet runs counted 6 or 7 in the same time. Paused, it still counted 3 in 150 seconds. I haven't looked into either yet, but Orbit runs its whole minute check on every occlusion change of any window in the app, so that's where I'll look first.

Getting Orbit

If you'd like a wallpaper that spends almost all of every minute doing nothing, and shows the real sky over your city while it does, that's Orbit: Live Sky Desktop. The space station crosses it during visible passes, and a Moon in the menu bar opens tonight's briefing. It's $6.99 once on the Mac App Store (₹699 in India), with no subscription or in-app purchases, for macOS 15 or later on Apple silicon and Intel. Screenshots are on the Mac page.

Common questions

How do live wallpapers work on a Mac?

macOS only lets an app set the wallpaper to an image file. So the usual way, and Orbit's, is a borderless window of the app's own just below the desktop icons, on every Space, that lets clicks pass straight through to the desktop beneath. Orbit draws one still picture of the sky into that window each minute. Quit it and your own wallpaper is back.

Does a live wallpaper affect FPS in games?

A full-screen game covers the desktop, and a wallpaper that checks for that can stop drawing. Orbit asks macOS whether any part of its desktop windows can be seen, and stops completely, with no timer running, when none can. Paused, it measured 0.00% CPU on my Mac mini. I've tested that pause behind the lock screen, not behind a real full-screen game.

Why is WindowServer using so much CPU?

WindowServer is the part of macOS that puts every window on screen, so its CPU climbs with everything that draws: animations, video, an app redrawing itself. While I measured Orbit, the Claude session running my tests held it at 41 to 51% with or without Orbit, and Orbit's pass preview added no visible step. To find your culprit, open Activity Monitor and quit apps one at a time until it drops.

How do I check an app's memory usage on a Mac?

Open Activity Monitor and look at the Memory tab. Developers can run footprint in Terminal with the app's name or process ID. It reports the memory the system counts against the app, broken down by kind, and footprint -v lists every memory region. Every memory figure I give for Orbit is a footprint reading.

Does SwiftUI Canvas perform better than shapes?

Apple says a Canvas might perform better for a complex drawing with dynamic data. For a few rings in Orbit's card on macOS 27 it cost memory instead: 23 MB to 69 MB as the card opened (77 MB at the peak), against 31 MB with the same rings as plain Circle shapes. Measure your own case before choosing.