MT Yazı

Sıfır bağımlılıkla bir parçacık sineması

Yayın — 6 dk okuma

Yirmi yıl görünmeyen işi kurdum. Bu, görünür olanı aynı disiplinle kurma denemesinin sökülmüş hâli.

Altyapı işinin tuhaf bir estetiği vardır: iyi yapıldığında kimse fark etmez. Bir yedekleme yirmi yıl boyunca sessizce çalışır, sonra bir gün çalışmaz ve o gün herkes onu öğrenir. Görünmeyen işin ölçüsü budur — yalnızca aksadığında yargılanır.

Bu site o alışkanlığı yüzeye çıkarma denemesi. Aşağıdakiler süsleme değil, sökülmüş parçalar: ne var, neden var, ve kaça mal oluyor.

Kısıt önce gelir

Tek bir kural her şeyi belirledi: harici istek yok.

Bu bir slogan değil, bir başlık satırı. Sitenin güvenlik politikası şunu söylüyor:

Content-Security-Policy: default-src 'self'; script-src 'self';
  style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self' data:;
  connect-src 'self'; object-src 'none'; base-uri 'self';
  form-action 'self'; frame-ancestors 'self'; upgrade-insecure-requests

default-src 'self' yazdığınız anda seçenekleriniz kapanır. GSAP yok, three.js yok, Lenis yok, bir CDN'den çekilen tek bir font bile yok. Animasyon kütüphanesi istiyorsanız yazacaksınız. Kaydırma yumuşatma istiyorsanız yazacaksınız. Parçacık motoru istiyorsanız — evet, onu da.

Bu kural kendini bir kez beklenmedik biçimde kanıtladı. Cloudflare, kendi analitik betiğini uç sunucuda sayfaya enjekte ediyor. Sitenin kendi politikası onu engelledi:

Loading the script 'https://static.cloudflareinsights.com/beacon.min.js/…'
violates the following Content Security Policy directive: "script-src 'self'"

Yerel sunucuda Lighthouse'un "Best Practices" skoru 100; canlıda 93. Aradaki yedi puan tam olarak bu. Politikayı gevşetip beacon'a izin vermek kolaydı. Bunun yerine beacon kapatıldı — çünkü kuralın anlamı, uygunsuz düştüğünde de geçerli olmasıdır.

Parçacıklar nerede yaşıyor

Sayfadaki hareketin tamamı tek bir <canvas> üzerinde, WebGL2 ile çiziliyor. Parçacıkların konumu ve hızı CPU'da tutulmuyor; iki dokuda yaşıyor ve her karede bir fragman gölgelendiricisi onları ilerletiyor. Sonuç bir dokuya yazılır, sonraki karede o doku girdi olur. GPGPU'nun en eski numarası: hesabı ekrana çizmeden, dokudan dokuya yapmak.

Bunun pratik anlamı şu: parçacık sayısı CPU'yu ilgilendirmiyor. Masaüstünde alan tam yoğunlukta koşuyor, telefonda 12.100 parçacığa iniyor — ama ikisinde de kare başına iş aynı gölgelendirici çağrısı.

İki dokunun tutulması küçük ama önemli bir ayrıntı. Aynı dokuya hem okuyup hem yazamazsınız; bu tanımsız davranıştır. Bu yüzden iki tampon dönüşümlü kullanılır: bu karede A okunur B'ye yazılır, sonraki karede roller değişir. Ping-pong denen bu kalıp GPGPU'nun temel hijyeni.

Renk ise ayrı bir hesap. Sahne HDR olarak çiziliyor, sonra parlak bölgeler ayıklanıp bulanıklaştırılıyor (seçici bloom) ve ACES eğrisiyle görüntülenebilir aralığa indiriliyor. Bu üç adım, birikmeli çizimde en kolay yapılan hatayı önlüyor: her şeyin beyaza yanması. Doğrulama süiti tam da bunu ölçüyor — kare başına tepe luma eşiğin altında kalmalı.

Cihaz farkı dört kademeli bir merdivenle yönetiliyor:

KademeNe kapanır
0Hiçbiri — tam sinema
1Zoom-blur yok, bloom iki geçişe iner
2Parçacık sayısı %55'e düşer
3WebGL1 yedeği — GPGPU tamamen kapalı

Kademe 3 önemli: WebGL2 olmayan bir tarayıcıda sayfa çalışmayı bırakmıyor, yalnız simülasyon katmanını kaybediyor. Metin, gezinme, iletişim — hepsi yerinde.

