İçeriğe geç
academia.sh

Ders 05 / 22

Kod Bölme ve Tembel Yükleme

Paketin rota sınırlarından bölünmesi; bağımlılık kapanışı, kullanım imzasına göre parça oluşturma, ilk yükleme maliyetinin ölçülmesi, gezinme başına ek indirme, önceden getirme ve bölmenin getirdiği bekleme ile hata durumları.

İçindekiler

Yönlendirme artık hangi ekranın kime açık olduğunu biliyor. Bilmediği şey, o ekranın kodunun ne zaman indirileceği. Kuzey Yamaç Ölçüm İstasyonu uygulamasının bütün ekranları tek bir pakette birlikte iniyor: yalnız oturum açmak için gelen bir kullanıcı, ölçüm grafiğinin çizim kütüphanesini ve istasyon haritasını da indiriyor.

Rota tanımı bu bölünmeyi yapmak için gereken bilgiyi zaten taşıyor. Hangi bileşenin hangi yolda kullanıldığı ağacın içinde yazılı; kullanıcının hangi yolda olduğu da çalışma zamanında bilinen tek şey. Kod bölme, bu iki bilgiyi birleştirip paketi rota sınırlarından ayırma işidir.

Bağımlılık Kapanışı

Bir rotanın gerçekte neye ihtiyaç duyduğu, doğrudan kullandığı modüllerle sınırlı değildir. Modüller birbirine bağlıdır: ölçüm grafiği çizim modülünü, o da tarih biçimlendirmeyi kullanır. Rotanın maliyeti, bağımlılık çizgesinde ondan erişilebilen tüm modüllerin toplamıdır. Bu kümeye bağımlılık kapanışı denir ve Veri Yapıları kursundaki çizge dolaşımıyla hesaplanır.

Kapanışlar çıkarıldıktan sonra ikinci soru gelir: bir modül birden çok rotada kullanılıyorsa hangi parçaya girecek? Her rotanın parçasına kopyalanırsa aynı kod birden çok kez iner. Tek bir ortak parçaya konursa, o parçayı istemeyen rotalar da onu indirmek zorunda kalır.

Çözüm modülleri kullanım imzasına göre gruplamaktır: bir modülün imzası, onu isteyen rotaların kümesidir. Aynı imzaya sahip modüller birlikte iner, çünkü hep birlikte gerekirler.

// kod-bolme.mjs — rota bagimlilik kapanisi, kullanim imzasina gore parcalama ve maliyet
const MODUL = {                       // boyut: aktarilan kB, bagimlilik: dogrudan cagrilanlar
  "cekirdek": { boyut: 38, bagimlilik: ["tasarim-sistemi"] },
  "tasarim-sistemi": { boyut: 24, bagimlilik: [] },
  "tablo": { boyut: 18, bagimlilik: ["tasarim-sistemi"] },
  "grafik": { boyut: 96, bagimlilik: ["tasarim-sistemi", "tarih-bicimlendirme"] },
  "harita": { boyut: 120, bagimlilik: ["tasarim-sistemi"] },
  "form-dogrulama": { boyut: 15, bagimlilik: ["tasarim-sistemi"] },
  "tarih-bicimlendirme": { boyut: 12, bagimlilik: [] },
  "disa-aktarma": { boyut: 30, bagimlilik: [] },
};

// Her rota: kendi duzen zinciri boyunca gereken modullerin listesi.
const ROTA = {
  "/oturum": ["form-dogrulama"],
  "/istasyon": ["tablo", "harita"],
  "/istasyon/:kimlik": ["tablo", "tarih-bicimlendirme"],
  "/istasyon/:kimlik/olcum": ["grafik", "tablo", "disa-aktarma"],
  "/istasyon/:kimlik/ayar": ["form-dogrulama"],
};

const GIRIS = ["cekirdek"];           // her ziyarette inen giris noktasi

