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

@@ -82,8 +82,9 @@ rozet, buton varyantı tasarlanmaz.
- **Tamamlanınca:** `tercihProfiliYaz` → çekmece kapanır → `sonucHref` ile - **Tamamlanınca:** `tercihProfiliYaz` → çekmece kapanır → `sonucHref` ile
`/sonuc` (girişliye `sihirbaz: "1"`, girişsize `hazir: "1"`). Giriş duvarı `/sonuc` (girişliye `sihirbaz: "1"`, girişsize `hazir: "1"`). Giriş duvarı
sihirbazın hemen arkasında değildir; değer önce gösterilir. sihirbazın hemen arkasında değildir; değer önce gösterilir.
- **Lenis:** çekmece içeriği ve kaydırma kabı `data-lenis-prevent` taşır; - **Kaydırma:** sayfa yerel tarayıcı kaydırmasını kullanır (Lenis/skew
sayfadaki smooth-scroll çekmece içi kaydırmayı ezmez. performans nedeniyle 21.09.2026'da kaldırıldı); çekmece içi kaydırma için
ek bir önlem gerekmez.
## 6. Erişilebilirlik ## 6. Erişilebilirlik

View File

@@ -26,7 +26,6 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"drizzle-orm": "^0.45.2", "drizzle-orm": "^0.45.2",
"iyzipay": "^2.0.69", "iyzipay": "^2.0.69",
"lenis": "^1.3.25",
"lucide-react": "^1.25.0", "lucide-react": "^1.25.0",
"marked": "^18.0.7", "marked": "^18.0.7",
"mermaid": "^11.16.0", "mermaid": "^11.16.0",

21
pnpm-lock.yaml generated
View File

@@ -35,9 +35,6 @@ importers:
iyzipay: iyzipay:
specifier: ^2.0.69 specifier: ^2.0.69
version: 2.0.69 version: 2.0.69
lenis:
specifier: ^1.3.25
version: 1.3.25(react@19.2.4)
lucide-react: lucide-react:
specifier: ^1.25.0 specifier: ^1.25.0
version: 1.25.0(react@19.2.4) version: 1.25.0(react@19.2.4)
@@ -4195,20 +4192,6 @@ packages:
layout-base@2.0.1: layout-base@2.0.1:
resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==}
lenis@1.3.25:
resolution: {integrity: sha512-mOKxayErlaONK8fm4LN3XNd99Qu4plTpn9h9qf8wxzjGrJDzuD84FYzZ81HCd6ZsWp++VWVwOzL286Pf2s2u4A==}
peerDependencies:
'@nuxt/kit': '>=3.0.0'
react: '>=17.0.0'
vue: '>=3.0.0'
peerDependenciesMeta:
'@nuxt/kit':
optional: true
react:
optional: true
vue:
optional: true
levn@0.4.1: levn@0.4.1:
resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@@ -9688,10 +9671,6 @@ snapshots:
layout-base@2.0.1: {} layout-base@2.0.1: {}
lenis@1.3.25(react@19.2.4):
optionalDependencies:
react: 19.2.4
levn@0.4.1: levn@0.4.1:
dependencies: dependencies:
prelude-ls: 1.2.1 prelude-ls: 1.2.1

View File

@@ -16,7 +16,7 @@ export default function GirisPage({
searchParams: Promise<{ callback?: string; hata?: string }>; searchParams: Promise<{ callback?: string; hata?: string }>;
}) { }) {
return ( return (
// -mb-28: layout'taki ScrollFlow sarmalayıcısının pb-28'ini nötrler — // -mb-28: layout'taki içerik sarmalayıcısının pb-28'ini nötrler —
// kart tam dvh içinde ortalanır, sayfa scroll etmez (sığmazsa scroll açılır). // kart tam dvh içinde ortalanır, sayfa scroll etmez (sığmazsa scroll açılır).
<div className="-mb-28 flex min-h-dvh flex-col bg-slate-50 text-slate-900"> <div className="-mb-28 flex min-h-dvh flex-col bg-slate-50 text-slate-900">
<main className="mx-auto flex w-full max-w-md flex-1 flex-col items-stretch justify-center px-6 py-8"> <main className="mx-auto flex w-full max-w-md flex-1 flex-col items-stretch justify-center px-6 py-8">

View File

@@ -252,7 +252,5 @@
} }
html { html {
@apply font-sans; @apply font-sans;
/* scroll-behavior: smooth KULLANMA — Lenis (ScrollFlow, anchors: true)
kaydırmayı zaten yumuşatıyor; ikisi birlikte çift yumuşatma yapar. */
} }
} }

