Bir web fontunun büyük bölümü sayfanızda hiç kullanılmaz. pyftsubset veya glyphhanger ile kullanılmayan glyph'leri kırpabilir, unicode-range ile koşullu yükleme ekleyebilir ve WOFF2 boyutunu ölçerek LCP üzerindeki etkiyi somutlaştırabilirsiniz.

Font Subsetting ile Sayfa Ağırlığı Nasıl Düşürülür?

Bir web fontunun dosya boyutu yüzlerce kilobayta ulaşabilir; oysa sayfanızda gerçekte kullanılan karakter sayısı genellikle birkaç yüzü geçmez. Latin alfabesiyle yazılmış Türkçe bir sayfa, Kiril, Yunan veya Çince bloklar içeren font dosyasının büyük bölümünü hiç işlemeden servis edebilir. Font subsetting bu fazlalığı kırpar: dosyayı yalnızca sayfanızın ihtiyaç duyduğu glyph'lerle sınırlı tutar.

Boyut küçültmek tek başına amaç değil. Tarayıcı bir font dosyasını indirirken render-blocking davranışına girmese de, ağ bağlantısının meşgul olması diğer kaynakların sıraya girmesini etkiler. LCP (Largest Contentful Paint) sinyalini oluşturan metin bloğu bir web fontuna bağlıysa, font ne kadar erken teslim edilirse o kadar erken boyanır. Subset edilmiş bir font bu pencereyi daraltır.

Ne zaman işe yarar, ne zaman gereksiz olduğunu anlamak için önce font dosyasının ne taşıdığını görmek gerekir.

Font dosyasının içindekiler: glyph tablosu ve alt kümeler

Modern bir font dosyası - TTF, OTF veya WOFF2 - bir karakter haritası (cmap tablosu) ve her karakter için çizim verisi (glyph outline) taşır. Google Fonts'un sunduğu standart "Latin Extended" içeren bir dosya genellikle 400-700 arasında glyph barındırır; Kiril veya Devanagari bloklarını destekleyen bir aile ise kolayca 1.000 glyph'i aşar. Türkçe için Latin artı Latin Extended (A ve B dahil) ve birkaç özel karakter yeterliyken, Rusça içerik olmayan bir sitede Kiril blokları boşuna taşınmış olur.

Subset oluşturmak, bu tablodan ihtiyaç duyulmayan satırları silmektir. Kalan glyph'lerin çizim verisi aynı oranda küçülür; bu küçülme, WOFF2 formatında daha belirgin biçimde görünür, çünkü tekrarlayan veri yapılarının azalması sıkıştırma verimliliğini de artırır. Küçük bir dosya daha küçük kalır.

Kendi ürettiğiniz subset dosyasını kullandığınızda Google Fonts CDN'ine olan bağımlılık da ortadan kalkar. Bu, üçüncü taraf isteğini azaltır; GDPR ve KVKK kapsamında harici istek kaygısı taşıyan siteler için ek bir kazanımdır.

pyftsubset ile karakter aralığı kırpma

pyftsubset, FontTools kütüphanesinin bir parçasıdır ve pip ile kurulur:

pip install fonttools brotli

Temel kullanım şöyle görünür:

pyftsubset NotoSans-Regular.ttf \
  --unicodes="U+0020-007E,U+00C0-024F,U+011E,U+011F,U+015E,U+015F,U+0130,U+0131" \
  --flavor=woff2 \
  --output-file=NotoSans-Regular.subset.woff2

Buradaki unicode aralıkları temel Latin (0020-007E), Latin-1 Supplement ve Latin Extended-A/B'nin geniş bir dilimini, ayrıca Türkçe'ye özgü büyük/küçük G-cedilla (011E/011F), S-cedilla (015E/015F) ve noktalı/noktasız I (0130/0131) karakterlerini kapsar. --flavor=woff2 bayrağı doğrudan WOFF2 çıktısı alır; ara adım gerekmez.

Özelleşmiş bir unicode listesi yerine metin dosyası da kullanılabilir:

pyftsubset NotoSans-Regular.ttf \
  --text-file=kullanilan_karakterler.txt \
  --flavor=woff2 \
  --output-file=NotoSans-Regular.subset.woff2

kullanilan_karakterler.txt dosyasına sayfanızın gerçekten kullandığı tüm benzersiz karakterleri yazarsınız; pyftsubset bu metinden otomatik olarak karakter setini türetir. Statik sitelerde tüm HTML dosyalarını birleştirerek bu listeyi üretmek mümkündür.

