İçeriğe geç
academia.sh

Ders 05 / 19

Susuzlaştırma ve Akış

Sunucuda üretilmiş işaretlemenin istemcide etkileşim kazanması; canlandırmanın maliyeti, parçalı ve gecikmeli biçimleri, parçalı gönderimle ilk baytın öne alınması ve iki tekniğin birlikte kurduğu denge.

İçindekiler

Sunucuda oluşturma dersinde ölçülen bir boşluk açık kalmıştı: gövde ekrana geldiği an ile düğmenin yanıt verdiği an aynı değildir. İşaretleme sunucuda üretilmiştir, olay dinleyicileri ise betik yürüdükten sonra bağlanır. Bu ders o aralığı adlandırır ve iki yönden daraltır.

Aralığı kapatan işleme bu kursta canlandırma (hydration) denir: sunucudan gelen durağan işaretlemenin, istemcide çalışan kodla eşleştirilip etkileşimli hâle getirilmesi. Dersin katalogdaki adı çeviri geleneğinden gelir; kavram kurs boyunca tek bir karşılıkla, canlandırma olarak anılır. İkinci teknik, yanıtın tek parçada değil parça parça gönderilmesidir; buna akışlı gönderim denir.

Canlandırma Ne Yapar

Sunucudan gelen belgede ağaç zaten kuruludur. İstemcideki kodun işi ağacı yeniden kurmak değil, kendi bileşen ağacını gelen ağaçla eşleştirmektir: hangi düğümün hangi bileşene karşılık geldiğini bulmak, olay dinleyicilerini bağlamak ve durum nesnelerini kurmak.

Bu eşleştirmenin üç maliyeti vardır. Bileşenlerin kodu indirilmelidir. İndirilen kod ayrıştırılıp yürütülmelidir. Ağaçtaki her düğüm için eşleşme yapılmalı ve dinleyiciler bağlanmalıdır. Üçüncü kalem, sayfa büyüdükçe düğüm sayısıyla doğrusal büyür.

Eşleştirmenin bir de doğruluk koşulu vardır: istemcinin ürettiği ağaç, sunucununkiyle aynı olmalıdır. Sunucuda o anki saate göre bir değer yazılmışsa ve istemci başka bir saat görüyorsa iki ağaç ayrışır. Bu ayrışma sessiz kalmaz; eşleşemeyen bölüm ya baştan üretilir ya da tutarsız bir arayüz bırakır. Bu yüzden canlandırılacak işaretlemenin belirlenimci olması gerekir: aynı girdiden iki tarafta da aynı çıktı çıkmalıdır.

Yanıtın Parça Parça Gönderilmesi

Canlandırma başlamadan önce belgenin gelmesi gerekir. Belge tek parçada üretiliyorsa en yavaş veri kaynağı bütün sayfayı bekletir. Parçalı gönderim bu bağı koparır: sunucu, hazır olan bölümü hazır olduğu anda yollar.

// akis-sunucu.mjs — ayni sayfayi iki gonderim bicimiyle veren sunucu
import { createServer } from "node:http";

const bekle = (ms) => new Promise((c) => setTimeout(c, ms));

// Sayfanin uc bolumu; her biri ayri bir veri kaynagindan gelir ve 200 ms surer.
const BOLUMLER = [
  ["baslik", '<h1>Kuzey Yamac Olcum Istasyonu</h1>'],
  ["olcumler", '<ul><li>06:00 -4.2 C</li><li>07:00 -3.8 C</li><li>08:00 -2.1 C</li></ul>'],
  ["yorum", '<p>Son 24 saatte donma noktasinin altinda 19 olcum var.</p>'],
];
const oku = async ([, govde]) => { await bekle(200); return govde; };

const BAS = `<!doctype html>
<html lang="tr"><head><meta charset="utf-8"><title>Kuzey Yamac</title></head><body>`;
const SON = `</body></html>\n`;

createServer(async (istek, yanit) => {
  yanit.sendDate = false;
  if (istek.url === "/tek-parca") {
    const parcalar = [];
    for (const bolum of BOLUMLER) parcalar.push(await oku(bolum));
    const govde = BAS + parcalar.join("") + SON;
    yanit.setHeader("Content-Type", "text/html; charset=utf-8");
    yanit.setHeader("Content-Length", Buffer.byteLength(govde));
    yanit.writeHead(200).end(govde);
  } else if (istek.url === "/akisli") {
    yanit.setHeader("Content-Type", "text/html; charset=utf-8");
    yanit.writeHead(200);            // Content-Length yazilmaz: parcali gonderim baslar
    yanit.write(BAS);                // kabuk beklemeden yola cikar
    for (const bolum of BOLUMLER) yanit.write(await oku(bolum));
    yanit.end(SON);
  } else {
    yanit.writeHead(404).end();
  }
}).listen(8175, "127.0.0.1", () => console.log("dinleniyor: 127.0.0.1:8175"));

