perf: Lenis smooth scroll ve scroll skew efektini kaldır

Site geneli ScrollFlow sarmalayıcısı (Lenis lerp'li kaydırma + hıza bağlı
skewY) tüm sayfa katmanını her karede transform'luyor, kaydırma performansını
düşürüyordu. Yerel tarayıcı kaydırmasına dönüldü; bölüm çapaları mevcut
scroll-mt değerleriyle çalışmaya devam eder. lenis bağımlılığı ve
data-lenis-prevent işaretleri temizlendi.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-09-21 15:31:11 +03:00
parent 294fb2cf6a
commit d79758adc0
15 changed files with 12 additions and 123 deletions

View File

@@ -178,7 +178,6 @@ export function DevPanel() {
</div>
<div
data-lenis-prevent
className="flex flex-col gap-4 overflow-y-auto p-4 text-sm"
>
{/* Oturum */}

View File

@@ -1,77 +0,0 @@
"use client";
import { type ReactNode } from "react";
import { ReactLenis } from "lenis/react";
import {
motion,
useReducedMotion,
useScroll,
useSpring,
useTransform,
useVelocity,
} from "motion/react";
/**
* Vitrin sayfalarının akışkan scroll efekti: Lenis ile lerp'li (yavaşlatılmış,
* momentumlu) kaydırma + scroll hızına bağlı içerik bükülmesi (skew).
* Hızlı kaydırdıkça sayfa hafifçe yana yatar, durunca yayla yerine oturur.
*
* Sohbet ve liste gibi iç scroll alanı olan uygulama sayfalarında KULLANMA —
* Lenis tekerlek olayını sayfaya yönlendirdiği için iç scroll'u bozar,
* transform da `fixed` konumlu çocukları koparır.
*
* Hareket azaltılmışsa hem yumuşatma hem bükülme tamamen kapalıdır.
*/
export function ScrollFlow({
children,
className,
maxSkew = 3,
}: {
children: ReactNode;
className?: string;
/** Derece cinsinden en fazla bükülme açısı. */
maxSkew?: number;
}) {
const reduceMotion = useReducedMotion();
const { scrollY } = useScroll();
const velocity = useVelocity(scrollY);
// Önce hızı kesintisiz bir hedef açıya dönüştür, sonra doğrudan görünen
// açıyı yumuşat. Böylece eşik/clamp kaynaklı ani hız değişimleri yaya girmez.
const targetSkew = useTransform(velocity, (v) => {
const RESPONSE = 1800;
return Math.tanh(v / RESPONSE) * maxSkew;
});
const smoothSkew = useSpring(targetSkew, {
stiffness: 240,
damping: 26,
mass: 0.7,
restDelta: 0.005,
restSpeed: 0.01,
});
// Yuvarlama görsel basamaklanma üretir. Tam hassasiyetle çiz; yalnızca açı
// algılanamayacak kadar küçüldüğünde transform'u kaldırarak dev sayfa
// katmanının GPU belleğinde boşuna tutulmasını önle.
const transform = useTransform(smoothSkew, (v) => {
if (Math.abs(v) < 0.01) return "none";
return `skewY(${v}deg)`;
});
// Hareket azaltılmışsa da AYNI ağaç render edilir (server/client farklı
// ağaç hydration uyumsuzluğu yaratıyordu); efektler koşullu kapatılır:
// Lenis tekerlek yumuşatması kapalı, skew transform'u hiç bağlanmaz.
return (
<ReactLenis
root
options={{ lerp: 0.09, anchors: true, smoothWheel: !reduceMotion }}
>
<motion.div
className={className}
style={reduceMotion ? undefined : { transform }}
>
{children}
</motion.div>
</ReactLenis>
);
}

View File

@@ -26,7 +26,6 @@ function ChatContainerRoot({
return (
<StickToBottom
className={cn("flex overflow-y-auto", className)}
data-lenis-prevent
resize="smooth"
initial="instant"
role="log"

View File

@@ -60,7 +60,6 @@ function DialogContent({
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
data-lenis-prevent
className={cn(
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
className

View File

@@ -68,7 +68,6 @@ function SelectContent({
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
data-lenis-prevent
data-align-trigger={position === "item-aligned"}
className={cn("relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", position ==="popper"&&"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className )}
position={position}

View File

@@ -8,9 +8,9 @@ const getClientSnapshot = () => true;
const getServerSnapshot = () => false;
/**
* Viewport'a sabit kalması gereken öğeleri, transform uygulanan sayfa
* katmanının dışına taşır. Böylece üst katmandaki scroll skew animasyonu
* `position: fixed` koordinat sistemini değiştirmez.
* Viewport'a sabit kalması gereken öğeleri sayfa katmanının dışına, doğrudan
* body'ye taşır. Böylece üst katmanlardaki bir transform (ör. Parallax/Reveal)
* `position: fixed` koordinat sistemini değiştiremez.
*/
export function ViewportPortal({ children }: { children: ReactNode }) {
const mounted = useSyncExternalStore(