A few days ago I was talking to Google Gemini about Splitser, the tiling browser I have been building. It did not know what I meant. It kept telling me about a Dutch app for splitting restaurant bills -- splitser.com, an entirely different thing with a much better claim to the name. Only when I typed spltzr did it find the browser.
Then it read the project, liked it, and asked a good question: how do I handle memory for workspaces that are open but not on screen? I told it the truth. It replied with a compliment about my "tight process management" and how well I was "managing memory pressure".
I am going to decline that compliment, because it is not true, and the truth is more interesting.
Nothing is being managed
Splitser lets you keep whole workspaces open -- separate grids of panes, each with its own layout and its own theme -- and switch between them instantly. Switching away from one is a single line:
sets.forEach((x) => { x.el.style.display = (x === s) ? '' : 'none'; });
That is the whole mechanism. A backgrounded workspace is display:none. Its panes
stay in the page, alive, exactly as you left them. There is no code anywhere in the app that
throttles, discards, freezes or de-prioritises them -- I went looking, and there is nothing to
find. The only thing that destroys a workspace is closing it. (There is one use of the word “throttle” in the source, and it is about rate-limiting a status counter, not pages.)
So memory is not reclaimed, because reclaiming it is the thing I did not want. A workspace you come back to is still logged in, still scrolled where you left it, still playing whatever it was playing. That is the feature. The cost of the feature is that it is all still in RAM.
What it actually costs
Read from my own live session rather than estimated -- one app, one window, five days up:
- 29 workspaces, 52 panes, 71 tabs
- 17.7 GB resident across 92 processes, 83 of them renderers -- counted at one moment; the renderer count drifts as things open and close
- 35 GB of 94 GB system memory in use
Seventeen gigabytes is a lot. I am not going to dress that up as efficiency. It is the price of a decision, and the decision is that switching workspaces should be instant and lossless. If you would rather have the memory back, the app has a button for that: close the workspace.
But does it cost CPU?
This is where I expected to be right and was wrong. Gemini claimed that Chromium's background
throttling reads the layout state of the page -- so a guest view hidden with
display:none would get throttled like any other hidden page. I did not believe it.
In Electron, a <webview>'s visibility usually derives from the window, not from
CSS, and hidden guests often keep running at full speed.
So I tested it. A page burning 8ms of work every 10ms, parked in a workspace, then a second workspace opened so the first went hidden:
| Background page | Visible | Hidden |
|---|---|---|
| Silent | 88.6% of a core | 2.0% |
| Making sound | 89.6% of a core | 76.6% |
Gemini was right and I was wrong. A silent hidden workspace collapses to near nothing -- a 44x drop, same page, same timer. Chromium really does follow the layout state through the webview.
The second row is the interesting one, and it is easy to get wrong. Chromium deliberately exempts
pages that are making noise from background throttling -- but that is not what keeps your
music playing. The music keeps playing because the workspace is never discarded in the first
place, which is the display:none story from the top of this post. Audio does not run
on the page's JavaScript timers, so a throttled page would go on making sound regardless. What the
exemption actually buys is that a page whose audio is driven by JavaScript -- a scheduler,
a mixer -- does not glitch when it goes to the background. The side effect is the one in the
table: an audible background workspace keeps most of its CPU.
So the honest headline is: a backgrounded workspace stops costing you CPU -- unless it is making noise, in which case Chromium keeps it running on purpose.
How I measured it, and what that does not prove
Worth saying plainly, because measurements like this are easy to get wrong and I got them wrong twice before I got them right.
- The throttling numbers come from a throwaway instance with a fresh profile and two panes, not from my 29-workspace session. I was not going to park a CPU burner in a browser I actually use.
- They were taken about 15 seconds after hiding. Chromium has a second, harsher stage that engages around the five-minute mark, so 2.0% is an upper bound -- the real resting figure is probably lower.
- Two of my first three runs produced plausible, invalid numbers -- once the wrong element was clicked, once the second workspace never got created, so nothing was ever hidden. Both times the output looked reasonable. The result is only trustworthy because the test now asserts the burn page is genuinely in the hidden set before it believes anything.
- I also looked at the live session and found 73 of the 84 renderers running at that moment sitting under 0.2%. That proves nothing, and I want to be clear that it is not evidence: idle renderers might simply be idle pages. Only the deliberately-hot timer settles the question.
Not a recommendation
Twenty-nine workspaces at 17.7 GB is a demonstration of a trade, not a configuration to copy. I have 94 GB, so I can spend memory on never losing my place. On a 16 GB machine this would swap, and swapping would cost you the very thing the design is buying -- the instant switch.
That is the actual claim, and it is smaller than the compliment I was offered: Splitser trades memory for state, deliberately, and here is what that costs on one real machine. Decide which side of the trade you are on.
Muting does not help
The obvious next question is whether muting a noisy background workspace lets Chromium throttle it again. I assumed it would. It does not:
| Audible page | Visible | Hidden | Hidden + muted |
|---|---|---|---|
| Same burn timer | 88.3% | 75.6% | 76.0% |
Muting changes nothing -- run twice, and the second run confirms the mute actually applied rather than silently failing. The likely reason, and this is reasoning rather than something I measured: muting silences the output, but the page is still generating audio, so Chromium still counts it as recently audible and keeps the exemption. Muting quiets it for you; it does not tell the engine the page has gone quiet.
Which produces the one piece of practical advice in this post, and it is not the one I expected: the mute button is not a performance control. There is currently no way to make an audible background workspace cheap except to stop whatever is playing, or close the workspace.
What I still do not know: whether anything short of closing a workspace will get that CPU back.
Two weeks later: a 9.45 GB tab
On 15 September, six days into one run, a single renderer in this browser was holding 9.45 GB and still climbing about a megabyte a minute. Which is a good test of everything above, because the first question is not "how do I fix it" -- it is whose memory is that.
It was not Splitser's. Every pane's guest is launched with the blink-feature switches a <webview> carries; the app's own window is not. Of 101 renderers, exactly one lacked them -- the interface itself, at 255 MB. So the 9.45 GB belonged to a page I had open, not to the code I wrote. That distinction is the whole reason to measure before explaining.
Worth recording for anyone trying this: Chromium marks renderer processes non-dumpable, so /proc/<pid>/smaps_rollup, maps and fd all become root-owned and you cannot get a proportional-set-size reading on them. /proc/<pid>/status still works, and its RssAnon is the number you want anyway: 9.49 GB of that 9.6 GB was anonymous, shared with nothing, and never swapped.
Here is the part that matters for this post. Take that one page out of the total:
| Resident | Renderers | Per renderer | |
|---|---|---|---|
| Published above, 5 days up | 17.7 GB | 83 | 218 MB |
| Six days up, minus the one page | 20.7 GB | 100 | 212 MB |
Six megabytes apart. The cost of the decision had not moved at all; the difference between 17.7 GB and 30 GB was seventeen more renderers and a single misbehaving page. If someone points at a 30 GB process tree as evidence that this design is worse than I claimed, that is the answer, and it is an arithmetic one.
The page had also burned 14.3 hours of CPU in 6.3 days -- 9.4% of a core, sustained. Look back at the table further up: hidden and silent was 2.0%, hidden and audible about 76%. This sat in neither bucket. A page living in one of thirty-one workspaces is out of sight almost all of the time by construction, though I did not verify it was hidden at every moment, so take the inference for what it is.
I could not tell you which tab it was
And that was the real gap. I had a process ID, a size, and no way to turn either into a tab. The sandbox that makes a renderer safe is the same sandbox that stops you asking it where it lives.
So 0.1.37 has a task manager: every tab in every workspace, sorted by resident memory, with the workspace and pane it sits in, cumulative CPU since that process started, live CPU, and whether it is making noise. Two things it deliberately does not do. It does not discard or throttle anything -- that is the decision this whole post is about, and an instrument that quietly overrides it would be worse than no instrument. And it does not pretend tabs and processes are the same thing:
| On my own session | |
|---|---|
| 91 tabs, in 90 processes | 17.55 GB |
| 16 iframe processes no tab owns | 1.93 GB |
| Browser, GPU, everything else | 1.73 GB |
| Total, 111 processes | 21.21 GB |
Ninety-one tabs in ninety processes, because two same-site tabs share one. Count per tab and you bill that memory twice. And cross-origin iframes are real renderer processes belonging to no tab at all -- nearly two gigabytes of this machine -- which a per-tab table would either drop or staple to the wrong row. So a process is counted once, orphans are listed as orphans, and the total reconciles with ps. A memory table that quietly sums to the wrong number is worse than not having one.
It named the page on the first look: a local analytics dashboard of my own, parked in a workspace I had not opened in days. Not a site doing something exotic. Mine.
The open question, answered
Above I wrote that I did not know whether anything short of closing a workspace gets that CPU back. It does: reload the one tab. The memory returns immediately, the CPU resets, and the workspace -- its layout, its other panes, its place in the bar -- is untouched.
Which is a duller answer than I expected, and the interesting part is why I could not give it in September. The mechanism was never missing. Reload has been a button on every pane since the first version. What was missing was knowing which of ninety-one tabs to point it at, and no amount of thinking about Chromium's throttling policy was going to supply that. It needed a measurement I had not built yet.
What I still do not know: why that page never throttled. It was quiet, it was out of sight, and by the table above it should have collapsed to about 2% of a core. It held 9% for six days instead. A dashboard fed by a push stream rather than by a timer would explain it, since the throttling I measured applies to timers -- but I have not tested that, so it stays a guess, and this is the third thing on this page I was confident about and wrong.