Parçaların varış anlarını görmek için bir okuyucuya gerek vardır; curl yalnızca ilk ve son anı bildirir.

// okuyucu.mjs — bir yanitin parcalarinin varis anlarini yazar
// Kullanim: node okuyucu.mjs /akisli
import { get } from "node:http";

const yol = process.argv[2] ?? "/akisli";
const basla = performance.now();
const gecen = () => (performance.now() - basla).toFixed(0).padStart(4);

get({ host: "127.0.0.1", port: 8175, path: yol }, (yanit) => {
  console.log(`${gecen()} ms  yanit basligi   Transfer-Encoding: ` +
    `${yanit.headers["transfer-encoding"] ?? "(yok)"}   ` +
    `Content-Length: ${yanit.headers["content-length"] ?? "(yok)"}`);
  yanit.on("data", (parca) => {
    const onizleme = parca.toString().replace(/\n/g, " ").slice(0, 46);
    console.log(`${gecen()} ms  parca ${String(parca.length).padStart(3)} B   ${onizleme}`);
  });
  yanit.on("end", () => console.log(`${gecen()} ms  yanit bitti`));
});

Aşağıdaki betik bu iki dosyayla aynı dizinde çalıştırılır. Bağlantı noktası 8175 keyfîdir ve boş olmalıdır; kullanımdaysa üç dosyada da değiştirilir.

#!/usr/bin/env bash
# akis-sunucu.mjs'yi baslatir, iki gonderim bicimini karsilastirir, sonra durdurur.
node akis-sunucu.mjs > /dev/null &
sunucu=$!
sleep 1

echo "--- ilk bayt sureleri (curl) ---"
for yol in /tek-parca /akisli; do
  printf '  %-11s ' "$yol"
  curl -sS -o /dev/null -w 'ilk bayt %{time_starttransfer} s   toplam %{time_total} s\n' \
    "http://127.0.0.1:8175$yol"
done

echo "--- /tek-parca: parcalarin varisi ---"
node okuyucu.mjs /tek-parca
echo "--- /akisli: parcalarin varisi ---"
node okuyucu.mjs /akisli

kill "$sunucu"
--- ilk bayt sureleri (curl) ---
  /tek-parca  ilk bayt 0.611718 s   toplam 0.611916 s
  /akisli     ilk bayt 0.001006 s   toplam 0.606051 s
--- /tek-parca: parcalarin varisi ---
 611 ms  yanit basligi   Transfer-Encoding: (yok)   Content-Length: 281
 613 ms  parca 281 B   <!doctype html> <html lang="tr"><head><meta ch
 613 ms  yanit bitti
--- /akisli: parcalarin varisi ---
   3 ms  yanit basligi   Transfer-Encoding: chunked   Content-Length: (yok)
   4 ms  parca  99 B   <!doctype html> <html lang="tr"><head><meta ch
 204 ms  parca  36 B   <h1>Kuzey Yamac Olcum Istasyonu</h1>
 407 ms  parca  72 B   <ul><li>06:00 -4.2 C</li><li>07:00 -3.8 C</li>
 610 ms  parca  59 B   <p>Son 24 saatte donma noktasinin altinda 19 o
 610 ms  parca  15 B   </body></html> 
 612 ms  yanit bitti

Süre alanları makineye bağlıdır; her çalıştırmada farklı çıkar. Yorumu iki oran taşır.

İlk bayt süresi yaklaşık altı yüz kat kısalmıştır. Tek parçalı yanıtta ilk bayt üç bölümün tamamını bekler; akışlı yanıtta kabuk, hiçbir veri kaynağı sorulmadan yola çıkar. Toplam süre değişmemiştir: iki yanıt da yaklaşık 610 milisaniyede biter. Akış, işi azaltmaz; işin sonucunu beklemeden göndermeye başlar.

Başlıklardaki fark bu davranışın taşıyıcı katmandaki karşılığıdır. Gövdenin uzunluğu gönderim başlarken bilinmediği için Content-Length yazılamaz; taşıma Transfer-Encoding: chunked ile parçalı yapılır ve her parça kendi uzunluğunu bildirir.

Kazanç ekranda da karşılığını bulur, çünkü belge ayrıştırıcısı gelen baytları beklemeden işler: kabuk geldiğinde biçem dosyaları istenebilir, başlık geldiğinde boyanabilir. İçerik sırayla belirir.

Akışın Getirdiği Kısıtlar

Yanıtın başlığı ilk parçayla birlikte gider ve bir daha değiştirilemez. Bunun üç sonucu vardır.

Durum kodu geri alınamaz. Üçüncü bölümün verisi alınamazsa 500 döndürülemez; başlık çoktan 200 olarak gönderilmiştir. Hata, gövdenin içinde bir bölüm olarak anlatılmak zorundadır. Bu yüzden akışlı bir yanıtta her bölümün kendi hata işaretlemesi tanımlanır.

