Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 28 additions & 9 deletions packages/ui/src/components/mesh-gradient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ interface GradientInstance {
};
}

function supportsWebgl(): boolean {
try {
const probe = document.createElement("canvas");
return Boolean(probe.getContext("webgl2") || probe.getContext("webgl"));
} catch {
return false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The supportsWebgl() probe swallows any error from document.createElement or getContext in a silent empty catch with no rationale comment and no observability. If the failure is something other than WebGL being genuinely unavailable (e.g. an allocation/sandboxing error), it becomes indistinguishable from real lack of support and goes completely unlogged. Since the rest of this PR adds an inline rationale comment to the init catch for exactly this reason, consider adding a brief inline comment here explaining that the probe is a best-effort feature check (so an unsupported environment simply keeps the static background), keeping the fix consistent with the swallow-with-rationale convention used below.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/ui/src/components/mesh-gradient.tsx, line 34:

<comment>The `supportsWebgl()` probe swallows any error from `document.createElement` or `getContext` in a silent empty catch with no rationale comment and no observability. If the failure is something other than WebGL being genuinely unavailable (e.g. an allocation/sandboxing error), it becomes indistinguishable from real lack of support and goes completely unlogged. Since the rest of this PR adds an inline rationale comment to the init catch for exactly this reason, consider adding a brief inline comment here explaining that the probe is a best-effort feature check (so an unsupported environment simply keeps the static background), keeping the fix consistent with the swallow-with-rationale convention used below.</comment>

<file context>
@@ -26,6 +26,15 @@ interface GradientInstance {
+		const probe = document.createElement("canvas");
+		return Boolean(probe.getContext("webgl2") || probe.getContext("webgl"));
+	} catch {
+		return false;
+	}
+}
</file context>

}
}
Comment on lines +29 to +36

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Repeated mounts or speed updates leak probe contexts until browsers refuse the real gradient context, leaving otherwise-supported gradients on the static fallback. Cache the capability result (or explicitly lose the probe context) so the check does not allocate a new context per effect run.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/ui/src/components/mesh-gradient.tsx, line 29:

<comment>Repeated mounts or `speed` updates leak probe contexts until browsers refuse the real gradient context, leaving otherwise-supported gradients on the static fallback. Cache the capability result (or explicitly lose the probe context) so the check does not allocate a new context per effect run.</comment>

<file context>
@@ -26,6 +26,15 @@ interface GradientInstance {
 	};
 }
 
+function supportsWebgl(): boolean {
+	try {
+		const probe = document.createElement("canvas");
</file context>
Suggested change
function supportsWebgl(): boolean {
try {
const probe = document.createElement("canvas");
return Boolean(probe.getContext("webgl2") || probe.getContext("webgl"));
} catch {
return false;
}
}
let webglSupport: boolean | undefined;
function supportsWebgl(): boolean {
if (webglSupport !== undefined) return webglSupport;
try {
const probe = document.createElement("canvas");
webglSupport = Boolean(
probe.getContext("webgl2") || probe.getContext("webgl"),
);
} catch {
webglSupport = false;
}
return webglSupport;
}


export function MeshGradient({
colors,
className = "",
Expand All @@ -37,18 +46,11 @@ export function MeshGradient({

useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
if (!canvas || !supportsWebgl()) return;

const gradient = new Gradient() as GradientInstance;
gradient.initGradient(`#${canvasId}`);

setTimeout(() => {
if (gradient?.uniforms?.u_global?.value?.noiseSpeed) {
gradient.uniforms.u_global.value.noiseSpeed.value = speed;
}
}, 100);

return () => {
const teardown = () => {
if (gradient.pause) {
gradient.pause();
}
Expand All @@ -65,6 +67,23 @@ export function MeshGradient({
gradient.disconnect();
}
};

// Guard scoped to init only: the library dereferences a null WebGL
// context when creation fails despite the probe above.
try {
gradient.initGradient(`#${canvasId}`);
} catch {
teardown();
return;
}

setTimeout(() => {
if (gradient?.uniforms?.u_global?.value?.noiseSpeed) {
gradient.uniforms.u_global.value.noiseSpeed.value = speed;
}
}, 100);

return teardown;
Comment on lines +80 to +86

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Cancel the delayed noiseSpeed update during teardown.

teardown does not retain the timeout handle. If the effect re-runs because speed changes, or the component unmounts before 100 ms, the callback still runs against the disconnected gradient. This creates stale work and can surface an uncaught post-cleanup error.

Store the handle before defining teardown, and clear it inside teardown.

Proposed fix
 		const gradient = new Gradient() as GradientInstance;
+		let noiseSpeedTimeout: ReturnType<typeof setTimeout> | undefined;

 		const teardown = () => {
+			if (noiseSpeedTimeout !== undefined) {
+				clearTimeout(noiseSpeedTimeout);
+			}
 			if (gradient.pause) {
 				gradient.pause();
 			}
 			// existing cleanup
 		};

-		setTimeout(() => {
+		noiseSpeedTimeout = setTimeout(() => {
 			if (gradient?.uniforms?.u_global?.value?.noiseSpeed) {
 				gradient.uniforms.u_global.value.noiseSpeed.value = speed;
 			}
 		}, 100);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ui/src/components/mesh-gradient.tsx` around lines 80 - 86, Update
the effect around the delayed noiseSpeed update to store the setTimeout handle
before defining teardown, then clear that handle inside teardown before
disconnecting or releasing the gradient. Preserve the existing callback behavior
while ensuring it cannot run after cleanup or a speed-triggered effect rerun.

}, [canvasId, speed]);

return (
Expand Down
Loading