View File

@@ -11,7 +11,6 @@ import { TercihProfiliKapisiLazy } from "@/features/liste/components/tercih-prof
import { HeaderGate } from "@/components/header-gate"; import { HeaderGate } from "@/components/header-gate";
import { ProgressiveBlur } from "@/components/ui/skiper-ui/skiper41"; import { ProgressiveBlur } from "@/components/ui/skiper-ui/skiper41";
import { DevPanelMount } from "@/components/dev/dev-panel-mount"; import { DevPanelMount } from "@/components/dev/dev-panel-mount";
import { ScrollFlow } from "@/components/scroll-flow";
import { JsonLd, organizationJsonLd, webSiteJsonLd, SITE_URL } from "@/lib/seo"; import { JsonLd, organizationJsonLd, webSiteJsonLd, SITE_URL } from "@/lib/seo";
import "./globals.css"; import "./globals.css";
@@ -79,10 +78,8 @@ export default function RootLayout({
return ( return (
<html <html
lang="tr" lang="tr"
// `h-full` VERME — Lenis içerik yüksekliğini <html>'e bağlı ResizeObserver // `h-full` verme: html auto yükseklikte içerikle büyür, viewport tabanı
// ile izliyor; html sabit %100 kalınca lazy yüklenen içerik sonrası limit // body'deki min-h-dvh'den gelir.
// bayatlıyor ve sayfa sonunda scroll kilitleniyor. html auto yükseklikte
// içerikle büyümeli, viewport tabanı body'deki min-h-dvh'den gelir.
className={`${calSans.variable} ${clarityCity.variable} ${geistMono.variable} ${bricolage.variable} antialiased`} className={`${calSans.variable} ${clarityCity.variable} ${geistMono.variable} ${bricolage.variable} antialiased`}
> >
<body className="min-h-dvh flex flex-col bg-slate-50"> <body className="min-h-dvh flex flex-col bg-slate-50">
@@ -107,9 +104,9 @@ export default function RootLayout({
<SiteTopBanner /> <SiteTopBanner />
<SiteHeader /> <SiteHeader />
</HeaderGate> </HeaderGate>
<ScrollFlow className="flex min-h-0 flex-1 flex-col pb-28 print:pb-0"> <div className="flex min-h-0 flex-1 flex-col pb-28 print:pb-0">
{children} {children}
</ScrollFlow> </div>
{/* Manuel 24'lük liste çekmecesi + "+" uçuş katmanı (navbar'dan açılır) */} {/* Manuel 24'lük liste çekmecesi + "+" uçuş katmanı (navbar'dan açılır) */}
<ListeCekmecesiLazy /> <ListeCekmecesiLazy />
{/* Girişsiz + 3 seçimli kullanıcıya "listen yalnızca bu tarayıcıda" banner'ı */} {/* Girişsiz + 3 seçimli kullanıcıya "listen yalnızca bu tarayıcıda" banner'ı */}

View File

@@ -17,7 +17,7 @@ export default function OdemePage({
searchParams: Promise<{ siparis?: string }>; searchParams: Promise<{ siparis?: string }>;
}) { }) {
return ( return (
// -mb-28: layout'taki ScrollFlow sarmalayıcısının pb-28'ini nötrler — o // -mb-28: layout'taki içerik sarmalayıcısının pb-28'ini nötrler — o
// padding gizlenen alt blur katmanı içindi, checkout'ta krom kapalı. // padding gizlenen alt blur katmanı içindi, checkout'ta krom kapalı.
<div className="-mb-28 flex min-h-dvh flex-col bg-slate-50 text-slate-900"> <div className="-mb-28 flex min-h-dvh flex-col bg-slate-50 text-slate-900">
<OdemeUstSerit /> <OdemeUstSerit />

View File

@@ -178,7 +178,6 @@ export function DevPanel() {
</div> </div>
<div <div
data-lenis-prevent
className="flex flex-col gap-4 overflow-y-auto p-4 text-sm" className="flex flex-col gap-4 overflow-y-auto p-4 text-sm"
> >
{/* Oturum */} {/* 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 ( return (
<StickToBottom <StickToBottom
className={cn("flex overflow-y-auto", className)} className={cn("flex overflow-y-auto", className)}
data-lenis-prevent
resize="smooth" resize="smooth"
initial="instant" initial="instant"
role="log" role="log"

View File

@@ -60,7 +60,6 @@ function DialogContent({
<DialogOverlay /> <DialogOverlay />
<DialogPrimitive.Content <DialogPrimitive.Content
data-slot="dialog-content" data-slot="dialog-content"
data-lenis-prevent
className={cn( 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", "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 className

View File

@@ -68,7 +68,6 @@ function SelectContent({
<SelectPrimitive.Portal> <SelectPrimitive.Portal>
<SelectPrimitive.Content <SelectPrimitive.Content
data-slot="select-content" data-slot="select-content"
data-lenis-prevent
data-align-trigger={position === "item-aligned"} 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 )} 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} position={position}

View File

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

View File

@@ -115,7 +115,6 @@ export function ListeCekmecesi() {
onAnimationEnd={(open) => setOturdu(open)} onAnimationEnd={(open) => setOturdu(open)}
> >
<DrawerContent <DrawerContent
data-lenis-prevent
className="min-h-0 max-h-[92dvh] bg-white sm:mx-auto sm:max-w-2xl sm:border-x sm:border-slate-200 sm:shadow-2xl" className="min-h-0 max-h-[92dvh] bg-white sm:mx-auto sm:max-w-2xl sm:border-x sm:border-slate-200 sm:shadow-2xl"
> >
<DrawerHeader className="border-b border-slate-100 text-left md:text-left"> <DrawerHeader className="border-b border-slate-100 text-left md:text-left">
@@ -182,7 +181,6 @@ export function ListeCekmecesi() {
</DrawerHeader> </DrawerHeader>
<div <div
data-lenis-prevent
className="min-h-0 flex-1 overflow-x-hidden overflow-y-auto px-4 py-4" className="min-h-0 flex-1 overflow-x-hidden overflow-y-auto px-4 py-4"
> >
{liste.length === 0 ? ( {liste.length === 0 ? (

View File

@@ -190,7 +190,6 @@ export function CtaSiraForm({ baslik }: { baslik?: string }) {
<Drawer open={acik} onOpenChange={acikDegisti} handleOnly> <Drawer open={acik} onOpenChange={acikDegisti} handleOnly>
<DrawerContent <DrawerContent
data-lenis-prevent
className="max-h-[92dvh] bg-white sm:mx-auto sm:max-w-2xl sm:border-x sm:border-slate-200 sm:shadow-2xl" className="max-h-[92dvh] bg-white sm:mx-auto sm:max-w-2xl sm:border-x sm:border-slate-200 sm:shadow-2xl"
> >
{/* Görünür başlıklar adıma göre değişir; erişilebilir ad sabit kalır. */} {/* Görünür başlıklar adıma göre değişir; erişilebilir ad sabit kalır. */}
@@ -207,7 +206,6 @@ export function CtaSiraForm({ baslik }: { baslik?: string }) {
</DrawerClose> </DrawerClose>
<div <div
data-lenis-prevent
className="min-h-0 flex-1 overflow-y-auto px-4 pt-5 pb-[max(1.25rem,env(safe-area-inset-bottom))] sm:px-6" className="min-h-0 flex-1 overflow-y-auto px-4 pt-5 pb-[max(1.25rem,env(safe-area-inset-bottom))] sm:px-6"
> >
{adim === "sira" ? ( {adim === "sira" ? (