Yönlendirme yapılamaz. Kimlik denetimi gibi yanıtın tamamını iptal edebilecek kararlar, ilk bayt gönderilmeden önce verilmelidir.

Sıra bir tasarım kararı hâline gelir. Bölümler belgede yazıldıkları sırada gider. En yavaş bölüm başa konursa akışın kazancı yok olur. Bu yüzden bölümler hızlarına göre değil, kullanıcının önce görmesi gereken şeye göre sıralanır ve yavaş bölümlerin yerine önce bir yükleme göstergesi yazılır.

Canlandırmanın Bölünmesi

Akış belgeyi öne çeker; canlandırma maliyeti yerinde durur. Onu azaltmanın yolu, sayfanın tamamını canlandırmamaktır.

Parçalı canlandırma, yalnızca etkileşimli bileşenlerin canlandırılmasıdır. Ölçüm tablosu bir metin bloğudur; hiçbir olayı dinlemez, hiçbir durumu tutmaz. Böyle bir bölümün canlandırılması boşa harcanan iştir.

Gecikmeli canlandırma, etkileşimli bileşenleri de hemen canlandırmamaktır. İlk ekranın dışında kalan bir grafik, görünür alana girdiğinde ya da ilk etkileşimde canlandırılabilir. Gözlemciler dersinde tanıtılan kesişim gözlemcisi, bu tetiklemenin doğal aracıdır.

// canlandirma.mjs — ayni sayfanin uc canlandirma bicimindeki ilk yuk maliyeti
// Girdiler varsayimdir; cikti bu varsayimlarin aritmetigidir, olcum degildir.

const CEKIRDEK_KB = 25;      // her bicimde gonderilen ortak calisma zamani
const AYRISTIRMA_MS_KB = 0.35;
const DUGUM_MS = 0.02;       // bir dugumu etkilesime baglama maliyeti

// Istasyon sayfasinin bilesen dokumu.
const BILESENLER = [
  { ad: "sayfa kabugu",   dugum: 120, etkilesimli: false, kb: 4,  ilkEkran: true },
  { ad: "gezinme menusu", dugum: 40,  etkilesimli: true,  kb: 6,  ilkEkran: true },
  { ad: "olcum tablosu",  dugum: 520, etkilesimli: false, kb: 9,  ilkEkran: true },
  { ad: "suzgec cubugu",  dugum: 30,  etkilesimli: true,  kb: 14, ilkEkran: true },
  { ad: "grafik",         dugum: 260, etkilesimli: true,  kb: 48, ilkEkran: false },
  { ad: "yorum kutusu",   dugum: 45,  etkilesimli: true,  kb: 12, ilkEkran: false },
  { ad: "dipnotlar",      dugum: 90,  etkilesimli: false, kb: 3,  ilkEkran: true },
];

const topla = (liste, alan) => liste.reduce((t, b) => t + b[alan], 0);
const TUM_KB = topla(BILESENLER, "kb");
const ETKILESIMLI = BILESENLER.filter((b) => b.etkilesimli);

// Uc bicim: ilk yukte hangi bilesenlerin canlandirildigi ve toplamda hangilerinin
// kodunun gerektigi degisir.
const BICIMLER = [
  ["tam canlandirma", () => true, BILESENLER],
  ["parcali canlandirma", (b) => b.etkilesimli, ETKILESIMLI],
  ["gecikmeli canlandirma", (b) => b.etkilesimli && b.ilkEkran, ETKILESIMLI],
];

console.log("bicim".padEnd(24) + "ilk yuk KB".padStart(12) + "dugum".padStart(8) +
  "ilk yuk ms".padStart(12) + "sonradan KB".padStart(13) + "hic gonderilmeyen".padStart(19));
for (const [ad, secili, gerekli] of BICIMLER) {
  const canlanan = BILESENLER.filter(secili);
  const ilkKb = CEKIRDEK_KB + topla(canlanan, "kb");
  const dugum = topla(canlanan, "dugum");
  const sure = ilkKb * AYRISTIRMA_MS_KB + dugum * DUGUM_MS;
  const sonradan = topla(gerekli, "kb") - topla(canlanan, "kb");
  const hic = TUM_KB - topla(gerekli, "kb");
  console.log(ad.padEnd(24) + ilkKb.toFixed(0).padStart(12) + String(dugum).padStart(8) +
    sure.toFixed(1).padStart(12) + sonradan.toFixed(0).padStart(13) +
    hic.toFixed(0).padStart(19));
}

