On bir kilobaytlık ana sayfa: statik siteler hâlâ iyi fikir
Önceden üretmenin size kazandırdıkları, karşılığında verdikleriniz ve siteyi küçük tutan build hattı, sayfa ağırlığı bütçesi ile cache kuralları.
Bu sitenin ana sayfası, CSS dahil, hat üzerinde yaklaşık on bir kilobayt. Onu üreten çalışan bir süreç yok, arkasında veritabanı yok ve bir şey bozulduğunda yeniden başlatılacak hiçbir şey yok, çünkü sayfayla ilgili hiçbir şey istek anında olmuyor. Bu ne nostalji ne de etrafından dolaştığım bir kısıt. İçeriği her ziyaretçi için aynı olan bir sayfada, diskteki bir dosya doğru implementasyondur ve mühendislik emeğinin çoğu da onu öyle tutmaya gider.
Ne kazanıyorsunuz
Önceden üretmek bir sorun sınıfını küçültmez, ortadan kaldırır. Runtime yoktur, dolayısıyla memory leak yoktur, kilitlenen worker yoktur, ayarlanacak bir süreç yöneticisi ve reboot'tan sonra ayakta tutulacak bir şey yoktur. Veritabanı yoktur, dolayısıyla tükenecek bir connection pool ve deploy ile hizalanması gereken bir migration yoktur. Hosting, dosya sunabilen her şeydir: bir CDN, bir object bucket ya da kiralayabileceğiniz en ucuz makine. Yük profili de düzdür, çünkü bir dosyayı bin kez sunmak, neredeyse hiçbir şeyin bin katıdır.
Az değer verilen kısım güvenlik tarafı. Web açıklarının çoğuna ulaşmak için bir request handler gerekir: girdiden kurulan bir sorgu, kullanıcı verisiyle render edilen bir template, çalınacak bir session, kötüye kullanılacak bir upload yolu. Dosyalardan oluşan bir dizinde bunların hiçbiri yoktur. Geriye sunucu konfigürasyonunun kendisi kalır; küçük ve iyi bilinen bir yüzeydir ama sıfır değildir, nitekim yanlış kurulan root'un .env dosyanızı açması bunu yeterince açık gösteriyor.
Maliyet tarafı da aynı yönde çalışır. Trafik on katına çıktığında statik bir sitede değişen tek şey bant genişliğidir; ölçeklenecek bir süreç, ayarlanacak bir pool ve izlenecek bir kuyruk yoktur. Sayfanın gece yarısı yavaşlaması için geriye tek bir ihtimal kalır, o da ağdır. Bu da arıza aramayı, saatlerce süren bir kazıdan birkaç dakikalık bir kontrole indirir.
Sonuncusu geri alma ve deploy'un hissini değiştiren de bu. Önceki build hâlâ bir dizin olarak duruyor. Geri dönmek bir symlink'i taşımaktır, bir milisaniye sürer ve yarısı başarılı olamaz. Geriye alınacak state yok, ısıtılacak cache yok, dolayısıyla geri dönme kararı bedava olur ve erken verilir; geri dönmenin ucuz olduğu an da tam olarak odur.
Neyden vazgeçiyorsunuz
- Ziyaretçiye göre değişen her şey. Köşedeki isim, sepet, kime sorulduğuna bağlı fiyat. Bunu sonradan tarayıcıdan atılan bir istekle boyayabilirsiniz ama o zaman yine bir runtime'ınız olur, sadece daha az görebildiğiniz bir tane.
- Formlar. POST'u bir şeyin kabul etmesi gerekir: bir servis, küçük bir function ya da zaten çalıştırdığınız bir sunucudaki tek bir endpoint.
- Tazeliğin build süresiyle sınırlanması. Build yirmi dakika sürüyorsa bir yazım hatasını düzeltmek bir tuş vuruşu değil bir olay haline gelir ve insanlar yazım hatalarını düzeltmeyi bırakır.
- Index göndermediğiniz sürece arama. Birkaç yüz sayfa için önceden üretilmiş bir JSON index yeter. On binlerce sayfa için yetmez ve burası gerçek bir yol ayrımıdır.
Sınır, sitenin büyüklüğüyle ya da ne kadar modern olduğuyla ilgili değil. Aynı saniyede soran iki ziyaretçinin aynı byte'ları alıp almayacağıyla ilgili. Alacaklarsa o sayfa bir dosya olabilir. Almayacaklarsa o sayfanın sunucuya ihtiyacı vardır ve diğer bütün sayfaları peşinden sürüklemeden bir sunucusu olabilir.
Onu üreten build
Bir statik build'i değerli kılan üç şey var ve üçünü de yanlış yapmak kolay.
Birincisi, içeriğin HTML'in içinde olması. Boş bir container gönderip tarayıcıdan dolduran bir sayfa, dinamik bir sitenin maliyetini hiçbir faydası olmadan üstlenir: ziyaretçi script'i bekler, crawler bekleyebilir de beklemeyebilir de ve başarısız bir istek eski içerik yerine bembeyaz bir sayfa gösterir.
İkincisi, içerik hash'i taşıyan asset adları. Böylece cache agresif olabilir ve kimseye bayat bir dosya ulaşmaz:
import { createHash } from 'node:crypto';
function emit(name, body) {
const hash = createHash('sha256').update(body).digest('hex').slice(0, 8);
const [base, ext] = [name.slice(0, name.lastIndexOf('.')), name.slice(name.lastIndexOf('.'))];
const out = `/a/${base}.${hash}${ext}`;
files.set(out, body);
return out;
}
const cssUrl = emit('site.css', css); // /a/site.b3f19a27.cssÜçüncüsü, build'i düşüren bir bütçe. Kimsenin denetlemediği bir hedef ayda bir kilobayt yukarı kayar:
const BUDGET = { '/index.html': 14 * 1024, '/blog/index.html': 20 * 1024 };
for (const [route, limit] of Object.entries(BUDGET)) {
const size = Buffer.byteLength(files.get(route));
if (size > limit) {
throw new Error(`${route} ${size} byte, bütçe ${limit}`);
}
}On dört kilobayt sihirli bir sayı değil, ama yeni bir bağlantının ilk birkaç gidiş dönüşüne sığan miktara yakın; gelen sayfa ile beliren sayfa arasındaki fark da orada. İlk ekranın ikinci bir istek beklemeden render olmasını sağlayan şey kritik CSS'i head içine gömmektir ve bu numara güvenlik başlıklarınızla ayrıca konuşulmayı hak eden biçimde etkileşir: satır içi ilk boyamayla katı bir içerik güvenlik politikası.
Cache kuralları
Cache tarafının tamamı iki kurala sığar ve bu ikisini ters kurmak, başka türlü hızlı olan statik sitelerde en sık görülen hatadır:
# HTML: adres hiç değişmez, o yüzden doğrulanmak zorundadır
location / {
add_header Cache-Control "public, max-age=0, must-revalidate" always;
}
# hash'li asset: yeni içerik yeni adres alır, o yüzden hiç doğrulanmaz
location /a/ {
add_header Cache-Control "public, max-age=31536000, immutable" always;
}Sıfır ömürle must-revalidate, dosyanın her seferinde indirileceği anlamına gelmez. Tarayıcının soracağı anlamına gelir ve yerinde bir ETag varsa cevap genelde gövdesiz bir 304 olur, yani birkaç yüz byte. Hash'li bir asset üzerindeki immutable ise tarayıcının hiç sormaması demektir ve geri dönen ziyaretçi için en büyük kazanç budur.
Fontlar ve görseller de hash'li kovaya girer. Yüklenirken kaymaya yol açmamalarını sağlayan şey fontu alt kümelemek ve yedek font metriğini eşlemektir; onu ayrıca anlatmıştım: fontu alt kümele, yedek font metriğini eşle.
Çalıştığını nasıl doğrularsınız
Sayfayı diskteki haliyle değil, hatta teslim edildiği haliyle ölçün; sıkıştırma cevabı üç katına kadar değiştirir:
curl -so /dev/null -w 'bytes=%{size_download} ttfb=%{time_starttransfer}\n' \
-H 'Accept-Encoding: br, gzip' https://ornek.com/
# bytes=11204 ttfb=0.061
curl -sI https://ornek.com/a/site.b3f19a27.css | grep -i cache-control
# cache-control: public, max-age=31536000, immutable
curl -sI https://ornek.com/ | grep -i "cache-control\|etag"
# cache-control: public, max-age=0, must-revalidate
# etag: "a91c4e2f"Sonra içeriğin gerçekten orada olduğunu, hiçbir script çalışmadan doğrulayın:
curl -s https://ornek.com/ | grep -c "<h1"
# 1Burada sıfır görüyorsanız, dosyalarda barınan dinamik bir site yapmışsınız demektir.
Nelere dikkat etmeli
- Gömülü CSS'in bedeli her sayfada ödenir. On sayfada tek bir stylesheet, onun on kopyasından toplamda ucuzdur; o yüzden yalnızca ilk ekranın ihtiyacı olan kuralları gömün, gerisini hash'li dosyayla yükleyin. Kırılma noktasını genel geçer bir kurala güvenerek değil kendi sitenizde ölçerek bulun.
- Yavaş build, sinir bozucu olmaktan öte bir kalite sorunudur. Birkaç dakikayı geçtikten sonra insanlar değişiklikleri toplar, küçük şeyleri düzeltmeyi bırakır ve geri bildirim döngüsü kullanılamayacak kadar uzadığı için daha özensiz inceler.
- Statik artı bir animasyon için iki yüz kilobaytlık script, ziyaretçinin hissedebileceği hiçbir anlamda statik değildir. Runtime sunucudan telefona taşınmıştır; orada daha yavaş ve daha az görünürdür.
- Başkasının endpoint'ine POST eden bir form, ziyaretçilerinizi ona gönderir. Birçok yerde hukuki ağırlığı olan bir karardır ve kendi endpoint'inizin maliyeti olan birkaç satırdan çoğu zaman pahalıdır.
- Sayfayı silmek yetmez. Eski adres hâlâ cache'lerde, bağlantılarda ve arama sonuçlarındadır; oraya ileride ters çevirebileceğiniz bir yönlendirme değil, bir süre geçerli bir yanıt bırakın.
Bu yaklaşımın ayakta kalmasını sağlayan şey, ters gidebilecek şeylerin listesinin kafada tutulacak kadar kısa olması; statik sitelerin yeniden moda olmasıyla bir ilgisi yok. Dosya sunucusunun kötü bir gecesi olmaz. İş, deterministik, test edilebilir ve iki kez koşturması ucuz olan build'e taşınır; ziyaretçiye ulaşan şey de tarif değil sonuç olur. Her projeye, hangi sayfaların gerçekten ziyaretçiye göre değiştiğini sorarak başlayın. Çoğu sitede o liste mimarinin ima ettiğinden çok daha kısadır ve listede olmayan her şey bir dosya olabilir.
Sorular ve cevaplar
- Kurumsal bir site için statik hâlâ makul bir tercih mi?
- Sayfaları her ziyaretçiye aynı görünen bir site için evet, üstelik genelde en ucuz doğru cevap odur. Tanıtım sayfaları, dokümantasyon, portfolyo, katalog ve blog bu tarife uyar. Bir sayfanın giriş yapmış kişiye kendi verisini göstermesi gerektiği anda o sayfanın sunucuya ihtiyacı olur ve bunu diğer sayfaları feda etmeden ekleyebilirsiniz.
- Backend olmadan form nasıl çalışır?
- POST'u bir şeyin kabul etmesi gerekir. Bu bir form servisi olabilir, küçük bir function olabilir ya da zaten çalıştırdığınız bir sunucudaki tek bir endpoint olabilir. Sonuncusu birkaç satıra mal olur ve ziyaretçi verisini sizde bırakır; üçüncü taraf bir endpoint'in tetiklediği gizlilik incelemesinden çoğu zaman daha ucuzdur.
- Statik sitede hangi cache başlıkları gönderilmeli?
- HTML'i kısa ömürle ve bir doğrulayıcıyla gönderin ki tarayıcı her seferinde sorsun ve genelde 304 alsın; çünkü bir sayfanın adresi hiç değişmez, içeriği değişir. Dosya adında içerik hash'i olan asset'leri bir yıllık ömürle ve immutable ile gönderin, çünkü değişiklik yeni bir dosya adı üretir. Bu ikisini karıştırmak statik sitelerde en sık görülen performans hatasıdır.
- Bir sayfa ne kadar küçük olmalı?
- İlk ekranın yavaş bir bağlantıda tek gidiş dönüşte gelmesini sağlayacak kadar küçük; pratikte bu, HTML artı gömülü CSS'in yaklaşık on dört kilobayta sığması demektir. Bu bir kanun değil hedeftir, ama build'i düşüren bir bütçe, kimsenin bakmadığı bir hedeften daha değerlidir.