Files
portfolio/app/components/ClientProviders.tsx
denshooter 7f9d39c275
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
perf: eliminate Three.js/WebGL, fix render-blocking CSS, add dev team agents
- 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>
2026-03-05 23:40:01 +01:00

73 lines
1.9 KiB
TypeScript

"use client";
import React, { useEffect, useState } from "react";
import { usePathname } from "next/navigation";
import dynamic from "next/dynamic";
import { ToastProvider } from "@/components/Toast";
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 }
);
export default function ClientProviders({
children,
}: {
children: React.ReactNode;
}) {
const [mounted, setMounted] = useState(false);
const pathname = usePathname();
useEffect(() => {
setMounted(true);
}, [pathname]);
return (
<ErrorBoundary>
<ErrorBoundary>
<ConsentProvider>
<ThemeProvider attribute="class" defaultTheme="light" enableSystem={false}>
<GatedProviders mounted={mounted}>
{children}
</GatedProviders>
</ThemeProvider>
</ConsentProvider>
</ErrorBoundary>
</ErrorBoundary>
);
}
function GatedProviders({
children,
mounted,
}: {
children: React.ReactNode;
mounted: boolean;
}) {
// Defer animated background blobs until after LCP
const [deferredReady, setDeferredReady] = useState(false);
useEffect(() => {
if (!mounted) return;
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), 200);
return () => clearTimeout(id);
}
}, [mounted]);
return (
<ErrorBoundary>
<ToastProvider>
{deferredReady && <BackgroundBlobs />}
<div className="relative z-10">{children}</div>
</ToastProvider>
</ErrorBoundary>
);
}