perf: sunucu waterfall'ları ve blocking analitik düzeltildi (Faz 1)

Vercel React Best Practices denetimi, sunucu tarafı:
- odeme: analitik olayı after() ile yanıt sonrasına alındı; ucuz ürün
  doğrulaması oturum turundan öne çekildi; sonuç kartında oturum+sipariş
  paralel okunuyor
- /api/soru POST: gövde+oturum ve 3 bağımsız sorgu Promise.all'a bağlandı
- rehber: prod'da globalThis cache + özetler için frontmatter fast-path
  (24 dosya okuma + marked.parse her render'da tekrarlanıyordu); arama
  kutusuna giden payload RehberAramaKaydi'ya kırpıldı (tarih serileşmiyor)
- /sonuc hattı: ProgramListeSatiri (fakulte/puan2025 client'a serileşmiyor)
- katalog: universite/bolum listelerinde O(n²) filtreler tek geçişe indi
- sira-gerekli + katalog-arama: effect'te setState kalktı (lint yeşil)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-08-08 13:48:27 +03:00
parent d2732828cc
commit 53edb2b307
15 changed files with 256 additions and 99 deletions

View File

@@ -1,13 +1,20 @@
"use client";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
useCallback,
useEffect,
useMemo,
useRef,
useState,
useSyncExternalStore,
} from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { AramaFunnelKarti } from "@/components/arama-funnel-karti";
import { RehberKapak } from "@/features/rehber/components/rehber-kapak";
import { UniLogo } from "@/components/uni-logo";
import { aramaNormalize, eslesmePuani } from "@/lib/arama";
import type { RehberOzet } from "@/lib/rehber";
import type { RehberAramaKaydi } from "@/lib/rehber";
import {
profilDuzenlemeyiAc,
sonucaGitIste,
@@ -60,6 +67,18 @@ const BOS_SONUCLAR: AramaSonuclari = { bolumler: [], universiteler: [] };
const REHBER_SONUC_SINIRI = 4;
const sayi = (deger: number) => deger.toLocaleString("tr-TR");
// Platform değişmez — abonelik gerektirmeyen dış değer (useSyncExternalStore)
const bosAbonelik = () => () => {};
const macMiSunucu = () => true;
function macMiOku(): boolean {
const platform =
(navigator as { userAgentData?: { platform?: string } }).userAgentData
?.platform ??
navigator.platform ??
"";
return /mac|iphone|ipad/i.test(platform);
}
const KESFET_TONLARI = {
slate: {
pencere: "border-slate-200/80 bg-white",
@@ -81,7 +100,7 @@ const KESFET_TONLARI = {
},
} as const;
export function KatalogArama({ rehberler }: { rehberler: RehberOzet[] }) {
export function KatalogArama({ rehberler }: { rehberler: RehberAramaKaydi[] }) {
const router = useRouter();
const profil = useTercihProfili();
const [acik, setAcik] = useState(false);
@@ -95,7 +114,10 @@ export function KatalogArama({ rehberler }: { rehberler: RehberOzet[] }) {
const [sorgu, setSorgu] = useState("");
const [sonuclar, setSonuclar] = useState<AramaSonuclari>(BOS_SONUCLAR);
const [yukleniyor, setYukleniyor] = useState(false);
const [macMi, setMacMi] = useState(true);
// Kısayol etiketi (⌘/Ctrl) platforma bağlı sabit bir dış değer: effect'te
// setState yerine useSyncExternalStore — SSR'da ⌘ (eski varsayılan),
// hydration sonrası gerçek platform.
const macMi = useSyncExternalStore(bosAbonelik, macMiOku, macMiSunucu);
const inputRef = useRef<HTMLInputElement>(null);
const kaydirmaRef = useRef<HTMLDivElement>(null);
const [kenarBlur, setKenarBlur] = useState({ ust: false, alt: false });
@@ -139,15 +161,6 @@ export function KatalogArama({ rehberler }: { rehberler: RehberOzet[] }) {
sonuclar.universiteler.length > 0 ||
rehberSonuclari.length > 0;
useEffect(() => {
const platform =
(navigator as { userAgentData?: { platform?: string } }).userAgentData
?.platform ??
navigator.platform ??
"";
setMacMi(/mac|iphone|ipad/i.test(platform));
}, []);
useEffect(() => {
function kisayolDinle(event: KeyboardEvent) {
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
@@ -722,7 +735,7 @@ function RehberSonucKarti({
yazi,
onSec,
}: {
yazi: RehberOzet;
yazi: RehberAramaKaydi;
onSec: () => void;
}) {
return (