function kapanis(kokler) {            // bagimlilik cizgesinde erisilen tum modulller
  const gorulen = new Set(), yigit = [...kokler];
  while (yigit.length) {
    const m = yigit.pop();
    if (gorulen.has(m)) continue;
    gorulen.add(m);
    yigit.push(...MODUL[m].bagimlilik);
  }
  return gorulen;
}

const toplam = (kume) => [...kume].reduce((t, m) => t + MODUL[m].boyut, 0);

const girisKapanis = kapanis(GIRIS);
const rotaKapanis = Object.fromEntries(
  Object.entries(ROTA).map(([yol, k]) => [yol, kapanis(k)]));

// Kullanim imzasi: bir modulu isteyen rotalarin kumesi. Ayni imza = ayni parca.
const imza = new Map();
for (const [modul] of Object.entries(MODUL)) {
  if (girisKapanis.has(modul)) continue;                 // giris parcasinda zaten var
  const isteyen = Object.keys(ROTA).filter((y) => rotaKapanis[y].has(modul));
  if (isteyen.length === 0) continue;
  const anahtar = isteyen.join("+");
  if (!imza.has(anahtar)) imza.set(anahtar, []);
  imza.get(anahtar).push(modul);
}

console.log("-- parcalar --");
console.log(`giris${" ".repeat(3)}${String(toplam(girisKapanis)).padStart(4)} kB  ${[...girisKapanis].sort().join(", ")}`);
const parcaAdi = new Map();
let no = 0;
for (const [anahtar, moduller] of [...imza].sort((a, b) => b[0].split("+").length - a[0].split("+").length)) {
  const ad = `parca-${++no}`;
  parcaAdi.set(anahtar, ad);
  console.log(`${ad} ${String(moduller.reduce((t, m) => t + MODUL[m].boyut, 0)).padStart(4)} kB  ${moduller.sort().join(", ")}`);
  console.log(`${" ".repeat(12)}isteyen: ${anahtar.split("+").join("  ")}`);
}

const tekPaket = toplam(new Set([...girisKapanis, ...Object.values(rotaKapanis).flatMap((s) => [...s])]));
const bolunmus = (yol) => toplam(new Set([...girisKapanis, ...rotaKapanis[yol]]));

console.log("-- ilk yukleme maliyeti --");
console.log("rota".padEnd(26), "tek paket", "bolunmus", "kazanc");
for (const yol of Object.keys(ROTA)) {
  const b = bolunmus(yol);
  console.log(yol.padEnd(26), String(tekPaket).padStart(9), String(b).padStart(9),
    `${String(Math.round((1 - b / tekPaket) * 100)).padStart(5)}%`);
}

console.log("-- gezinmede ek indirme --");
for (const [onceki, sonraki] of [
  ["/istasyon", "/istasyon/:kimlik"],
  ["/istasyon/:kimlik", "/istasyon/:kimlik/olcum"],
  ["/istasyon/:kimlik/olcum", "/istasyon/:kimlik/ayar"],
  ["/oturum", "/istasyon"],
]) {
  const inen = new Set([...girisKapanis, ...rotaKapanis[onceki]]);
  const eksik = [...rotaKapanis[sonraki]].filter((m) => !inen.has(m));
  console.log(`${onceki} -> ${sonraki}`.padEnd(52),
    String(eksik.reduce((t, m) => t + MODUL[m].boyut, 0)).padStart(4), "kB ",
    eksik.sort().join(", ") || "-");
}
-- parcalar --
giris     62 kB  cekirdek, tasarim-sistemi
parca-1   18 kB  tablo
            isteyen: /istasyon  /istasyon/:kimlik  /istasyon/:kimlik/olcum
parca-2   15 kB  form-dogrulama
            isteyen: /oturum  /istasyon/:kimlik/ayar
parca-3   12 kB  tarih-bicimlendirme
            isteyen: /istasyon/:kimlik  /istasyon/:kimlik/olcum