Sekiz dil, tek doğruluk kaynağı

Site sekiz dilde yayınlanıyor: Türkçe kökte, diğer yedisi kendi dizininde. Ama sekiz sayfa elle yazılmıyor.

Türkçe sayfa master. İçindeki her çevrilebilir düğüm bir işaret taşıyor:

<span data-i="about.e1t">Kurumsal altyapı</span>

Bir üretici bu işaretleri tarayıp i18n/tr.json dosyasını çıkarıyor — şu an 100 anahtar. Diğer yedi dil aynı anahtarları kendi metinleriyle dolduruyor, üretici de sayfaları basıyor. JSON-LD'nin çevrilebilir alanları da aynı boru hattından geçiyor: unvan, konum, uzmanlık listesi, hizmet açıklaması.

Bu kurulumun en sert kuralı şu: eksik anahtarda üretim durur. Fallback yok, İngilizceye düşme yok. Bir dil geride kalırsa yayın hiç çıkmaz. Sessizce yarım çevrilmiş bir sayfa yayınlamaktansa durmak yeğ.

Türkçe sayfanın üretilmemesi de bilinçli. Master elle yazılıyor çünkü içinde ölçüm yorumları, maske payları, optik hizalama notları var — bunlar üreticinin içine taşınırsa okunamaz hâle gelir.

Sonuç ölçülebilir: sekiz sayfa arasında hreflang bağlantıları 64/64 yön karşılıklı, x-default sekizinde de doğru, sitemap 8 URL × 9 alternatif taşıyor.

Fontlar en ağır parça

Sayfanın tamamı 172 KB, on bir kaynak. Dağılım şaşırtıcı:

KalemAğırlık
Font (woff2)116 KB
JavaScript56 KB
Görsel0 KB
CSS0 KB — satır içi

Sitede tek bir <img> yok. Bütün görsel yük canvas'ta ve SVG'de. Buna karşılık on beş woff2 dosyası var: Latin için dört aile, Kiril için ayrı ikameler. Kiril dosyaları aynı aile adlarına unicode-range ile bağlı — yani Rusça sayfa Latin dosyasını hiç indirmiyor, tarayıcı doğru dosyayı kendi seçiyor.

@font-face bildirimleri HTML'in içine gömülü. Bu, ölçülmüş bir karar: ayrı bir stil dosyası tek render engelleyici kaynaktı ve gömüldüğünde mobil Lighthouse skoru 99'dan 100'e çıktı.

Sıkıştırma sonrası gerçek transfer:

KaynakHamBrotliOran
HTML87.890 B20.675 B%76,5
app.js186.011 B55.354 B%70,2

Ölçmeden "oldu" denmiyor

Depoda iki doğrulama süiti var: sahne fiziği için 72 test, çeviri ve keşif katmanı için 94 test. İkisi de gerçek bir tarayıcıda koşuyor, ekran görüntüsüne bakmıyor — canvas'tan luma okuyor, durum kanallarını sorguluyor, eşikleri sayıyla kontrol ediyor.

Bunun sebebi basit: görsel doğrulama yalan söyler. Bir panel kendiliğinden kayabilir, sekme arkaya düşebilir, requestAnimationFrame hiç tiklememiş olabilir. Ekran görüntüsü "çalışıyor" der, oysa hiçbir şey başlamamıştır.

En öğretici test şu eşikte: tepe luma 0,42'nin altında kalmalı. Sahnede birikmeli (additive) çizim var; doygunluk kolay ve beyaza yanmış bir kare ekran görüntüsünde "parlak" görünür, ölçümde "bozuk".

Ölçüm nasıl yalan söyler

Bu sitede en çok zaman alan şey kod değil, ölçümün kendisine güvenmemeyi öğrenmek oldu.

Bir örnek. Bir otomasyon tarayıcısında sayfa açıldı ve yetmiş saniye boyunca içerik görünmedi. Ekranda siyahlık, açılış perdesi hiç ilerlememiş. İlk yorum açık görünüyordu: sayfa takılmış.

Sonuca varmadan önce üç şey ölçüldü:

document.visibilityState  →  "hidden"
2 saniyedeki rAF karesi   →  0
açılış ilerlemesi (preT)  →  0