const tumDugum = topla(BILESENLER, "dugum");
const etkilesimliDugum = topla(ETKILESIMLI, "dugum");
console.log(`\ntoplam dugum ${tumDugum}, etkilesimli dugum ${etkilesimliDugum} ` +
  `(%${((etkilesimliDugum / tumDugum) * 100).toFixed(0)})`);
bicim                     ilk yuk KB   dugum  ilk yuk ms  sonradan KB  hic gonderilmeyen
tam canlandirma                  121    1105        64.4            0                  0
parcali canlandirma              105     375        44.3            0                 16
gecikmeli canlandirma             45      70        17.1           60                 16

toplam dugum 1105, etkilesimli dugum 375 (%34)

Son satır kararın dayanağıdır: sayfadaki düğümlerin yalnızca yüzde 34’ü etkileşimlidir. Tam canlandırma, kalan yüzde 66 için de eşleşme yapar ve o bileşenlerin kodunu gönderir.

Parçalı biçimde ilk yük süresi 64,4 milisaniyeden 44,3 milisaniyeye iner; kazanç düğümlerden gelir. Gecikmeli biçimde süre 17,1 milisaniyeye düşer, çünkü ilk yükte yalnızca 70 düğüm ve 45 kilobayt vardır; 60 kilobayt sonraya bırakılır. Son sütun, durağan bileşenlerin 16 kilobaytlık kodunun hiç gönderilmediğini gösterir: bu, ertelenen değil, tümüyle ortadan kalkan bir maliyettir.

Gecikmeli biçimin bedeli, ertelenen bileşene ilk dokunuşta ödenir. Kullanıcı grafiğe tıkladığında 48 kilobaytlık kod istenir ve etkileşim o kadar gecikir. Bu yüzden erteleme, ilk ekranın dışındaki ya da kullanılma olasılığı düşük bileşenlere uygulanır; ilk etkileşimin beklendiği yere değil.

İkisinin Birlikte Verdiği Sonuç

Akış ile parçalı canlandırma farklı iki kaleme dokunur ve birbirinin yerine geçmez. Akış, belgenin ilk baytını öne çeker: kullanıcı içeriği daha erken görür. Parçalı canlandırma, etkileşime hazır olma anını öne çeker: kullanıcı daha erken tıklayabilir.

İkisi birlikte kullanıldığında sayfanın her bölümü kendi hızında olgunlaşır. Kabuk hemen gelir, bölümler sırayla belirir, etkileşimli bölümler kodları geldikçe canlanır. Bu düzenin doğal ölçüsü tek bir sayı değildir; bölüm başına “göründü” ve “canlandı” anlarıdır.

Ödenen bedel karmaşıklıktır. Bölüm sınırları, bölümlerin sırası, her bölümün yükleme ve hata durumu, canlandırma tetikleyicileri — hepsi açıkça tanımlanmak zorundadır. Bir sayfada üç bölüm ve iki ertelenmiş bileşen varsa sınanacak durum sayısı, tek parçalı bir yanıta göre kat kat fazladır.

Özet

  • Canlandırma, sunucudan gelen durağan işaretlemenin istemcideki bileşen ağacıyla eşleştirilip etkileşimli hâle getirilmesidir; kod indirme, ayrıştırma ve düğüm başına eşleşme olmak üzere üç maliyeti vardır.
  • Eşleştirmenin koşulu belirlenimciliktir: sunucu ile istemcinin ürettiği ağaç ayrışırsa ilgili bölüm ya baştan üretilir ya da tutarsız kalır.
  • Akışlı gönderim ilk baytı öne çeker ama toplam süreyi değiştirmez; gövde uzunluğu bilinmediği için Content-Length yerine parçalı aktarım kullanılır.
  • Akışın bedeli, başlığın geri alınamamasıdır: durum kodu ve yönlendirme kararları ilk bayt gönderilmeden verilmeli, hatalar gövdenin içinde anlatılmalıdır.
  • Parçalı canlandırma yalnızca etkileşimli bileşenleri, gecikmeli canlandırma yalnızca ilk ekrandakileri canlandırır; ölçülen sayfada ilk yük süresi 64,4 milisaniyeden sırasıyla 44,3 ve 17,1 milisaniyeye iner.

Sonraki Adım

Bu iki teknik, sunucu ile kullanıcı arasındaki mesafeyi olduğu gibi bırakır. İlk dersteki zincir hesabında ağ turu 60 milisaniye varsayılmıştı ve bu kalem kod değişikliğiyle azalmıyordu: sinyal, aradaki fiziksel uzaklığı kat etmek zorundadır. Sunucu tek bir yerde duruyorsa uzaktaki bir kullanıcı her turda bu bedeli öder. Sonraki ders, oluşturmanın kullanıcıya coğrafi olarak yaklaştırılmasını ele alır: mesafenin gecikmeye çevrimi, kenarda çalışan bir katmanın neyi yapıp neyi yapamayacağı ve kaynak sunucuyla iş bölümü.

İ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