parca-4  126 kB  disa-aktarma, grafik
            isteyen: /istasyon/:kimlik/olcum
parca-5  120 kB  harita
            isteyen: /istasyon
-- ilk yukleme maliyeti --
rota                       tek paket bolunmus kazanc
/oturum                          353        77    78%
/istasyon                        353       200    43%
/istasyon/:kimlik                353        92    74%
/istasyon/:kimlik/olcum          353       218    38%
/istasyon/:kimlik/ayar           353        77    78%
-- gezinmede ek indirme --
/istasyon -> /istasyon/:kimlik                         12 kB  tarih-bicimlendirme
/istasyon/:kimlik -> /istasyon/:kimlik/olcum          126 kB  disa-aktarma, grafik
/istasyon/:kimlik/olcum -> /istasyon/:kimlik/ayar      15 kB  form-dogrulama
/oturum -> /istasyon                                  138 kB  harita, tablo

Parça listesi imza gruplamasının sonucudur. Tasarım sistemi her rotada kullanıldığı için giriş parçasına düşmüştür. Tablo modülü üç rotanın ortağıdır ve tek bir parçada durur; üç rotaya kopyalanmadığı gibi, onu istemeyen oturum ekranına da yüklenmez. Dışa aktarma ile grafik modülleri aynı parçada birleşmiştir, çünkü ikisini de yalnızca ölçüm geçmişi ister — ayrı parçalara konmaları fazladan bir istek doğururdu, hiçbir rotaya kazanç getirmezdi.

Ölçülen Kazanç

İkinci tablo bölmenin neden yapıldığını sayıyla söyler. Oturum ekranıyla gelen bir kullanıcı 353 kB yerine 77 kB indirir. Kazanç rotaya göre değişir: haritayı ya da grafiği isteyen rotalarda daha küçüktür, çünkü asıl ağırlık oradadır.

Buradan iki karar çıkar. Ağır modülü bölmek, hafif modülü bölmekten çok daha fazla kazanç sağlar; bölme çabası boyuta bakılarak yöneltilir. Ve kazanç, girişin büyüklüğüyle sınırlıdır: her ziyarette inen 62 kB hiçbir bölmeyle azalmaz. Giriş parçasına gereksiz modül koymak, bölmeyle kazanılanı geri verir.

Bölmenin bir maliyeti de vardır ve tabloda görünmez: her parça ayrı bir istektir. Modüller, Araçlar ve Ekosistem kursundaki paketleme dersinde tanımlanan bu maliyet, çok küçük parçalarda indirilen bayttan büyük olabilir. Ölçüt şudur: bir parça, kaçınılan indirme kadar kazanç sağlamıyorsa ayrılmaz.

Gezinme Başına Maliyet

Üçüncü tablo bölmenin ilk yüklemeden sonraki etkisini gösterir. İstasyon ayrıntısından ölçüm geçmişine geçiş 126 kB’lik yeni bir indirme gerektirir. Kullanıcı bağlantıya bastığı anda bu indirme başlar; bitene kadar ekranda içerik yoktur.

Tek paketle bu bekleme yoktu — kod zaten inmişti. Bölme, ilk yükleme süresini gezinme süresine dağıtır. Bu takas her zaman kârlı değildir: kullanıcıların büyük çoğunluğunun girdiği bir ekranın kodunu geciktirmek, ilk yüklemeden kazanılanı gezinmede fazlasıyla geri verir.

Takası ortadan kaldıran düzenek önceden getirmedir. Bir parça, gerekmeden önce, boşta kalan zamanda indirilir. Yönlendirmenin bunun için iki iyi işareti vardır: ekranda görünür hâle gelen bağlantılar ve kullanıcının üzerine geldiği bağlantı. Tarayıcı ve Web Platformu kursunda tanıtılan kesişim gözlemcisi birincisini, işaretçi olayları ikincisini verir.