Sekme arka plandaydı. requestAnimationFrame arka plandaki sekmelerde tiklemez; sekans hiç başlamamıştı. Sayfada hata yoktu — ölçüm ortamında vardı.

Bu, "bulgu" diye rapora girmesine bir adım kalmış bir yanlıştı. Depoda bunun için yazılı bir kural var: ölçmeden "oldu" deme, ve gördüğünü de ölç. Ekran görüntüsü kanıt değildir; kanıt canvas'tan okunan luma ya da durum kanalının kendisidir.

İki sessiz bozulma

Bir denetim sırasında ortaya çıkan en ilginç şeyler, aranan şeyler değil.

Birincisi: sitemap dosyasındaki lastmod alanı, tarihi eski sitemap dosyasından okuyordu. Yani üretici kendi çıktısını girdi alıyordu. Değer bir kez yazıldıktan sonra hiç ilerlemeyecekti — içerik ne kadar değişirse değişsin, dosya sonsuza dek aynı günü gösterecekti.

Tazelik sinyali veren bir alanın donması, sinyali yalana çevirir. Bayat bir lastmod hiç olmamasından kötüdür: tarayıcıya "bu sayfa değişmiyor" der. Tarih artık tek bir yerde yaşıyor ve sitemap onu izliyor.

İkincisi: JSON-LD'nin çevrilebilir alan listesi üreticide iki ayrı yere kopyalanmıştı — biri çıkarma, biri yazma tarafında. Yeni bir alan yalnız birine eklenirse ne olur? Alan sessizce çevrilmez. Türkçe metin yedi dil sayfasının yapısal verisine olduğu gibi kopyalanır ve doksan dört testin hiçbiri bunu yakalamaz.

Bu, düzeltilmesi gereken şeyin kendisini bozacak bir tuzaktı: eksik description alanını eklemeye kalkan biri, farkında olmadan yedi dile Türkçe metin yazacaktı. Liste tek kaynağa indirildi.

İkisinin ortak dersi şu: bir sistemin en tehlikeli hataları, hiçbir testin başarısız olmadığı hatalardır.

Reddedilen numara

Sitenin en dürüst satırı bir hata kaydında değil, bir karar kaydında.

Lighthouse mobilde bu sayfanın "Largest Contentful Paint" değerini üretemiyor. Sebebi açılış sekansı: perde dört saniye, sonra temas donması, sonra patlama, sonra mürekkep geçişi. Hiçbir DOM metni bu sürenin öncesinde görünür olmuyor; Lighthouse ise izini çok daha erken kapatıyor. Sonuç: NO_LCP — metrik hesaplanamıyor.

Bir düzeltme ölçüldü. İçerik katmanının başlangıç opaklığı 0 yerine 0.004 yapılınca LCP 9.052 ms'den 164 ms'ye iniyor ve tam hero başlığı LCP adayı olarak damgalanıyor. Görsel bedeli yok: 0,004 opaklık en fazla 1/255 birim katkı verir, yani görünmez.

Tam da bu yüzden reddedildi.

LCP'nin tanımı "görüntü alanında görünen en büyük metin bloğu". 0,004 opaklıktaki bir başlık görünmüyor. Chromium'un opacity: 0 istisnası zaten bu kalıbı engellemek için var; 0,004 onun kenarından sıyrılıyor. Sayı düzelirdi, gerçek düzelmezdi: ziyaretçi başlığı yine dokuz saniye sonra görecekti.

Karar dokümana şöyle yazıldı: bu deseni geri getirmeyin.

Bu sayfa neden farklı

Okuduğunuz sayfa yukarıda anlatılan motoru yüklemiyor. Arkadaki parçacık alanı ayrı ve çok küçük bir betik; metin ilk boyamada tam görünür, hiçbir opacity kapısı arkasında beklemiyor.

Sebebi doğrudan yukarıdaki bölüm: bir yazının işi okunmak ve alıntılanmak. Sinematik açılış ana sayfanın imzası, ama bir metin sayfasında aynı sekans yalnız ölçümü değil okumayı da geciktirirdi. Bu bölüm için yazılı bir bütçe var — LCP 2,5 saniyenin altında, sayfa JS'i 60 KB'ın altında, içerikte opaklık kapısı yok — ve doğrulama süiti bunu her koşumda kontrol ediyor.

Aynı disiplin, farklı sonuç. Görünmeyen işin estetiği hiç aksamamasıysa, görünen işin estetiği de ölçülebilir olmasıdır.