You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
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!
Would it help to transfer the control of the <canvas> to a Web Worker through canvas.transferControlToOffscreen()? It'll move all of the render loop, frame throttling and uniform updates off of the main thread.
Also, you wrote how cutting the loop iterations from 60 to 45 alters the ray distance (z) and changes the visual output. I belive this can be fixed without changing the visuals too much.
Instead of fixed steps, you can scale the step size inversely with iteration count.
Fragment precision can be set to precision mediump floatl instead of highp where high trignometric accuracy isn't important. You can also break out of the raymarch loop early if the ray hits an opaque threshold or exits the bounding volume.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Plasmaacross all four variants (JS/TS × CSS/Tailwind):renderScale(default0.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(default1.5) — caps the devicePixelRatio used for rendering. A blurred background rarely needs full DPR on high-density screens.targetFps(default60) — throttles the render loop to a target rate instead of lettingrequestAnimationFramerun flat out.quality(default60) — 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.document.visibilitychange, on top of the existingIntersectionObserverpause for off-screen instances. Right now the component keeps rendering in a backgrounded tab.A few smaller things I found along the way:
mousemoveevent.requestAnimationFramerather than callingsetSizesynchronously on everyResizeObserverfiring.prefers-reduced-motion— it paints one static frame and stops instead of starting the loop.A note on
qualityrenderScale,maxDpr, andtargetFpsare 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.qualityisn'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
qualityas 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
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.