Riske dikkat: dinamik içerik veya kullanıcı girişi barındıran sayfalarda sabit bir subset tehlikelidir. İleride eklenen bir karakter mevcut subsette yoksa tarayıcı fallback font'a geçer ve görsel tutarsızlık oluşur. Yorum alanları, arama kutuları veya kullanıcı profili sayfaları için ya tam font, ya çok geniş bir subset, ya da sistem fontu tercih edin.

glyphhanger ile otomatik karakter listesi çıkarma

glyphhanger, tarayıcı aracılığıyla gerçek sayfanızın kullandığı karakterleri tarayarak bir unicode listesi üretir. Node.js ile kurulur:

npm install -g glyphhanger

Temel tarama:

glyphhanger https://orneksayfa.com/makale/ --subset=NotoSans-Regular.ttf

Komut sayfayı yükler, DOM'daki tüm text node'larını okur ve bunları pyftsubset'e uygun unicode aralıklarına dönüştürerek hem subset font dosyasını üretir hem de kullanılan unicode listesini terminale basar. Birden fazla sayfayı taramak için URL listesi verilir:

glyphhanger \
  --urls=https://orneksayfa.com/,https://orneksayfa.com/hakkimizda/ \
  --subset=NotoSans-Regular.ttf --formats=woff2

glyphhanger'ın pratikliği açıktır: sayfanızın gerçekte ne kullandığını elle yazmak yerine otomatik tespit eder. Kısıtı da bellidir - çalışması için Puppeteer (Headless Chrome) gerektirir ve CI ortamında kurmak ek adım demektir. Statik içeriği küçük projeler için hızlı bir başlangıç noktası sunar; içerik çok değişkense manuel unicode aralığı yaklaşımı daha güvenilirdir.

Kısa bir kural: içeriği sizin kontrol ettikleriniz için glyphhanger, kullanıcı tarafından üretilenler için pyftsubset ile geniş Latin Extended bloğu.

unicode-range ile koşullu yükleme

unicode-range CSS özelliği tarayıcıya şunu söyler: "bu font ailesini yalnızca şu karakter aralığı sayfada kullanılıyorsa yükle." Google Fonts'un yıllardır uyguladığı bu mekanizma, kendi barındırdığınız fontlara da aynen uygulanabilir.

