Ders 16 / 20
Paket Boyutu ve Başlangıç
Ertelenmiş bir ekran yüklenince yer tutucu söküldü, ekranın düğümü kuruldu. Ders tembel yüklemenin kurulmanın zamanını değiştirdiğini, akıbeti değiştirmediğini ölçer.
Yayın Güncelleme
Bu derste
- Bir ekranın kodunu ertelemenin açılıştaki yüklenen modül sayısını nasıl değiştirdiğini ölçer
- Ertelenmiş ekran yüklenince yer tutucunun sökülen, ekranın kurulan olduğunu gösterir
- Tembel yüklemenin kurulmanın zamanını değiştirdiğini, akıbeti değiştirmediğini açıklar
- Modülün yüklü kalmasını düğümün sökülmesinden ayırır
İçindekiler
Önceki ders ağacı üç kez yeniden üretti ve hiçbir komut çıkmadı. Ekran baştan oradaydı. Gerçek bir uygulamada her ekran baştan orada değildir ve ekranın kodu da baştan yüklü olmak zorunda değildir. Uygulamanın JS kodu bir pakette durur; açılışta o paketin okunması ve çalıştırılması gerekir. Paket büyüdükçe açılış, kullanıcının hiç açmayacağı ekranların kodunu da taşır.
Kursun sorusu: bu güncelleme görünüm ağacındaki düğüme ne yapıyor? Yanıtlar korunan, güncellenen, kurulan ve sökülen; korunan dışındakilerin toplamı komuttur. Kural çekirdek dosyadadır.
// fate.mjs — bir guncellemenin gorunum agacindaki her dugume ne yaptigini siniflandirir export const FATES = ["korunan", "guncellenen", "kurulan", "sokulen"]; export function el(type, props = {}, children = [], key = null) { return { type, props, children, key }; } function segment(node) { return node.key === null ? node.type : `${node.type}[${node.key}]`; } function identity(node, index) { return node.key === null ? `#${index}` : `key:${node.key}`; } function sameProps(a, b) { const names = [...new Set([...Object.keys(a), ...Object.keys(b)])].sort(); return names.every((n) => JSON.stringify(a[n]) === JSON.stringify(b[n])); } function whole(node, path, fate, ops) { const here = `${path}/${segment(node)}`; ops.push({ fate, path: here }); node.children.forEach((child) => whole(child, here, fate, ops)); } export function reconcile(prev, next, path = "", ops = []) { if (prev === null) { whole(next, path, "kurulan", ops); return ops; } if (next === null) { whole(prev, path, "sokulen", ops); return ops; } if (prev.type !== next.type || prev.key !== next.key) { whole(prev, path, "sokulen", ops); whole(next, path, "kurulan", ops); return ops; } const here = `${path}/${segment(next)}`; ops.push({ fate: sameProps(prev.props, next.props) ? "korunan" : "guncellenen", path: here }); const before = new Map(prev.children.map((child, i) => [identity(child, i), child])); const matched = new Set(); next.children.forEach((child, i) => { const id = identity(child, i); matched.add(id); reconcile(before.has(id) ? before.get(id) : null, child, here, ops); }); prev.children.forEach((child, i) => { if (!matched.has(identity(child, i))) reconcile(child, null, here, ops); }); return ops; } export function printOps(ops) { ops.forEach((op) => console.log(`${op.fate}: ${op.path}`)); } export function countFates(ops) { for (const name of FATES) { console.log(`${name}: ${ops.filter((op) => op.fate === name).length}`); } console.log(`komut: ${ops.filter((op) => op.fate !== "korunan").length}`); }
Bu derste ekranın kodunu yüklemek ağaç dışında bir iştir. Kod yüklenince ağaca bir düğüm girer ve akıbeti o düğüme bakar. İki dosya bu ayrımı kurar. Birincisi ekran kodunu ilk istekte yükleyen ve yüklenen modülleri sayan yükleyicidir. Yükleme bir Promise ile ertelenir; modül ilk istekte yüklenir, sonraki isteklerde aynı modül döner.
// loader.mjs — ekran kodunu ilk kullanimda yukler ve yuklenen modulleri sayar export const loaded = { count: 0 }; const cache = new Map(); const MODULES = { Reports: () => Promise.resolve({ title: "Raporlar", rows: 3 }), Settings: () => Promise.resolve({ title: "Ayarlar", rows: 2 }), }; export function loadScreen(name) { if (!cache.has(name)) { cache.set( name, MODULES[name]().then((mod) => { loaded.count += 1; return mod; }) ); } return cache.get(name); }
loadScreen her modülü ilk istekte yükler ve sayacı artırır. İkinci istekte önbellekteki söz döner; sayaç artmaz. loaded.count ağaçta değildir ve kuralın girdisi olmaz; ders onu yalnız yüklenen modül sayısını göstermek için tutar.
İkinci dosya durumdan ağaç üreten işlevdir. Uygulamanın bir başlığı ve bir içerik alanı vardır. Rapor ekranı yüklenmemişse içerik alanında bir yer tutucu durur, yüklenmişse rapor ekranı durur.
// app.mjs — uygulamayi gorunum agacina cevirir import { el } from "./fate.mjs"; export function renderApp(state) { const content = state.reports === null ? el("Placeholder", { text: "Yukleniyor" }) : el("Reports", { rows: state.reports.rows }); return el("App", {}, [el("Header", { title: "Panel" }), content]); }
Açılışta Ertelenmiş Ekran
Uygulama açılırken rapor ekranı henüz istenmedi. Açılış ağacı yer tutucuyu taşır; ağaç sıfırdan kurulduğu için reconcile(null, ...) her düğümü kurulan verir. Yüklenen modül sayısı sıfırdır.
// main.mjs import { reconcile, printOps } from "./fate.mjs"; import { renderApp } from "./app.mjs"; import { loaded } from "./loader.mjs"; const first = renderApp({ reports: null }); console.log(`yuklenen modul: ${loaded.count}`); printOps(reconcile(null, first));
yuklenen modul: 0 kurulan: /App kurulan: /App/Header kurulan: /App/Placeholder
Açılışta üç düğüm kuruldu ve hiçbir ekran modülü yüklenmedi. Rapor ekranının kodu pakette durabilir, ama ağaca girmediği ve istenmediği için açılışın yükü olmadı.
Yüklenince
Kullanıcı rapor ekranını açtı. Önce modül yüklenir, sonra durum güncellenir ve ağaç yeniden üretilir. Yeni ağaçta içerik alanında yer tutucu yoktur, rapor ekranı vardır.
// main.mjs import { reconcile, printOps } from "./fate.mjs"; import { renderApp } from "./app.mjs"; import { loadScreen, loaded } from "./loader.mjs"; const first = renderApp({ reports: null }); const mod = await loadScreen("Reports"); const second = renderApp({ reports: mod }); console.log(`yuklenen modul: ${loaded.count}`); printOps(reconcile(first, second));
yuklenen modul: 1 korunan: /App korunan: /App/Header sokulen: /App/Placeholder kurulan: /App/Reports
Başlık ve kök korunan kaldı. İçerik alanında iki ayrı tür vardı: yer tutucu ile rapor ekranı. Tür değiştiği için kural özelliklere bakmadan eskiyi söktü ve yeniyi kurdu. /App/Placeholder sökülen, /App/Reports kurulan çıktı. Rapor ekranı, önceki ağaçta karşılığı olmayan bir düğümdür; yer tutucunun bir sonraki hâli değildir.
Tembel yükleme burada kurulmanın zamanını değiştirdi. Rapor ekranının düğümü açılışta kurulmadı; kullanıcı istediği anda kuruldu. Aynı düğüm erken yüklemeyle açılışta da kurulabilirdi. Bunu ayrıca ölçelim.
Erken Yükleme
Erken yüklemede iki ekranın kodu da açılıştan önce yüklenir. İlk ağaçta rapor ekranı zaten vardır; ağaç sıfırdan kurulduğu için reconcile(null, ...) yine yalnız kurulan verir.
// main.mjs import { reconcile, countFates } from "./fate.mjs"; import { renderApp } from "./app.mjs"; import { loadScreen, loaded } from "./loader.mjs"; await Promise.all([loadScreen("Reports"), loadScreen("Settings")]); console.log(`erken: yuklenen modul ${loaded.count}`); const tree = renderApp({ reports: await loadScreen("Reports") }); countFates(reconcile(null, tree));
erken: yuklenen modul 2 korunan: 0 guncellenen: 0 kurulan: 3 sokulen: 0 komut: 3
Erken yüklemede açılış iki modül yükledi ve üç düğüm kurdu; tembel yüklemede açılış sıfır modül yükledi ve üç düğüm kurdu. Üç kurulan düğüm iki ölçümde de aynıdır, ama içlerinden biri farklıdır: erkende üçüncü düğüm rapor ekranıdır, tembelde yer tutucudur. Tembel yükleme ağaçtaki komut sayısını azaltmadı; yükü açılıştan alıp ilk istek anına taşıdı. Bunun bedeli, ilk istekte kullanıcının yer tutucuyu görmesidir.
M12/K04’te bir parçanın bu istekte koşup koşmadığı soruluyordu. Buradaki ekran için de aynı soru sorulabilir, ama akıbet o soruya bakmaz. Ertelenmiş ekran yüklenince düğümü kurulandır; “ertelendi” akıbet değildir. Ertelenmiş bir ekranın ağaçta yeri yoktur; yüklenene kadar yer tutucu vardır. Yer tutucunun kendi akıbeti sökülendir ve ekranın akıbeti kurulandır.
Ertelemenin Kaynakta Görünüşü
Gerçek bir uygulamada ekranı ertelemenin iki yolu vardır. Birincisi ekranı ertelenmiş bir içe almayla tanımlamak ve ekran istenene kadar yer tutucu göstermektir. Model bu parçayı çalıştırmaz.
// Platform kodu: App
const Reports = React.lazy(() => import("./Reports"));
function App() {
return (
<Suspense fallback={<Placeholder />}>
<Reports />
</Suspense>
);
}
Suspense yer tutucuyu tutar: Reports hazır olmadığı sürece ağaçta Placeholder durur, hazır olunca yerini Reports alır. Bu, ölçümde gördüğümüz sökme ve kurma çiftidir.
İkinci yol, modülü bir düğmeye basılana kadar okumamaktır. Modül işlevin içinde istendiği için çalıştırılması da oraya kadar ertelenir.
// Platform kodu: OpenReports
function OpenReports({ onOpen }) {
const open = () => {
const mod = require("./Reports");
onOpen(mod.default);
};
return <Button title="Raporlar" onPress={open} />;
}
İki yol da kodun yüklenmesini ya da çalıştırılmasını ilk kullanıma bırakır. İkisi de açılışta daha az iş demektir, ve ikisi de ilk kullanımda bir bekleme getirir. Beklemenin ne kadar sürdüğü modelde yoktur; bu süre gerçek aygıtta ve gerçek paketle ölçülür.
Kaynak Parçalarını Satır Satır Okumak
İlk parçada üç satır işe yarar. React.lazy(() => import("./Reports")) ekranı hemen okumaz; yalnız “gerektiğinde şu modülü getir” diyen bir tanım kurar. <Suspense fallback={<Placeholder />}> ekran hazır olana kadar ağaçta duracak düğümü söyler. <Reports /> ise ağaçta olması istenen asıl düğümdür; hazır olmadığı sürece ağaca girmez, yerinde yer tutucu durur. Modeldeki loadScreen ilk satırın, renderApp ikinci ve üçüncü satırın karşılığıdır.
İkinci parçada require("./Reports") işlevin içindedir ve bu yüzden yalnız open çağrıldığında çalışır. onOpen(mod.default) yüklenen ekranı uygulamaya verir; onPress={open} ise çağrıyı kullanıcının dokunuşuna bağlar. Burada ağaç düzeyinde bir yer tutucu yoktur: düğmeye basılana kadar ağaçta ekran da yer tutucu da yoktur, basılınca ekran kurulur.
İki parça arasındaki fark, yer tutucunun akıbetini değiştirir. Birinci yolda ağaçta yer tutucu vardır ve yüklenince sökülen olur. İkinci yolda yer tutucu hiç kurulmaz; ekran doğrudan kurulan olur. Dersin akıbeti her iki yolda da ekranın kurulan olmasıdır.
“Ertelendi” Neden Bir Akıbet Değildir
Yukarıdaki ayrımı bir örnekle işlemek yararlıdır. Açılışta ekran henüz yüklenmediği için /App/Reports yolunda ağaçta hiçbir şey yoktur. Kural yolu olmayan bir düğüm için kayıt tutmaz; “ertelendi” diye bir kayıt da yoktur. Yüklenme gerçekleştiği anda ağaçta yeni bir düğüm belirir ve kural onu ilk kez görür: kurulan.
Yani erteleme düğümün akıbetini değiştirmez; yalnız düğümün ağaca ne zaman gireceğini değiştirir. Ertelenmiş ekranın düğümü, kurulacağı güncellemeye kadar akıbet almaz.
Yaygın Yanılgılar
İlk yanılgı, ertelemenin paketi küçülttüğü sanısıdır. Erteleme kodun ne zaman yükleneceğini değiştirir; pakette olup olmadığını değiştirmez. Ekranın kodu yine pakete girer ya da ayrı bir parça olarak yanında gelir. Boyutu küçültmek ayrı bir iştir.
İkinci yanılgı, her ekranı ertelemenin kazanç olduğudur. Erteleme ilk kullanımda bir bekleme getirir. Kullanıcının hemen gideceği bir ekranı ertelemek açılışı hafifletmez, yalnız beklemeyi başka bir ana kaydırır. Erken Yükleme ölçümü bunu gösterir: kod önceden yüklenince kullanıcı bekleme görmez ama açılışta daha çok iş yapılır.
Üçüncü yanılgı, yer tutucunun ağaçta ücretsiz olduğudur. Yer tutucu bir düğümdür; kurulur ve yüklenince sökülür. İki komut bu yüzden yüklenme anında gider: biri yer tutucuyu söker, öbürü ekranı kurar.
Paket Boyutu
Paketin boyutu iki şeyden oluşur: uygulamanın kendi kodu ve içe aldığı kod. Boyutu küçültmek için önce neyin pakette olduğuna bakılır. Hiç kullanılmayan bir içe alma pakette yer tutar; büyük bir kitaplığın küçük bir parçası için bütün kitaplığın içe alınması da öyle. Bu ders boyut yazmaz, çünkü boyut modelde yoktur; gerçek bir paketin içeriğini döküp inceleyen araçlar bu işi yapar.
Başlangıç ise boyuttan farklı bir ölçüdür. İki ayrı soru sorulur: açılışta kaç modül yükleniyor, ve ilk ağaç ne zaman kuruluyor. Küçük bir paket çok modül içerebilir ve hepsi açılışta çalıştırılıyorsa başlangıç yine uzar. Büyük bir paketin çoğu modülü ertelenmişse başlangıç kısa kalabilir. Modeldeki loaded.count bu ikinci soruya karşılık gelir: yüklenen modül sayısı.
Sınırlayıcı Ölçüm
Son ölçümde ekran yüklenir, gösterilir ve sonra gizlenir. İki güncelleme sayılır: yer tutucudan rapor ekranına, rapor ekranından yeniden yer tutucuya. Yüklenen modül sayısı güncellemelerden sonra da okunur. Kural rapor ekranının yoluna süzülür.
// main.mjs import { reconcile, printOps } from "./fate.mjs"; import { renderApp } from "./app.mjs"; import { loadScreen, loaded } from "./loader.mjs"; const empty = renderApp({ reports: null }); const mod = await loadScreen("Reports"); const shown = renderApp({ reports: mod }); const ops = reconcile(empty, shown); reconcile(shown, empty, "", ops); printOps(ops.filter((op) => op.path === "/App/Reports")); console.log(`yuklenen modul: ${loaded.count}`);
kurulan: /App/Reports sokulen: /App/Reports yuklenen modul: 1
Odak düğüm /App/Reports. İlk güncellemede kurulan çıktı: dersin akıbeti budur. İkinci güncellemede ekran gizlendiğinde aynı düğüm sökülen oldu. Modül sayısı ise bir kaldı: düğüm sökülünce modül yüklemesi geri alınmadı.
Bu, ertelemenin sınırını gösterir. Düğümün kurulması ve sökülmesi ağacın işidir; modülün yüklü kalması yükleyicinin işidir. İkisi birbirine bağlı değildir. Modelde ekranı gizlemek modül yüklemesini geri almaz; modül yüklü kalır ve ikinci açılışta yeniden yüklenmez. Kural ikinci açılışta düğümü yine kurulan sayar, çünkü ağaçta düğüm yoktu. Kuralın göremediği şey, ikinci kurulumun yeniden modül yüklemesi gerektirmediğidir.
BU3. Model, bu akıbetin kuralını kurar; model çerçevenin kendisi değildir. Modelde modül yüklemesi bir Promise ile ertelenir ve bir sayaç yüklenen modülleri sayar. Gerçek uygulamada ertelenen şey ya paketin içinde durup çalıştırılması ertelenen bir modüldür ya da paketten ayrı yüklenen bir parçadır; hangisinin olduğu araç zincirine göre değişir ve model ikisini ayırmaz. Paket boyutu, açılış süresi ve yükleme bekleme süresi modelde yoktur.
Özet
- Bu dersin akıbeti: kurulan.
/App/Reportsdüğümü, ertelenmiş ekran yüklenince ağaca girdi ve kurulan çıktı; yer tutucu aynı güncellemede sökülen oldu. - Tembel yüklemede açılış sıfır modül yükledi; erken yüklemede iki modül yükledi. Her iki açılışta da üç düğüm kuruldu.
- Tembel yükleme kurulmanın zamanını değiştirir, akıbeti değil: “ertelendi” bir akıbet değildir.
- Düğüm sökülünce modül yüklü kaldı; düğümün kurulması ve sökülmesi ile modülün yüklenmesi iki ayrı iştir.
- Kural paket boyutunu ve bekleme süresini görmez; bunları gerçek aygıtta ve gerçek paketle ölçmek gerekir.
Sonraki Adım
Bu derste ekranın kodu yüklenebiliyordu ve ağaç ona göre kurulabiliyordu. Kod yüklenirken ya da çalışırken hata çıkarsa ne olur? Sıradaki ders, Hata Ayıklama Araçları, bir hata sınırının bozuk alt ağacı söküp yedek görünümü kurmasını ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.