Önceden getirme, gerçek isteğin önüne geçmemelidir. Düşük öncelikli yapılır ve boşta çalışma penceresinde tetiklenir; ölçülü ağ bağlantılarında ise hiç yapılmaz. Aksi hâlde kullanıcının o an ihtiyaç duyduğu veriyle yarışır.

Bekleme ve Hata

Bölünmüş bir parçanın yüklenmesi eşzamansız bir işlemdir; dolayısıyla iki yeni durum doğar ve ikisi de arayüzde karşılanmalıdır.

Bekleme durumu, parça inerken ne görüneceğini belirler. Boş bir ekran bırakmak, kullanıcının gezinmenin çalışmadığını düşünmesine yol açar. Doğru davranış, düzen zincirinin korunan bölümünü ekranda tutup yalnız değişen yuvaya bir gösterge koymaktır: kabuk yerinde kaldığı için ekran sarsılmaz. Çok kısa beklemelerde göstergenin görünüp hemen kaybolması da rahatsız edicidir; gösterge küçük bir gecikmeden sonra çıkarılır.

Hata durumu daha kolay unutulur. Ağ kesilmiş olabilir; parça sunucudan silinmiş olabilir. İkinci durum sanıldığından sıradandır: kullanıcı sayfayı açıkken yeni bir sürüm yayımlanırsa, eski sayfanın istediği parça adları artık bulunmaz. Bu, Eşzamansız JavaScript ve Çalışma Zamanı kursundaki Hata ve Yeniden Deneme Kalıpları dersinin doğrudan uygulamasıdır: yükleme başarısız olduğunda bir kez yeniden denenir, yine başarısız olursa kullanıcıya sayfayı yenilemesini öneren bir hata görünümü gösterilir.

Üçüncü bir tuzak koruyucularla kesişir. Yetkisiz bir kullanıcının hiç göremeyeceği bir ekranın parçası indirilmemelidir; koruyucu kararı, parça isteğinden önce verilir. Aksi hâlde bölme, hem gereksiz bayt indirir hem de yetkisiz kullanıcıya o ekranın var olduğunu bildirir.

Özet

  • Bir rotanın maliyeti, bağımlılık çizgesinde ondan erişilen tüm modüllerin kapanışıdır.
  • Modüller kullanım imzasına — kendilerini isteyen rotalar kümesine — göre gruplanır; aynı imzalı modüller aynı parçaya girer, böylece ne kopyalanır ne de gereksiz indirilir.
  • Bölmenin kazancı girişin büyüklüğüyle sınırlıdır ve ağır modüllerde yoğunlaşır; her parça bir istek maliyeti getirdiği için kazanç sağlamayan bölme yapılmaz.
  • Bölme, ilk yükleme süresini gezinme süresine dağıtır; boşta zamanda ve düşük öncelikle yapılan önceden getirme bu takası hafifletir.
  • Parça yüklemesi eşzamansızdır: bekleme durumunda korunan kabuk ekranda tutulur, hata durumunda yeniden deneme ve açık bir hata görünümü gerekir.
  • Koruyucu kararı parça isteğinden önce verilir; görülemeyecek bir ekranın kodu indirilmez.

Sonraki Adım

Yönlendirme tamamlandı: adres hangi ekranın görüneceğini, kime açık olduğunu ve hangi kodun ineceğini belirliyor. Adresin taşıdığı bir durum türü de tanımlandı — süzgeç, sıralama, sayfa. Uygulamanın geri kalan durumu ise hâlâ dağınık: oturum bilgisi koruyucularda, istasyon listesi bir yerde, açık paneller bileşenlerin içinde, sunucudan gelen ölçümler bir başka yerde. Bunların hepsine “durum” demek, aralarındaki en önemli farkı gizler: bazıları uygulamanın kendi ürettiği, bazıları uzaktaki bir kaynağın kopyası. Sonraki konu durumu türlerine ayırmakla başlar ve her türün nereye konacağını belirleyen ölçütleri kurar.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat