I was sure it was a memory leak. It was a glowing border using 131 % of a CPU core

2026-11-04来源 dev.to CSSEnglish

My app is an Electron desktop for Claude Code: glass windows with backdrop-filter blur, a WebGL rain background, a few terminals. One day I wrote in my own bug list: "If I leave it running for a while, it gets really slow."

"Slow after a while" sounds like a memory leak. I was so sure that I nearly went looking for one. Then I measured first.

The measurement

One Claude Code session working in a terminal, the app in front. Numbers are CPU time per process, as a percentage of one core:

Version GPU process Renderer
installed 1.9 131 % 30 %

Memory was flat. Main process around 110 MB, renderer heap around 20 MB, no growth. No leak.

What actually happened: the laptop (integrated Intel graphics) got hot and throttled after a few minutes. "Slow after a while" was thermals, not memory.

So what was burning a core and a third on the GPU process while I was just watching a terminal?

Suspect 1: the glow

When Claude is working, its window gets a soft, breathing glow:

.gwin.claude-working { animation: claude-glow 2.8s ease-in-out infinite; } @keyframes claude-glow { 50% { box-shadow: 0 0 0 1px rgba(159,184,255,.3), 0 0 40px rgba(124,156,255,.26); } } 

Pretty, harmless, right? Not with glass around it.

A smooth infinite animation means the compositor has to produce a new frame 60 times a second. And every new frame recomputes the big backdrop-filter blurs of every glass panel on screen. My rain shader was carefully throttled to 15–30 FPS. One CSS animation quietly overrode that and pushed everything to 60.

Two things surprised me:

  • Size doesn't matter. An 8-pixel equalizer icon in the dock cost the same as the big window glow. Any animated pixel means a new frame, and the blur gets recomputed.
  • transform instead of box-shadow didn't help either. The usual "animate only transform and opacity" advice is about layout and paint cost. Here the expensive part was the frame itself.

The fix is one word:

.gwin.claude-working { animation: claude-glow 2.8s steps(5) infinite; } 

steps(5) makes the glow breathe in five visible stages instead of a smooth curve. Between the steps nothing changes, so no new frames are produced. At normal viewing distance it still reads as "breathing". I moved every long-running infinite animation to steps(): the waiting dot (which can sit there for hours), the equalizer, a live indicator. Short loading spinners stay smooth, because they disappear after a second.

Suspect 2: the rain that wasn't raining

The WebGL rain has an intensity slider. At 0 % you see a still background. But the shader kept rendering 15–30 frames a second of zero raindrops, because the "paused" flag was never passed down.

Now the frame scheduler returns "no frames" at 0 %:

export function frameInterval(s: FrameState): number | null { if (s.paused || (s.rain !== undefined && s.rain <= 0)) return null if (s.idle) return 1000 / FPS_IDLE return 1000 / (s.saver ? FPS_SAVER : FPS_FOCUS) } 

Suspect 3: every line of terminal output

This one I can't fully fix. A terminal that's streaming output changes pixels all the time, and every change under a glass panel recomputes its blur. For battery mode the glass now turns into plain, nearly opaque panels:

.app.saver .gwin, .app.saver .dock { backdrop-filter: none; background: rgba(13,17,25,.9); } 

After

Step GPU process Renderer
1.9 (smooth glow) 131 % 30 %
glow on steps() ~26 % ~14 %
rain off really means off ~20 % ~12 %
battery saver without live blur ~11 % ~11 %

From 131 % to about 11 %, mostly by deleting frames nobody needed.

How I measured (and two traps)

  • I started a second, isolated copy with Playwright's _electron, a copy of my real settings and its own user-data folder, so the measurement didn't touch my working setup.
  • Trap 1: app.getAppMetrics() reports CPU divided by the number of cores. On 8 cores, "16 %" is actually more than a full core. I switched to measuring the difference in CPU seconds per process from the OS.
  • Trap 2: a hidden or covered Electron window stops requestAnimationFrame. My first measurement showed almost 0 % GPU because the test window was behind another one. Bring it to the front (setAlwaysOnTop, focus) before you measure.

What this doesn't prove

It's one app on one laptop with integrated graphics. On a desktop GPU you'd likely never notice the glow. But if your Electron or web app combines backdrop-filter with any infinite animation, it's worth five minutes with the profiler.

Question

Have you ever chased a "leak" that turned out to be something else? What was it?

A) thermals / throttling

B) a runaway animation or render loop

C) a cache doing its job

D) an actual leak, finally

(The app is VibeDesk, open source, so all of the CSS above is in the repo.)

原文:https://www.a8q9.cn/en/article/i-was-sure-it-was-a-memory-leak-it-was-a-glowing-border-using-131-of-a-cpu-core-59254f.html

相关文章