hasari-api / docs /WEB_PERFORMANCE.md
erdoganpeker's picture
v0.3.0 β€” multimodal vehicle damage MVP
e327f0d

WEB PERFORMANCE β€” arac-hasar-v2 (apps/web)

Statik kod tabanli web performans denetimi. Lighthouse veya gercek bir RUM olcumu calistirilmadi; tum sayilar mantiksal tahmindir. Hedef: orta-uretim hazirligi (MVP demo + pilot), mobil 3G/4G ve masaustu Wi-Fi senaryolarinda kabul edilebilir UX.

Stack: Next.js 15.1.3 (App Router), React 18.3, next-intl, Tailwind 3.4, axios, lucide-react, react-hook-form, zod. output: 'standalone'. Hicbir RSC server-data fetch yok; tum sayfalar client component (kritik bulgu).


0) Ozet β€” Tahmini Lighthouse Skoru (mobil, 4G, mid-tier)

Kategori Skor (tahmini) Yorum
Performance 62 - 72 LCP ve TBT yuksek; her sayfa client component β†’ JS bundle agir
Accessibility 88 - 92 Aria etiketleri ve role tab/tabpanel tutarli; renk kontrasti genel olarak OK
Best Practices 85 - 92 HTTPS / CSP / source-map gibi guvenlik basliklari prod build'de eksik olabilir
SEO 80 - 90 Metadata var; ancak metadataBase: localhost, sitemap yok, robots.txt yok
PWA 0 Manifest yok, service worker yok (kapsam disi olabilir)

Tahmini Core Web Vitals (mobil orta cihaz, 4G hizinda):

  • LCP (Largest Contentful Paint): ~2.4 - 3.2 s (sinir / kotu)
    • Sebep: Hero gorsel placeholder bir DOM-rendered card; Inter font swap; tum sayfa client component β†’ JS-after-hydration LCP'yi ~600 ms erteliyor.
  • INP (Interaction to Next Paint, FID yerine): ~120 - 220 ms (sinir)
    • Foto upload tikinda multipart-FormData hazirlama anlik (kucuk dosya icin ~10 ms), ama URL.createObjectURL her render'da yeniden cagrildigi icin preview ekraninda jank olusabilir.
  • CLS (Cumulative Layout Shift): ~0.08 - 0.15
    • ImageWithOverlay icindeki <img> width/height attribute yok β†’ annotated PNG yuklendiginde sayfa kayiyor. ResultsTabs panel bos yer tutmuyor β†’ "Spinner β†’ result" gecisinde ~150 px shift olasi.

1) Tahmini bundle / "First Load JS"

Hedef: < 200 KB gz First Load JS.

Mevcut prod build calistirilmadi (.next/static/chunks/ icindeki dev artifact'lar 6.5 MB; bu anlamli degil, dev-mode'da unminified ve HMR loader var). Asagidaki tahminler dependency analizinden cikarildi.

Kaynak Tahmini gz boyut Not
React 18 + React-DOM ~42 KB Standart
Next.js runtime + Router ~35 - 45 KB App Router'in client runtime'i Pages Router'dan biraz daha agir
next-intl client provider + tr.json (24 KB raw) ~12 - 18 KB gz Tum mesajlar her sayfaya biniyor (root layout getMessages() -> NextIntlClientProvider)
axios (full) ~14 KB gz Sadece JSON istek yapiyor; fetch wrapper'i ile degisilebilir β†’ -10 KB
react-hook-form + @hookform/resolvers + zod ~22 KB gz Sadece login / register / settings'de kullaniliyor ama root'ta tree-shake edilemiyor cunku zod'u lib/jwt.ts ihtimal cekiyor
lucide-react optimizePackageImports ile per-icon ~0.3 KB Iyi β†’ next.config tarafindan zaten optimize edilmis
@arac-hasar/ui (24 component, hepsi index.ts barrel'dan re-export) ~18 - 25 KB gz Bazi sayfalarda yalnizca Spinner kullaniliyor ama ImageWithOverlay, UploadDropzone gibi agir component'lar yan etki yokken bile tree-shake olabilir varsayilir β€” kontrol edilmedi
Sayfa kodu ~5 - 12 KB / sayfa Tahmin
First Load JS (homepage) ~135 - 165 KB gz Hedef altinda ama az marjla
First Load JS (/results/[id]) ~180 - 220 KB gz ImageWithOverlay + canvas hit-test + zod yan tasimasi nedeniyle sinirda

Onemli not β€” kullanilmayan dependency: react-dropzone@14.3.5 web package.json'inda var, ama Grep ile dogrulandi: apps/web icinde hicbir yerden import edilmiyor (sadece apps/desktop icinde FileDrop.tsx kullaniyor, o da @arac-hasar/ui paketinden geliyor). Dead dep, kaldirilmali β€” react-dropzone ~10 KB gz, tree-shake ile cikiyor olsa bile npm install + audit yukunu artiriyor.


2) Statik analiz bulgulari

