perf: harita/tablo re-render'ları + content-visibility (Faz 6)

- tercih-haritasi: viewBox animasyonu transient — kare başına setState
  (380ms × ~23 render × 81 path) yerine svg.setAttribute; il path ref
  callback'leri key başına stabil; il özetleri/şerit/pin grupları useMemo;
  reduce×3+baskinRisk tek geçiş; çift lookup ve çift uniLogoYoluFromAd kalktı
- liste-store: O(1) idSet + useListedeMi/useListeDoluMu primitive selector'ları;
  program-ekle-butonu artık tüm listeye abone değil — 50 satırlık tabloda
  mutasyon başına 50 yerine yalnız değişen buton render olur
- bolum-program-listesi: decorate-sort-undecorate yerine stable toSorted;
  sıralama/sayfalama startTransition'da
- il-secim-haritasi: normalizeIlAdi modül seviyesinde precompute (render
  başına ~1000 string normalizasyonu vardı); Map/Set/sort useMemo
- kt-cv-satir utility: uzun tablo satırlarında content-visibility:auto +
  contain-intrinsic-size (program/bolum/universite/rapor tabloları)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-08-08 14:23:05 +03:00
parent 668568b5b5
commit f3d8bf9b94
9 changed files with 237 additions and 110 deletions

View File

@@ -1,6 +1,6 @@
"use client";
import { useMemo, useRef, useState } from "react";
import { startTransition, useMemo, useRef, useState } from "react";
import Link from "next/link";
import { ChevronDown, ChevronUp } from "lucide-react";
import type { Program } from "@/types/yokatlas";
@@ -104,19 +104,17 @@ export function BolumProgramListesi({
const siraliProgramlar = useMemo(() => {
if (!aktifAlan) return programlar;
return programlar
.map((program, index) => ({ program, index }))
.sort((a, b) => {
const aDeger = a.program[aktifAlan];
const bDeger = b.program[aktifAlan];
if (aDeger == null && bDeger == null) return a.index - b.index;
if (aDeger == null) return 1;
if (bDeger == null) return -1;
const fark = aDeger - bDeger;
return fark === 0 ? a.index - b.index : yon === "artan" ? fark : -fark;
})
.map(({ program }) => program);
// toSorted stable: eşitlikte orijinal sıra zaten korunur, index
// dekorasyonu (N obje allocation + 2 ek geçiş) gereksizdi.
return programlar.toSorted((a, b) => {
const aDeger = a[aktifAlan];
const bDeger = b[aktifAlan];
if (aDeger == null && bDeger == null) return 0;
if (aDeger == null) return 1;
if (bDeger == null) return -1;
const fark = aDeger - bDeger;
return yon === "artan" ? fark : -fark;
});
}, [aktifAlan, programlar, yon]);
const gosterilenSayfa = aktifAlan ? istemciSayfa : sayfa;
@@ -129,18 +127,22 @@ export function BolumProgramListesi({
[siraliProgramlar, gosterilenSayfa],
);
// Yüzlerce satırın sort+render'ı tıklamayı bloklamasın; buton geri
// bildirimi CSS'ten geldiği için tüm güncelleme non-urgent kalabilir.
function sirala(alan: SiralamaAlani) {
setIstemciSayfa(1);
if (aktifAlan === alan) {
setYon((mevcut) => (mevcut === "artan" ? "azalan" : "artan"));
return;
}
setAktifAlan(alan);
setYon("artan");
startTransition(() => {
setIstemciSayfa(1);
if (aktifAlan === alan) {
setYon((mevcut) => (mevcut === "artan" ? "azalan" : "artan"));
return;
}
setAktifAlan(alan);
setYon("artan");
});
}
function istemciSayfaDegistir(n: number) {
setIstemciSayfa(n);
startTransition(() => setIstemciSayfa(n));
listeRef.current?.scrollIntoView({ block: "start" });
}
@@ -193,7 +195,7 @@ export function BolumProgramListesi({
const devlet = p.unitur === "DEVLET";
return (
<TableRow key={p.id}>
<TableRow key={p.id} className="kt-cv-satir">
<TableCell className="max-w-0 w-full">
<span className="flex min-w-0 items-center gap-2.5">
{p.universiteSlug ? (

View File

@@ -66,7 +66,7 @@ export function UniversiteProgramTablosu({
{programlar.map((p) => {
const taban = programEtkinSira(p);
return (
<TableRow key={p.id}>
<TableRow key={p.id} className="kt-cv-satir">
<TableCell className="max-w-0 w-full">
{p.bolumSlug ? (
<Link