perf: hero bỏ canvas three.js + còn 1 shader, gate theo tầm nhìn (giảm 3 ngữ cảnh WebGL)

This commit is contained in:
tuyenio
2026-06-25 08:47:36 +07:00
parent cea9efab99
commit e0117ab4bd
2 changed files with 25 additions and 143 deletions
@@ -1,111 +0,0 @@
"use client";
import { useEffect, useRef } from "react";
import * as THREE from "three";
interface HeroPanoramaBackgroundProps {
imageSrc: string;
}
export function HeroPanoramaBackground({ imageSrc }: HeroPanoramaBackgroundProps) {
const mountRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const mount = mountRef.current;
if (!mount) {
return;
}
const renderer = new THREE.WebGLRenderer({
alpha: true,
antialias: true,
powerPreference: "high-performance",
});
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.6));
renderer.setSize(mount.clientWidth, mount.clientHeight);
renderer.domElement.className = "h-full w-full";
mount.appendChild(renderer.domElement);
const camera = new THREE.PerspectiveCamera(
58,
mount.clientWidth / Math.max(mount.clientHeight, 1),
0.1,
1000,
);
camera.position.set(0, 0, 0.1);
const scene = new THREE.Scene();
const geometry = new THREE.SphereGeometry(500, 96, 64);
geometry.scale(-1, 1, 1);
const material = new THREE.MeshBasicMaterial({
color: 0xffffff,
transparent: true,
opacity: 0,
});
const sphere = new THREE.Mesh(geometry, material);
sphere.rotation.y = -0.28;
scene.add(sphere);
let frame = 0;
let disposed = false;
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const loader = new THREE.TextureLoader();
loader.load(
imageSrc,
(texture) => {
if (disposed) {
texture.dispose();
return;
}
texture.colorSpace = THREE.SRGBColorSpace;
texture.minFilter = THREE.LinearFilter;
material.map = texture;
material.opacity = 1;
material.needsUpdate = true;
},
undefined,
() => {
material.opacity = 0;
},
);
const resize = () => {
const width = mount.clientWidth;
const height = Math.max(mount.clientHeight, 1);
camera.aspect = width / height;
camera.updateProjectionMatrix();
renderer.setSize(width, height);
};
const animate = () => {
if (!reduceMotion) {
sphere.rotation.y += 0.00075;
}
renderer.render(scene, camera);
frame = requestAnimationFrame(animate);
};
const resizeObserver = new ResizeObserver(resize);
resizeObserver.observe(mount);
animate();
return () => {
disposed = true;
cancelAnimationFrame(frame);
resizeObserver.disconnect();
geometry.dispose();
material.map?.dispose();
material.dispose();
renderer.dispose();
renderer.domElement.remove();
};
}, [imageSrc]);
return <div ref={mountRef} aria-hidden className="absolute inset-0" />;
}
@@ -2,7 +2,8 @@
import { MeshGradient } from "@paper-design/shaders-react";
import type React from "react";
import { HeroPanoramaBackground } from "@/app/components/ui/hero-panorama-background";
import { useEffect, useRef, useState } from "react";
import { createRenderGate } from "@/app/lib/render-gate";
interface HeroShaderBackgroundProps {
children: React.ReactNode;
@@ -10,45 +11,37 @@ interface HeroShaderBackgroundProps {
}
export function HeroShaderBackground({ children, imageSrc }: HeroShaderBackgroundProps) {
return (
<div className="relative isolate overflow-hidden border-b border-[var(--surface-border)]">
<svg aria-hidden className="absolute inset-0 h-0 w-0">
<defs>
<filter id="heritage-glass-effect" x="-50%" y="-50%" width="200%" height="200%">
<feTurbulence baseFrequency="0.005" numOctaves="1" result="noise" />
<feDisplacementMap in="SourceGraphic" in2="noise" scale="0.3" />
<feColorMatrix
type="matrix"
values="1 0 0 0 0.03
0 1 0 0 0.04
0 0 1 0 0.02
0 0 0 0.88 0"
result="tint"
/>
</filter>
</defs>
</svg>
const rootRef = useRef<HTMLDivElement>(null);
const [shaderActive, setShaderActive] = useState(false);
useEffect(() => {
const el = rootRef.current;
if (!el) return;
return createRenderGate(el, setShaderActive);
}, []);
return (
<div
ref={rootRef}
className="relative isolate overflow-hidden border-b border-[var(--surface-border)]"
>
<div
aria-hidden
className="absolute inset-0 bg-cover bg-center opacity-[0.58] saturate-[1.12] contrast-[1.04]"
style={{ backgroundImage: `url("${imageSrc}")` }}
/>
<HeroPanoramaBackground imageSrc={imageSrc} />
<div aria-hidden className="absolute inset-0 bg-[rgb(243_247_240_/_0.12)]" />
<div aria-hidden className="absolute inset-0 overflow-hidden opacity-48 mix-blend-soft-light">
<MeshGradient
className="absolute inset-0 h-full w-full"
colors={["#f8fbf2", "#d6b468", "#e0eddf", "#245f4b", "#ffffff"]}
speed={0.28}
/>
<MeshGradient
className="absolute inset-0 h-full w-full opacity-45"
colors={["#ffffff", "#e7cb81", "#dcebdd", "#0e3427"]}
speed={0.2}
/>
</div>
{/* Một lớp shader duy nhất, chỉ vẽ khi hero trong tầm nhìn + tab hiển thị */}
{shaderActive ? (
<div aria-hidden className="absolute inset-0 overflow-hidden opacity-40">
<MeshGradient
className="absolute inset-0 h-full w-full"
colors={["#f8fbf2", "#d6b468", "#e0eddf", "#245f4b", "#ffffff"]}
speed={0.24}
/>
</div>
) : null}
<div
aria-hidden