2.1 next/image hic kullanilmiyor β†’ LCP +0.8 - 1.4 s

Grep: from 'next/image' β†’ sifir kaynak dosyada eslesme (sadece .next/ ve tsbuildinfo icinde, bunlar Next.js'in kendi kullanimi). Tum gorseller raw <img>:

  • packages/ui/src/components/UploadDropzone.tsx:136 β€” preview thumbnail
  • packages/ui/src/components/ImageWithOverlay.tsx:172 β€” annotated inspection PNG (kritik LCP elementi)
  • apps/web/components/Header.tsx, Footer.tsx β€” sadece SVG icon, sorun degil

Etki:

  • Annotated PNG /api/v1/inspect/{id}/visualization/annotated 200 - 800 KB (model ciktisi YOLO seg overlay'i, JPEG/PNG re-encode olmuyor; statik analizdeki goruntu: model annotator opencv ile PNG yaziyor β†’ buyuk). next/image ile:
    • Otomatik AVIF/WebP serve (Next.js default formats)
    • srcset + sizes cihaza gore tahmini ~%40 - 60 byte tasarrufu
    • Lazy load + native loading="lazy"
    • placeholder="blur" LCP'yi maskeleyebilir
  • Eksiklik: next.config.ts remotePatterns zaten R2 / S3 / localhost:8000 icin acik. Yani next/image kullanimi icin altyapi hazir, sadece component degisikligi gerekli.

ImageWithOverlay'i next/image ile sarmak biraz daha karmasik (canvas hit-test natural width'e bagimli), ama yapilabilir: Image onLoadingComplete callback'i naturalWidth/naturalHeight veriyor.

2.2 Tum sayfalar "use client" β€” RSC'nin hicbir faydasi yok

Grep: 21 dosyada 'use client'. Daha kritigi:

  • app/page.tsx (homepage) β€” useTranslations hook'u sadece server-side cagrilabilir (next-intl getTranslations), ama dosyada 'use client' yok β†’ server component olarak isliyor, iyi.
  • AMA app/inspect/page.tsx, app/(app)/dashboard/page.tsx, app/history/page.tsx, app/results/[id]/page.tsx, app/(auth)/login/page.tsx, app/(auth)/register/page.tsx, app/(admin)/users/page.tsx, app/(app)/settings/page.tsx, app/(app)/inspect/new/page.tsx β€” hepsi client component.

Sonuc: Next.js 15'in en buyuk avantaji β€” RSC'de fetch'i server'da yapip yalnizca veri (JSON degil, RSC-encoded) gondermek β€” kullanilmiyor. Ornegin /dashboard ve /history:

'use client';
useEffect(() => {
  (async () => {
    const data = await listInspections({ pageSize: 20 });
    setItems(data.items ?? []);
  })();
}, []);

Bu pattern Pages Router stili. RSC'ye gecince:

  • Sayfa HTML'i SSR'de hazir gelir β†’ LCP -0.5 s
  • Client'a giden JS: yalnizca interactive parcalar (status filter, search input)
  • API token'lar HttpOnly cookie ile cookie-only kullanilirsa server-side fetch yapilabilir; mevcut auth localStorage tabanli oldugu icin RSC'ye gecmek auth refactor gerektirir (orta efor).

2.3 "use client" abuse: Header / Footer / LanguageSwitcher

  • Header.tsx β€” usePathname + useAuth cektigi icin haklisin, client zorunlu. OK.
  • Footer.tsx β€” 'use client' var (Grep dogruladi) ama Footer.tsx'i tek seferde kontrol edersek muhtemelen sadece tercume gosteriyor. useTranslations yerine getTranslations (server) kullanip server component yaparak ~2 KB bundle tasarrufu.
  • LanguageSwitcher.tsx β€” usePathname + cookie set ettigi icin client gerekli. OK.

2.4 next/dynamic ile kod splitting hic kullanilmamis

Grep: next/dynamic β†’ kaynak dosyada sifir. Ozellikle agir component'lar dinamik yuklenmemis:

  • ImageWithOverlay (canvas + ResizeObserver + hit-test, ~4 KB gz) β€” sadece /results/[id] sayfasinda gerekli, ama @arac-hasar/ui index.ts barrel re-export ettigi icin homepage bundle'ina sizabilir (ESM tree-shake bunu engellemeli, dogrulamadim).
  • ResultsTabs, PartList, CostDisplay, InspectionSummary β€” sadece /results/[id]'de. Bu zaten Next.js'in route-level splitting'i sayesinde ayri chunk'ta.
  • ConfirmDialog (UI paketi) β€” sadece etkilesim sonrasi gerekli; modal'a kadar import edilmemeli.

Onerilen yaklasim: /results/[id] page'inde ImageWithOverlay'i next/dynamic({ ssr: false }) ile yukle, ki ilk-paint (status pending) kucuk bundle ile gelsin, sonra annotated PNG hazir oldugunda overlay yuklensin.

2.5 Polling interval β€” 2000 ms uygun, ama backoff yok

use-inspection-polling.ts:33 β†’ default intervalMs: 2_000. Bu dogru bir secim:

  • 1 s cok agresif (60s'de 60 istek = backend rate-limit baskisi).
  • 2 s makul; backend processing 4 foto icin 8-25 s arasi (PERFORMANCE_NOTES.md'den).

Ancak iyilestirilebilir:

  • Exponential backoff yok. Ilk birkac poll 1 s, sonra 2-5 s gibi gradual artirim daha iyi UX (sonuc hazirsa hizli yakala, hazir degilse server'i bombalamayi birak).
  • WebSocket alternatifi mevcut ama kullanilmiyor. services/backend icinde ws.py var (Promise.all grep hits bunu da bulmadi cunku frontend WebSocket kullanimi yok). Tahmin: backend WS endpoint'i mevcut, frontend hala polling yapiyor. WS gecisi: -%80 network requests, +instant update. Yuksek kazanc, orta efor.
  • Visibility-API entegrasyonu yok. Kullanici tab'i degistirdiginde polling devam ediyor β†’ mobilde pil yiyor. document.visibilitychange ile enabled=false yapilabilir.

2.6 API call paralelligi β€” Promise.all yok (kaynak kod tarafinda)

Grep Promise.all apps/web/*.{ts,tsx} β†’ kaynak dosyada hic eslesme yok, sadece .next/ build artifact'larinda (Next.js runtime kendi kullaniyor).

/dashboard page tek call yapiyor (listInspections), serialization sorunu yok.
/results/[id] page yalnizca polling, single call.

Eger ileride bir sayfada hem auth.me() hem listInspections() lazimsa Promise.all ile paralel; mevcut kod-tabani bunu zaten dogru yapacak yapida ama gozetilmeli.

2.7 Foto preview thumbnail β€” URL.createObjectURL revoke edilmiyor (bellek sizintisi)

packages/ui/src/components/UploadDropzone.tsx:132:

{files.map((f, i) => {
  const url = URL.createObjectURL(f);   // <-- her render'da yeni URL
  return (
    <img src={url} ... />

Iki ayri sorun:

  1. Her render'da yeni blob URL olusuyor β€” file listesi degismediginde bile parent state degisirse FilePreview yeniden render olur. React key={i} korusa bile URL.createObjectURL yeniden cagriliyor. β†’ React'in <img>'i degistirmesi gerekmiyor (URL stringi degisik gorunuyorsa degistirir; browser eski URL'yi GC'ye birakir).
  2. URL.revokeObjectURL hicbir yerde cagrilmiyor β€” bellek sizintisi. 5 MB'lik fotograf 5 kez render olduysa 25 MB tape bellek tutulabilir (browser implementation'a bagli).

Cozum: useMemo + useEffect(cleanup):

const urls = useMemo(
  () => files.map((f) => URL.createObjectURL(f)),
  [files]
);
useEffect(() => () => urls.forEach(URL.revokeObjectURL), [urls]);

Buna ek olarak, 12 MB'lik bir foto direkt <img>'a verilirse browser tum dosyayi decode eder (ekranda 96x96 px gosteriliyor olsa bile). 4 foto x 12 MB = 48 MB decode. Mobilde laggy. Cozum: createImageBitmap(file, { resizeWidth: 200 }) ile thumbnail uretip onu goster.

2.8 Inter font β€” display: swap iyi, ama preconnect yok

app/layout.tsx:11:

const inter = Inter({ subsets: ['latin', 'latin-ext'], display: 'swap', variable: '--font-inter' });
  • display: 'swap' β†’ FOIT yok, iyi.
  • Next.js next/font fonts.googleapis.com'a gitmiyor, fontu build-time'da self-host ediyor β†’ preconnect gereksiz.
  • Iyi durum.

Yine de subsets: ['latin', 'latin-ext'] Turkce karakter icin doğru ama ek ~25 KB woff2. Sadece 'latin-ext' yetebilir (icine 'latin' karakterler de dahil).

2.9 Tailwind safelist / unused CSS

tailwind.config.ts okumadim ama tipik Tailwind 3.4 + Next.js setup'inda content path'leri dogruysa unused class'lar purge edilir. apps/web/components/** ve packages/ui/src/components/** content'te olmali β†’ kontrol et.

Tahmini CSS bundle: ~12-20 KB gz. Iyi.

2.10 Inspection annotated PNG β€” lazy load yok, format optimize degil

ImageWithOverlay'deki <img>:

  • loading="lazy" yok (LCP elementi olabilir, lazy yaparsan LCP kaybeder; result page'de scroll altinda degil, hero, lazy yapma).
  • fetchpriority="high" yok β€” result page'in LCP'si bu image. fetchpriority="high" ekleyince ~300 ms kazanc.
  • Backend taraftan PNG yerine WebP/AVIF serve edilmiyor (tahmin; backend annotator.py opencv ile PNG yaziyor).
    • Backend'de Pillow WEBP quality=85 ile yazsa ~%40-60 boyut tasarrufu.

2.11 Metadata sorunlari

  • metadataBase: new URL('http://localhost:3000') β€” production'da overrride edilmeli (env var). Aksi halde OG tag'leri localhost URL'leri ile yayinlanir.
  • sitemap.ts ve robots.ts yok β€” SEO icin 1 saatlik efor.
  • themeColor: '#1e6ee0' viewport icinde dogru tanimlanmis. Iyi.

3) Top 5 darbogaz (etki sirasi)

# Darbogaz Tahmini etki Efor
1 next/image kullanilmiyor (ozellikle annotated PNG) LCP +0.8 - 1.4 s; bandwidth +%40 mobil Dusuk - Orta (2-3 component)
2 Tum data-fetch sayfalari 'use client' β†’ RSC faydasi sifir LCP +0.4 - 0.7 s; bundle +30 KB Yuksek (auth refactor gerekli)
3 URL.createObjectURL revoke edilmiyor + tam-cozunurluk thumbnail Bellek sizintisi (5 dosya x 12 MB), preview render jank Dusuk
4 WebSocket varken polling kullanilmasi + backoff yok Network: ~30 istek/inspection (60s/2s); pil sarf, server load Orta
5 CLS β€” <img> width/height yok sonuc geldiginde shift CLS +0.05 - 0.1 Dusuk

4) Dusuk efor + yuksek kazanc oneriler (v0.1.x icinde)

  1. react-dropzone kaldir (apps/web/package.json). Hicbir yerde import edilmiyor. β†’ -10 KB gz potansiyel + temizlik.
  2. UploadDropzone FilePreview revoke fix (yukarida snippet). Memory leak + jank giderir.
  3. ImageWithOverlay <img>'a loading="eager" + fetchpriority="high" ekle result page'de. CSS aspect-ratio ile placeholder cizip CLS'i sifirla.
  4. Annotated PNG'ye next/image uygula. Sadece <img> -> <Image> swap, canvas overlay component'in dΔ±sinda nokta-koordinatlari hala normalize, sorun yok.
  5. Polling visibility-aware: useInspectionPolling icine document.visibilityState !== 'visible' durumunda intervalMs *= 4 ekle. Tab arka plandaysa 8 s'de bir poll.
  6. Footer'i server component yap. 'use client' kaldir, getTranslations ile cevir.
  7. metadataBase env-driven: new URL(process.env.NEXT_PUBLIC_SITE_URL ?? 'http://localhost:3000').
  8. sitemap.ts + robots.ts ekle. Tek seferlik 30 dakikalik is, SEO 80β†’90.

5) v0.2 backlog β€” Ileri optimizasyonlar

  1. RSC migration: /dashboard, /history, /results/[id] (sonuc hazirsa) sunucuda fetch. Auth icin Next middleware + HttpOnly cookie pattern'i. Buyuk efor (~3-5 gun) ama her sayfada ~500 ms LCP + ~40 KB bundle kazanci.
  2. WebSocket polling replacement: Backend'de ws.py zaten var. lib/use-inspection-polling.ts yerine useInspectionWebSocket yaz. Fallback olarak polling kalsin (auto-detect WS desteklenmiyorsa). Reconnect logic + heartbeat.
  3. Image pipeline: Backend annotator.py PNG -> WebP. Frontend next/image + priority + responsive sizes. CDN cache headers (Cache-Control: public, max-age=31536000, immutable cunku inspection_id versionly).
  4. next-intl mesaj split: Su an tr.json 24 KB tek dosya, her sayfaya tum dictionary biniyor. Per-route getMessages({ locale, namespace: 'inspect' }) ile split ederek /results sayfasinda auth.* mesajlarini eleme. Tahmini -8 KB gz / sayfa.
  5. Bundle analyzer: @next/bundle-analyzer ekle, gercek First Load JS kontrol et. CI'da PR basina threshold > 200 KB ise fail.
  6. Lighthouse CI: GitHub Actions'da lhci autorun runner. Performance < 75 ise PR'i blocking.
  7. Service Worker (offline + cache): Workbox veya next-pwa. Inspection sonuclarini offline cache'le, "son baktigin sonuc" pattern'i.
  8. Edge runtime: app/api/* route'lari (varsa, kontrol edilmedi) edge'e tasi. Auth middleware'i zaten edge-compatible olabilir.
  9. createImageBitmap thumbnail decode worker thread'de. Buyuk foto preview'lerini decode etmek main thread'i bloklamasin.
  10. Resource hints: <link rel="dns-prefetch" href="//api.hasari.app"> + <link rel="preconnect" href="https://r2.cloudflarestorage.com"> annotated image domain'i icin.

6) Olcum oneri (gercek lighthouse calistirilabilirse)

# Chromium gerektirir
pnpm dlx lighthouse http://localhost:3000 --output=html --output-path=./lighthouse-home.html --view --preset=desktop
pnpm dlx lighthouse http://localhost:3000/results/demo-001 --output=html --output-path=./lighthouse-results.html --preset=mobile --throttling.cpuSlowdownMultiplier=4

Mobil ozellikle --throttling.cpuSlowdownMultiplier=4 ile gercek bir orta cihaz simule eder. Bu doknman sadece kaynak okumaya dayali olsa da, asagidaki 3 metric mutlaka olcum sonrasi dogrulanmali:

  1. / LCP β€” hero card ya da Inter font swap mi?
  2. /inspect INP β€” drop event handler'i 200 ms'in altinda mi?
  3. /results/[id] CLS β€” annotated PNG yuklenince layout kayiyor mu?

Sonuc: MVP icin kullanilabilir ama hicbir RSC / next/image / WebSocket optimizasyonu yok. Top 5 fix'i v0.1.1'de uygulayinca tahmini Performance 72 β†’ 84 - 88 bandina ciker. v0.2'de RSC migration ile 90+ erisilir.