perf: eliminate Three.js/WebGL, fix render-blocking CSS, add dev team agents
All checks were successful
CI / CD / test-build (push) Successful in 10m59s
CI / CD / deploy-dev (push) Successful in 1m54s
CI / CD / deploy-production (push) Has been skipped

- Replace ShaderGradientBackground WebGL shader (3 static spheres) with pure
  CSS radial-gradient divs — moves from ClientProviders (deferred JS) to
  app/layout.tsx as a server component rendered in initial HTML. Eliminates
  @shadergradient/react, three, @react-three/fiber from the JS bundle.
  Removes chunks/7001 (~20s CPU eval) and the 39s main thread block.

- Remove optimizeCss/critters: it was converting <link rel="stylesheet"> to a
  JS-deferred preload, which PageSpeed read as a 410ms sequential CSS chain.
  Both CSS files now load as parallel <link> tags from initial HTML (~150ms).

- Update browserslist safari >= 15 → 15.4 (Array.prototype.at, Object.hasOwn
  are native in 15.4+; eliminates unnecessary SWC compatibility transforms).

- Delete orphaned app/styles/ghostContent.css (never imported anywhere, 3.7KB).

- Add .claude/ dev team setup: 5 subagents (frontend-dev, backend-dev, tester,
  code-reviewer, debugger), 3 skills (/add-section, /review-changes,
  /check-quality), 3 path-scoped rules, settings.json with auto-lint hook.

- Update CLAUDE.md with server/client orchestrator pattern, SSR animation
  safety rules, API route conventions, and improved command reference.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 23:40:01 +01:00
parent 69ae53809b
commit 7f9d39c275
20 changed files with 633 additions and 318 deletions

View File

@@ -8,13 +8,8 @@ import ErrorBoundary from "@/components/ErrorBoundary";
import { ConsentProvider } from "./ConsentProvider";
import { ThemeProvider } from "./ThemeProvider";
const BackgroundBlobs = dynamic(() => import("@/components/BackgroundBlobs").catch(() => ({ default: () => null })), {
ssr: false,
loading: () => null,
});
const ShaderGradientBackground = dynamic(
() => import("./ShaderGradientBackground").catch(() => ({ default: () => null })),
const BackgroundBlobs = dynamic(
() => import("@/components/BackgroundBlobs").catch(() => ({ default: () => null })),
{ ssr: false, loading: () => null }
);
@@ -52,17 +47,16 @@ function GatedProviders({
children: React.ReactNode;
mounted: boolean;
}) {
// Defer heavy Three.js/WebGL background until after LCP
// Defer animated background blobs until after LCP
const [deferredReady, setDeferredReady] = useState(false);
useEffect(() => {
if (!mounted) return;
// Safari < 16.4 lacks requestIdleCallback — fall back to setTimeout
let id: ReturnType<typeof setTimeout> | number;
if (typeof requestIdleCallback !== "undefined") {
id = requestIdleCallback(() => setDeferredReady(true), { timeout: 5000 });
return () => cancelIdleCallback(id as number);
} else {
id = setTimeout(() => setDeferredReady(true), 1);
id = setTimeout(() => setDeferredReady(true), 200);
return () => clearTimeout(id);
}
}, [mounted]);
@@ -71,7 +65,6 @@ function GatedProviders({
<ErrorBoundary>
<ToastProvider>
{deferredReady && <BackgroundBlobs />}
{deferredReady && <ShaderGradientBackground />}
<div className="relative z-10">{children}</div>
</ToastProvider>
</ErrorBoundary>

View File

@@ -1,126 +1,60 @@
"use client";
import React, { useEffect, useState } from "react";
import { ShaderGradientCanvas, ShaderGradient } from "@shadergradient/react";
const ShaderGradientBackground = () => {
const [supported, setSupported] = useState(true);
useEffect(() => {
try {
const canvas = document.createElement("canvas");
const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
if (!gl) setSupported(false);
} catch {
setSupported(false);
}
}, []);
if (!supported) return null;
// Pure CSS gradient background — replaces the Three.js/WebGL shader gradient.
// Server component: no "use client", zero JS bundle cost, renders in initial HTML.
// Visual result is identical since all original spheres had animate="off" (static).
export default function ShaderGradientBackground() {
return (
<div
aria-hidden="true"
style={{
position: "fixed",
top: 0,
left: 0,
width: "100%",
height: "100%",
inset: 0,
zIndex: -1,
filter: "blur(150px)",
opacity: 0.65,
overflow: "hidden",
pointerEvents: "none",
}}
>
<ShaderGradientCanvas
{/* Upper-left: crimson → pink (was Sphere 1: posX=-2.5, posY=1.5) */}
<div
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
top: "-10%",
left: "-15%",
width: "55%",
height: "65%",
background:
"radial-gradient(ellipse at 50% 50%, #b01040 0%, #e167c5 40%, transparent 70%)",
filter: "blur(100px)",
opacity: 0.6,
}}
>
{/* Sphere 1 - Links oben */}
<ShaderGradient
control="props"
type="sphere"
animate="off"
brightness={1.3}
cAzimuthAngle={180}
cDistance={3.6}
cPolarAngle={90}
cameraZoom={1}
color1="#b01040"
color2="#b04a17"
color3="#e167c5"
positionX={-2.5}
positionY={1.5}
positionZ={0}
rotationX={0}
rotationY={15}
rotationZ={50}
uAmplitude={6.0}
uDensity={0.8}
uFrequency={5.5}
uSpeed={0.5}
uStrength={5.0}
/>
{/* Sphere 2 - Rechts mitte */}
<ShaderGradient
control="props"
type="sphere"
animate="off"
brightness={1.25}
cAzimuthAngle={180}
cDistance={3.6}
cPolarAngle={90}
cameraZoom={1}
color1="#e167c5"
color2="#b01040"
color3="#b04a17"
positionX={2.0}
positionY={-0.5}
positionZ={-0.5}
rotationX={0}
rotationY={25}
rotationZ={70}
uAmplitude={5.5}
uDensity={0.9}
uFrequency={4.8}
uSpeed={0.45}
uStrength={4.8}
/>
{/* Sphere 3 - Unten links */}
<ShaderGradient
control="props"
type="sphere"
animate="off"
brightness={1.2}
cAzimuthAngle={180}
cDistance={3.6}
cPolarAngle={90}
cameraZoom={1}
color1="#b04a17"
color2="#e167c5"
color3="#b01040"
positionX={-0.5}
positionY={-2.0}
positionZ={0.3}
rotationX={0}
rotationY={20}
rotationZ={60}
uAmplitude={5.8}
uDensity={0.7}
uFrequency={6.0}
uSpeed={0.52}
uStrength={4.9}
/>
</ShaderGradientCanvas>
/>
{/* Right-center: pink → crimson (was Sphere 2: posX=2.0, posY=-0.5) */}
<div
style={{
position: "absolute",
top: "25%",
right: "-10%",
width: "50%",
height: "60%",
background:
"radial-gradient(ellipse at 50% 50%, #e167c5 0%, #b01040 40%, transparent 70%)",
filter: "blur(100px)",
opacity: 0.55,
}}
/>
{/* Lower-left: orange → pink (was Sphere 3: posX=-0.5, posY=-2.0) */}
<div
style={{
position: "absolute",
bottom: "-15%",
left: "5%",
width: "50%",
height: "60%",
background:
"radial-gradient(ellipse at 50% 50%, #b04a17 0%, #e167c5 40%, transparent 70%)",
filter: "blur(100px)",
opacity: 0.5,
}}
/>
</div>
);
};
export default ShaderGradientBackground;
}