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

@@ -4,6 +4,7 @@
// il seçilen Türkiye haritası. Mavi ton ildeki program sayısını, turuncu
// kontur seçimi gösterir. KKTC gibi harita dışı yerler çip olarak eklenir.
import { useMemo } from "react";
import { cities } from "turkey-map-react/lib/data";
import {
HARITA_GENISLIK,
@@ -12,6 +13,14 @@ import {
normalizeIlAdi,
} from "@/lib/harita";
// normalizeIlAdi (3 replace + toLocaleUpperCase) modül yükünde İL BAŞINA BİR
// kez koşar; eskiden her render'da sort comparator içinde ~1000 kez
// çağrılıyordu (81 il × ~çift log n karşılaştırma × 2 çağrı).
const NORMAL_SEHIRLER = cities.map((city) => ({
city,
key: normalizeIlAdi(city.name),
}));
export function IlSecimHaritasi({
iller,
secili,
@@ -22,9 +31,20 @@ export function IlSecimHaritasi({
secili: string[];
onToggle: (il: string) => void;
}) {
const adetler = new Map(iller.map((i) => [i.il, i.adet]));
const maxAdet = Math.max(1, ...iller.map((i) => i.adet));
const seciliSet = new Set(secili);
const { adetler, maxAdet } = useMemo(() => {
const adetler = new Map(iller.map((i) => [i.il, i.adet]));
let maxAdet = 1;
for (const i of iller) if (i.adet > maxAdet) maxAdet = i.adet;
return { adetler, maxAdet };
}, [iller]);
const seciliSet = useMemo(() => new Set(secili), [secili]);
const siraliSehirler = useMemo(
() =>
[...NORMAL_SEHIRLER].sort(
(a, b) => Number(seciliSet.has(a.key)) - Number(seciliSet.has(b.key)),
),
[seciliSet],
);
return (
<div>
@@ -35,14 +55,7 @@ export function IlSecimHaritasi({
aria-label="Okumak istediğin illeri haritadan seç"
>
{/* Seçili iller en son çizilir ki turuncu kontur komşularca örtülmesin */}
{[...cities]
.sort(
(a, b) =>
Number(seciliSet.has(normalizeIlAdi(a.name))) -
Number(seciliSet.has(normalizeIlAdi(b.name))),
)
.map((city) => {
const key = normalizeIlAdi(city.name);
{siraliSehirler.map(({ city, key }) => {
const adet = adetler.get(key);
const seciliMi = seciliSet.has(key);
const yogunluk = adet ? adet / maxAdet : 0;