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,4 +1,5 @@
import "server-only";
import { after } from "next/server";
import { and, eq } from "drizzle-orm";
import { appDb, schema } from "./appdb";
import { grantCredits, URUNLER } from "./credits";
@@ -62,15 +63,19 @@ export async function odemeyiSonuclandir(
// Dönüşüm kaydı. Koşullu geçişin ardında olduğu için sipariş başına tam bir
// kez düşer (self-healing fallback tekrar çağrılsa da rowsAffected 0 döner).
await sunucuOlayi("odeme_tamamlandi", {
kullaniciId: order.userId,
pathname: "/odeme/sonuc",
ozellikler: {
urun: order.product,
kredi: order.credits,
tutar: order.amountKurus / 100,
},
});
// after(): yanıt gönderildikten sonra koşar — callback redirect'ini ve
// /odeme/sonuc render'ını Rybbit'in 3 sn'lik timeout'u bloklayamaz.
after(() =>
sunucuOlayi("odeme_tamamlandi", {
kullaniciId: order.userId,
pathname: "/odeme/sonuc",
ozellikler: {
urun: order.product,
kredi: order.credits,
tutar: order.amountKurus / 100,
},
}),
);
return "paid";
}

View File

@@ -25,6 +25,13 @@ export type RehberOzet = {
tarih: string;
};
/**
* Header'daki ⌘K aramasına giden asgari alan seti. `tarih` client'ta hiç
* okunmadığı için RSC payload'ına taşınmaz (root layout üzerinden her
* sayfaya serileşiyordu).
*/
export type RehberAramaKaydi = Pick<RehberOzet, "slug" | "baslik" | "aciklama">;
export type RehberSema = {
/** Mermaid tanımı (aria/altyazi yönergeleri ayıklanmış hâli) */
tanim: string;
@@ -40,6 +47,16 @@ export type RehberYazi = RehberOzet & { parcalar: RehberParca[] };
const REHBER_DIZIN = path.join(process.cwd(), "content", "rehber");
// SiteHeader root layout'ta olduğu için getAllRehberler her sayfa
// render/prerender'ında koşuyor; cache'siz hâli sayfa başına 24 dosya okuma +
// 24 marked.parse demekti. Yalnızca prod'da cache'lenir — dev'de içerik
// düzenlenirken taze fs okuması korunur (katalog.ts/db.ts globalThis deseni).
const globalRehber = globalThis as unknown as {
rehberYazilar?: Map<string, RehberYazi | null>;
rehberOzetler?: RehberOzet[];
};
const CACHE_AKTIF = process.env.NODE_ENV === "production";
function parseFrontmatter(raw: string): {
meta: Record<string, string>;
body: string;
@@ -105,21 +122,62 @@ function parcala(slug: string, body: string): RehberParca[] {
return parcalar;
}
function oku(slug: string): RehberYazi | null {
const dosya = path.join(REHBER_DIZIN, `${slug}.md`);
if (!fs.existsSync(dosya)) return null;
const { meta, body } = parseFrontmatter(fs.readFileSync(dosya, "utf8"));
function metaDogrula(
slug: string,
meta: Record<string, string>,
): asserts meta is { baslik: string; aciklama: string; tarih: string } {
if (!meta.baslik || !meta.aciklama || !meta.tarih) {
throw new Error(
`rehber: ${slug}.md frontmatter eksik (baslik/aciklama/tarih zorunlu)`,
);
}
}
function oku(slug: string): RehberYazi | null {
if (CACHE_AKTIF) {
const onbellek = globalRehber.rehberYazilar?.get(slug);
if (onbellek !== undefined) return onbellek;
}
const dosya = path.join(REHBER_DIZIN, `${slug}.md`);
let yazi: RehberYazi | null = null;
if (fs.existsSync(dosya)) {
const { meta, body } = parseFrontmatter(fs.readFileSync(dosya, "utf8"));
metaDogrula(slug, meta);
yazi = {
slug,
baslik: meta.baslik,
aciklama: meta.aciklama,
tarih: meta.tarih,
parcalar: parcala(slug, body),
};
}
if (CACHE_AKTIF) {
(globalRehber.rehberYazilar ??= new Map()).set(slug, yazi);
}
return yazi;
}
/**
* Liste görünümleri için fast-path: yalnızca frontmatter parse edilir,
* `parcala()`/`marked.parse` hiç çağrılmaz (özetler `parcalar`ı kullanmıyor).
* Tam parse zaten cache'teyse dosyaya inmeden onu kırpar.
*/
function ozetOku(slug: string): RehberOzet | null {
const tam = CACHE_AKTIF ? globalRehber.rehberYazilar?.get(slug) : undefined;
if (tam !== undefined) {
if (tam === null) return null;
const { slug: s, baslik, aciklama, tarih } = tam;
return { slug: s, baslik, aciklama, tarih };
}
const dosya = path.join(REHBER_DIZIN, `${slug}.md`);
if (!fs.existsSync(dosya)) return null;
const { meta } = parseFrontmatter(fs.readFileSync(dosya, "utf8"));
metaDogrula(slug, meta);
return {
slug,
baslik: meta.baslik,
aciklama: meta.aciklama,
tarih: meta.tarih,
parcalar: parcala(slug, body),
};
}
@@ -139,14 +197,22 @@ export function getRehber(slug: string): RehberYazi | null {
}
export function getAllRehberler(): RehberOzet[] {
return getAllRehberSlugs()
.map((slug) => oku(slug))
.filter((y): y is RehberYazi => y !== null)
.map(({ slug, baslik, aciklama, tarih }) => ({
slug,
baslik,
aciklama,
tarih,
}))
if (CACHE_AKTIF && globalRehber.rehberOzetler) {
return globalRehber.rehberOzetler;
}
const ozetler = getAllRehberSlugs()
.map((slug) => ozetOku(slug))
.filter((y): y is RehberOzet => y !== null)
.sort((a, b) => b.tarih.localeCompare(a.tarih));
if (CACHE_AKTIF) globalRehber.rehberOzetler = ozetler;
return ozetler;
}
/** Header aramasının RSC payload'ına giden kırpılmış liste. */
export function getRehberAramaKayitlari(): RehberAramaKaydi[] {
return getAllRehberler().map(({ slug, baslik, aciklama }) => ({
slug,
baslik,
aciklama,
}));
}