I added one sparkline to AirStats, about forty pixels wide, and memory went from 11 MB to 104 MB.
My first assumption was that I had leaked something, because that is what a 93 MB jump looks like. I had not. That is what Canvas costs, and it costs it to everyone.
Why I reached for Canvas in the first place
AirStats draws about a dozen small charts: a sparkline per metric in the panel, per core bars, a network graph.
The obvious tool for that in SwiftUI is Canvas. It is the imperative drawing surface, it hands you a GraphicsContext, and for a chart that redraws once a second it looks like the right primitive. Every tutorial you will find agrees with you.
It is the wrong one, and the reason has nothing to do with how much you draw.
The cost is admission, not usage
The first time any Canvas anywhere in your process renders a single frame, SwiftUI brings up the Metal rendering path, which costs roughly 93 MB of resident memory.
The part that took me a while to believe is that the figure does not scale. Not with the size of the canvas, not with the number of canvases, not with the complexity of what you draw. One Canvas drawing one line costs the same as fifty drawing full charts. It is a fixed process-global allocation and you pay it the moment the first frame lands.
You also never get it back. Dismiss the view, tear down the window, wait. The allocation stays for the lifetime of the process.
Whether that matters depends on what you are
For most apps this is invisible, and I want to be fair about that. If you are a document editor already sitting at 400 MB, another 93 MB for a real GPU-backed rendering pipeline is a bargain.
For a menu bar utility whose entire pitch is that it costs nothing to run, it is the whole budget and then some. AirStats cold is 11 MB, so a single Canvas would have made it 104 MB, before the panel, settings, or anything else the app actually does.
What I did instead
Drew the charts as plain SwiftUI shapes. That is the entire fix.
- A sparkline is a
Pathbuilt from the sample buffer and stroked. - A bar chart is a stack of rounded rectangles.
- Per core load is a
GeometryReaderand some capsules.
None of that touches the Metal path, and none of it costs anything beyond the view tree it creates.
You do give up real things. There is no per-frame imperative drawing, no easy blend modes, and complex paths get genuinely annoying to express. For charts that are fundamentally lines and rectangles, that turned out to cost almost nothing.
This one decision is most of the memory gap in the benchmark: 11 MB cold against 145 MB for Stats and 64 MB for the iStat Menus suite. Warm, once the panel and settings have been drawn, AirStats settles near 70 MB, which is roughly where a Canvas version would have started.
How to check your own app
Attach to the process and watch resident size across the first frame that contains a Canvas. If you see a step change of around 90 MB that never comes back down, that is this.
Check even if you are certain you never wrote Canvas yourself. Some SwiftUI system components use it internally, so you can be paying for the Metal renderer without a single mention of it in your own source. That is how I would have found it in an afternoon instead of a week.