Skip to content

Large performence improvements to Plasma background#1007

Open
elias4044 wants to merge 4 commits into
DavidHDev:mainfrom
elias4044:fix/plasma-performance
Open

Large performence improvements to Plasma background#1007
elias4044 wants to merge 4 commits into
DavidHDev:mainfrom
elias4044:fix/plasma-performance

Conversation

@elias4044

Copy link
Copy Markdown

Summary

Plasma runs a per-pixel raymarching shader with a 60-iteration loop, at full resolution and full device pixel ratio, every frame. That's expensive, on a 1440p+ display at DPR 2 it's shading roughly 15 million pixels per frame, most of which is invisible given how soft the final output is. Along side this I noticed that this background just doesn't run well on lower-end machines in general. This PR adds four opt-in props that let devs trade a bit of that invisible fidelity for a real drop in GPU cost, without changing much for people already using the component.

Changes

Added to Plasma across all four variants (JS/TS × CSS/Tailwind):

  • renderScale (default 0.55) — renders the WebGL buffer at a fraction of the container size and lets CSS scale it back up. This is where most of the win comes from, since cost tracks pixel count almost linearly.
  • maxDpr (default 1.5) — caps the devicePixelRatio used for rendering. A blurred background rarely needs full DPR on high-density screens.
  • targetFps (default 60) — throttles the render loop to a target rate instead of letting requestAnimationFrame run flat out.
  • quality (default 60) — controls the raymarch iteration count. Unlike the three props above, this one changes the actual pattern rather than just how much of it gets rendered or how often — see note below.
  • Tab visibility handling — pauses the loop on document.visibilitychange, on top of the existing IntersectionObserver pause for off-screen instances. Right now the component keeps rendering in a backgrounded tab.

A few smaller things I found along the way:

  • Mouse position now gets read into the GL uniform once per rendered frame instead of on every mousemove event.
  • Resize events are batched through requestAnimationFrame rather than calling setSize synchronously on every ResizeObserver firing.
  • The component now respects prefers-reduced-motion — it paints one static frame and stops instead of starting the loop.

A note on quality

renderScale, maxDpr, and targetFps are all "free" in the sense that they only change how much gets rendered or how often, the underlying pattern at any given moment is identical either way. quality isn't free in that sense.

The loop accumulates a running distance (z) that feeds into the next iteration's position, which is standard raymarching — each step is figuring out how far a ray has traveled based on every step before it. Cut the iteration count from 60 to 45 and the ray simply hasn't traveled as far, so it's landing on a different point of the pattern, not a lower-detail version of the same point. In testing, lower values noticeably changed how much of the bright/white area shows up in the render — not just softness or sharpness, but the actual balance of the pattern.

Because of that, I'd treat quality as a distinct trade-off from the other three rather than something to casually lower alongside them.

Testing

Tested with renderScale={0.55}, maxDpr={1.5}, targetFps={60}.

Before

Before

After

After

Visually near-identical at normal viewing distance, roughly 2x improvement. My GPU usage went from 95% down to 45%. My laptop, which was visibly lagging and very noticeably slow on the older version, handles the new version with no problem and there is zero noticeable lag. Try it out yourself!

❤️ from Elias.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant