Giriş animasyonunuz en büyük boyamayı geciktiriyor
opacity sıfırla başlayan öğe boyanmış sayılmaz. Fade in animasyonu, Largest Contentful Paint değerini gecikme kadar ve fazlası kadar öteler.
Sayfa tarayıcıda sakin ve özenli görünür, raporda ise yavaş çıkar. HTML küçüktür, görseller doğru boyuttadır, sunucu onlarca milisaniyede yanıt verir ama Largest Contentful Paint yine iki saniyeye yakın gelir. Network dökümünde bu farkı açıklayan hiçbir satır yoktur. Sebep, sayfadaki her bloğun başladığı fade in animasyonudur ve tarayıcı burada gayet mantıklı davranmaktadır.
Gerçekte ne oluyor
Largest Contentful Paint, ilk viewport içindeki en büyük görselin veya metin bloğunun ekrana düştüğü andır. Kritik kelime "düştüğü". Tarayıcı, o öğenin pikselleri çizildiğinde bir boyama kaydeder. opacity: 0 olan bir öğe hiçbir şey çizmez. Kutusu vardır, layout'u vardır, fontu yüklenmiş ve görseli çözülmüş olabilir, metrik açısından henüz ortaya çıkmamıştır.
Bu durum sıradan bir girişi şöyle bir zaman çizelgesine çevirir:
- HTML ve CSS gelir, hero başlığı yerleşir ve saydamdır.
- Animasyon kendi gecikmesini bekler, diyelim 200 milisaniye.
- Animasyon 600 milisaniye boyunca saydamdan opağa gider.
- Başlığın gerçekten çizildiği ilk frame, tarayıcının boyama olarak raporladığı frame olur.
Boyama zamanı artık gerçek boyama zamanı artı gecikme, artı ekranda bir şey belirene kadar geçen animasyon payıdır. Güncel motorlar sıfırdan farklı ilk opacity değerine sahip frame'i alır, yani kesin olarak ödediğiniz kısım gecikmedir. Bir zamanlayıcıdan sonra JavaScript ile tetiklenen ya da sayfa yüklendiğinde eklenen bir class ile başlayan giriş, size animasyonun tamamını da ödetebilir.
Stagger, yani sıralı gecikme, işi en iyi niyetli biçimde büyütür. Her çocuğa bir öncekinden 100 milisaniye fazla gecikme veren desen tasarım sunumunda çok hoştur, en büyük öğe dördüncü çocuksa pahalıdır, çünkü metriğin izlediği öğe tam olarak en büyük olandır.
En kötü senaryo sayfanın tamamını kapsayan fade'dir:
/* boyamaya bağlı her metrik artık 200ms sonra başlıyor */
body {
opacity: 0;
animation: enter 700ms 200ms ease forwards;
}Gecikme boyunca hiçbir şey boyanmaz. First Contentful Paint bu gecikmeyi yer, Largest Contentful Paint de yer. Sunucuda veya görsellerde yapacağınız hiçbir iyileştirme sayıyı animasyonun altına indiremez.
Speed Index ise ayrı bir konudur. Görünen alanın ne kadar hızlı dolduğunu, kaydedilen her frame'i son kareyle karşılaştırarak puanlar. İkinci saniyede hâlâ hareket eden bir öğe, her frame'i son kareden farklı tutar ve içerik teknik olarak geldikten sonra bile puan birikmeye devam eder. Parallax katmanı, sonsuz kayan bir şerit veya sürekli süzülen bir arka plan, Largest Contentful Paint kusursuz olsa bile bu sayıyı yukarıda tutar.
transform ile kurulan girişler farklı davranır ve bütün mesele bu farktır. Opak bir öğeyi on piksel öteleyip geri getirdiğinizde, öğe ilk frame'de viewport içinde, bütün pikselleriyle çizilir. Boyanmıştır, sayılmıştır ve boyama zamanı dürüst olandır. Hareket Speed Index'te küçük bir bedel çıkarır, Largest Contentful Paint'te hiçbir şey çıkarmaz.
Nasıl görülür
Tarayıcıya hangi öğeyi hangi anda seçtiğini sorun. Şunu konsola yapıştırıp sayfayı yenileyin:
new PerformanceObserver((list) => {
for (const e of list.getEntries()) {
console.log(Math.round(e.startTime), e.element?.tagName, e.element?.className);
}
}).observe({ type: 'largest-contentful-paint', buffered: true });Her satır bir adaydır, son satır ise sayılan değerdir. Orada yazan öğe hero başlığıysa ve zaman, animasyon gecikmenize bir iki frame eklenmiş haline şüpheli biçimde yakınsa aradığınızı buldunuz demektir.
Tartışmayı bitiren ölçüm, aynı sayfanın animasyonlar kapalıyken alınan halidir. CSS'i elle değiştirmek yerine anahtarı sayfanın içine koyun, böylece istediğiniz kadar tekrarlayabilirsiniz:
if (new URLSearchParams(location.search).has('still')) {
document.documentElement.classList.add('no-entrance');
}.no-entrance *,
.no-entrance *::before,
.no-entrance *::after {
animation: none !important;
transition: none !important;
opacity: 1 !important;
}İki sürümü de kısıtlanmış bir profilde üçer kez çalıştırın ve en iyi sonucu değil medyanı alın. Bir projede aynı sayfa, byte'ları hiç değişmeden, girişli halde 2,4 saniye, girişsiz halde 1,2 saniye raporladı. Sayfa insanın fark edeceği biçimde hızlanmamıştı. Sadece çoktan çizdiği şeyi saklamayı bırakmıştı.
Çözüm
Meselenin büyük kısmını kapsayan kural tek cümle: ilk viewport içinde hiçbir şey opacity sıfırla başlamaz.
- Hero başlığı, hero görseli ve en büyük kart ilk frame'den itibaren görünürdür. Hareket etmelerine izin var, ortaya çıkmalarına yok.
- Katlama çizgisinin üstündeki hareket yalnızca
transformile, kısa mesafede, gecikmesiz veya 100 milisaniyenin altında bir gecikmeyle yapılır. - Katlama çizgisinin altında istediğiniz kadar stagger kullanın. İlk viewport dışındaki öğeler aday olmadığı için gecikmeleri bu metrikte bedava.
- Büyük bir öğede fade kaçınılmazsa animasyonu sıfırdan değil, görünür bir değerden başlatın.
.hero h1 {
animation: rise 320ms cubic-bezier(0.2, 0, 0, 1) both;
}
@keyframes rise {
from { transform: translate3d(0, 10px, 0); }
to { transform: none; }
}
/* tasarım fade istiyorsa sıfırdan başlamayın */
@keyframes soften {
from { opacity: 0.35; }
to { opacity: 1; }
}0.35 ile başlayan öğe ilk frame'de boyanır. Boyama anında kaydedilir, fade ise görünür bir sayfanın üstüne eklenmiş süs olur. İnsana neredeyse aynı görünür, raporda hiçbir şeye mal olmaz.
Çözümün diğer yarısı, script çalışmadığında ne olacağıdır. Girişler çoğu zaman öğe görüş alanına girdiğinde class ekleyen bir observer ile tetiklenir, bu da class gelene kadar öğenin saydam kalması demektir. CSS'i, görünür durum varsayılan olacak ve animasyon bir iyileştirme olarak üstüne binecek şekilde yazın. İçeriğinin görünür olması için script'e muhtaç olan sayfa, başarısız tek bir istek sonrasında bomboş kalır.
Son olarak sistem ayarına saygı gösterin:
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 1ms !important;
animation-iteration-count: 1 !important;
transition-duration: 1ms !important;
scroll-behavior: auto !important;
}
}none yerine bir milisaniye yazmak bilinçli bir tercih. animationend olayını bekleyen kod olayı yine duyar, böylece daha az hareket isteyen kullanıcıda hiçbir akış askıda kalmaz.
Çalıştığını nasıl doğrularsınız
Observer parçasını iki modda da tekrar çalıştırın. Raporlanan öğe aynı, zaman ise birbirine yakın olmalı:
# önce, girişli hali
1840 H1 hero title
# sonra, sadece transform
1160 H1 hero titleSonra sayıya değil, yüklemenin kare kare kaydına bakın. Karelerde başlığın var olduğunu ve yerine oturduğunu görmelisiniz, boş bir kutunun ardından aniden beliren bir metin değil. İlk üç kare boşsa ve dördüncü kare tamsa, bir animasyon hazır olan bir şeyi hâlâ saklıyor demektir. Aynı ilke font ve görsel için de geçerli: alt kümelenmiş ve yedek metrikleri eşlenmiş bir font metni erken ekrana getirir, o emeği verip üstünü fade ile örtmenin anlamı yoktur.
Nelere dikkat etmeli
- Üst öğeye verilen opacity içindeki her şeyi saklar. Sıfırdan açılan bir kart sarmalayıcısı kendi görselini de saklar, üstelik metriğin kullanacağı aday tam olarak o görseldir.
- Viewport dışından kayarak gelen öğe, içeri girene kadar aday değildir. Uzun bir kayma zarif hissettirebilir ama boyama, animasyonun bittiği ana kaydedilir.
- Lab ölçümü ile saha verisi burada aynı yöne gider, çünkü gecikme her cihazın ödediği sabit bir bedeldir. Yavaş telefon kendi sorunlarını üstüne ekler, sizinkini silmez.
- Bunu tasarımdan animasyonu kaldırarak çözmeyin. Amaç sayfayı durağanlaştırmak değil, ortaya çıkma mekanizması olarak opacity kullanmayı bırakmak. Değişikliği tartışmak yerine ölçün, tıpkı ölçmeden optimize etmemek gibi.
Giriş animasyonları, tasarım niyeti ile ölçümün anlaşılır bir sebeple çeliştiği ve sebep anlaşıldığında kolayca düzelen ender yerlerden biri. Tarayıcı, görünmez kalmasını söylediğiniz pikseller için size puan vermez, dolayısıyla iş animasyon başlamadan bitmiş olsa bile fade'in bedelini skor tam olarak öder. Görünür başlayın, azıcık hareket ettirin, hareketi metriğin izlemediği yerlere saklayın. Bu, bir formatı doğru göründüğü için seçmekle dosyaları karşılaştırdığınız için seçmek arasındaki farkla da aynı şey; bu sitedeki noktalardan oluşan görsellerin neden PNG kaldığı da oradan çıkmıştı.
Sorular ve cevaplar
- CSS fade in animasyonu Largest Contentful Paint'i gerçekten etkiler mi?
- Evet. Metrik boyanan pikselle ilgilenir, opacity sıfır olan öğenin ise boyanan pikseli yoktur. Tarayıcı öğeyi görünür bir opacity ile çizene kadar bekler ve o anı boyama zamanı olarak raporlar. Yani animasyon gecikmesinin her milisaniyesi skora eklenir. Layout, font ve çözülmüş görsel çoktan hazır olabilir, sayı bunu göstermez.
- Hangi giriş animasyonları performans açısından güvenli?
- Tam opak bir durumdan başlayan her şey. Küçük bir translate, 0.98'den başlayan bir scale, renk geçişi veya zaten görünen bir öğedeki blur ilk frame'de boyanır ve boyama zamanı gerçek boyama zamanı olur. Hareketi viewport'un içinde tutun, çünkü dışarıdan kayarak gelen bir öğe içeri girene kadar aday bile sayılmaz.
- Kendi animasyonlarımın maliyetini nasıl ölçerim?
- Sayfaya bütün animasyonları ve geçişleri kapatan bir anahtar koyun, sonra aynı sayfayı kısıtlanmış bir profilde iki kere ölçün. largest-contentful-paint tipinde bir PerformanceObserver kullanın ki hangi öğenin ne zaman raporlandığını görün. Her mod için üç koşu ve medyan, 100 milisaniyelik farkla 900 milisaniyelik farkı ayırmaya yeter.
- Azaltılmış hareket isteyen kullanıcılarda animasyonu tamamen kaldırmalı mıyım?
- prefers-reduced-motion ayarına uyun ama animasyonu silmek yerine süreyi kısaltın. animation değerini none yaparsanız animationend olayını bekleyen kod o olayı hiç duymaz ve arayüz takılı kalabilir. Bir milisaniyelik süre insan için anlıktır ve olayı yine de tetikler.