@font-face {
  font-family: 'NotoSans';
  src: url('/fonts/NotoSans-latin.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153,
                 U+02BB-02BC, U+02C6, U+02DA, U+02DC,
                 U+2000-206F, U+20AC, U+2122, U+FEFF, U+FFFD;
}

Sayfada bu aralığın dışında hiçbir karakter yoksa tarayıcı bu font dosyasına hiç istek göndermez. Dili karışık sayfalarınız varsa her dil bloğu için ayrı bir @font-face tanımı oluşturup her birini kendi unicode-range'iyle işaret edebilirsiniz; tarayıcı yalnızca gerekli olanları indirir.

İki adımın birbiriyle uyumlu olması şarttır. Subset aralığını yanlış keserseniz ve unicode-range'i doğru belirtirseniz, tarayıcı dosyayı indirir ama eksik karakter için fallback'e düşer. Subset oluştururken kullandığınız unicode listesi ile CSS'e yazdığınız unicode-range değeri aynı kapsamı tanımlamalıdır; bunları birbirinden bağımsız değiştiremezsiniz.

font-display: swap değerini unicode-range ile birlikte kullanmak LCP için genellikle doğru seçimdir; metin, font yüklenene kadar sistem fontuyla görünür kalır ve yeniden boyanır. Swap süresi uzarsa Cumulative Layout Shift (CLS) riski de doğar - font metriğini sistem fontuyla eşleştirmeyi atlamamak gerekir.

woff2 boyutunu ölçmek ve kazancı doğrulamak

Dosyayı ürettikten sonra ölçüm yapmadan "iyileştirme" iddiasında bulunmak doğru değildir. Terminal üzerinden basit bir karşılaştırma yeterlidir:

ls -lh NotoSans-Regular.woff2 NotoSans-Regular.subset.woff2

Ya da Python ile:

import os
orig = os.path.getsize("NotoSans-Regular.woff2")
sub  = os.path.getsize("NotoSans-Regular.subset.woff2")
print(f"Orijinal: {orig/1024:.1f} KB")
print(f"Subset:   {sub/1024:.1f} KB")
print(f"Fark:     {(orig-sub)/1024:.1f} KB")

Boyut kazancı ne kadar olur? Bu tamamen başlangıç fontuna, kapsanan karakter sayısına ve kullandığınız aralıklara bağlıdır. Temel Latin artı Türkçe karakterlerle sınırlı bir subset, Kiril veya Devanagari içeren bir aileden büyük bir dilim çıkarır; yalnızca Latin bloğu içeren bir fontta ise kazanç sınırlı kalır. WOFF2 zaten etkili bir sıkıştırma sunar; subset bu sıkıştırmaya katkı sağlar, ancak sıfıra yaklaşamazsınız.

Ağ sekmesinden de doğrulama yapılabilir. Chrome DevTools'ta Network filtresini "Font" olarak ayarlayın, sayfayı yenileyin ve font dosyasının transfer boyutunu gözlemleyin. Transfer boyutu disk boyutundan düşükse sunucunuzun brotli veya gzip sıkıştırması aktiftir; her iki değeri de not alın. Hedef LCP zamanlamasına yansımasıdır: ilk içerik boyaması ile font yükleme bitişi arasındaki fark daraldıysa optimizasyon işe yaramıştır.

Ne zaman vazgeçmeli ya da sınırlı tutmalısınız

Font subsetting her durumda doğru hamle değildir. Birkaç senaryoda ters etki yaratır veya maliyeti kazancı aşar.

İçeriği kullanıcı üretenler ya da sık değişenler için subseti çok dar tutmak, eksik karakter riskini doğurur. E-ticaret ürün isimlerinde, kullanıcı profil sayfalarında veya çok dilli destek sistemlerinde sabit bir unicode listesi yetersiz kalır; burada ya sistem fontuna geçin ya da Unicode Latin Extended bloğunun tamamını dahil edin.

Değişken font (variable font) kullanıyorsanız subsetting daha dikkatli bir yaklaşım ister. Bazı değişken fontlarda agresif bir subset, axes (ağırlık, genişlik) interpolasyon tablolarını bozar ve ara ağırlıklar yanlış render eder. pyftsubset'in --with-variation-data bayrağı interpolasyon tablolarını korur; bu bayrağı atlarsanız kırılma sessizce yaşanabilir.

Yalnızca Google Fonts kullanıyorsanız ve kullanıcıların tarayıcı cache'inde bu fontun bulunma ihtimali yüksekse ayrıca subset üretmek anlamsızlaşır. Üçüncü taraf CDN fontları için kendi subset pipeline'ınızı kurmak, aynı zamanda font dosyasını kendi sunucunuza taşıma fırsatı sunar; bu iki adım birleşince hem dosya boyutu hem de harici istek sayısı azalır.

CJK (Çince, Japonca, Korece) gibi glyph sayısı on binlerle ölçülen yazı sistemlerinde subset zorunlu hale gelir - bu fontların tam hali web için pratik değildir. Türkçe için Latin bloğu dar bir hedef; seçici davranmak mümkün, ama tutarlı davranmak zorunludur.

Font subsetting, doğru koşullarda net ve ölçülebilir bir kazanç sunar: gereksiz glyph'lerden arındırılmış bir WOFF2 dosyası hem daha hızlı indirilir hem de tarayıcının font parse süresini kısaltır. Kazancın somut olması için subset aralığını ve unicode-range tanımını tutarlı tutmak, dinamik içerik senaryolarını önceden göz önünde bulundurmak ve değişken font kullanılıyorsa interpolasyon tablolarını korumak gerekir.

Araç seçiminde sabit bir kural yok. Statik içerikli, az sayıda sayfadan oluşan siteler için glyphhanger'ın otomatik taraması pratik bir başlangıç noktasıdır; içeriği sık değişen veya çok dilli siteler için pyftsubset ile elle tanımlanmış unicode aralıkları daha güvenli sonuç verir. Her iki araç da aynı hedefe, farklı yollardan ulaşır.

Boyut kazancını ölçtükten sonra LCP zamanlamanıza bakın: font yükleme, bant genişliği kuyruğunun ne kadarını tutuyordu? Subset bu payı düşürdüyse öncelik doğru verilmiştir. Düşürmediyse başka bir darboğaz var demektir; onu aramak bu adımdan daha verimli olacaktır.