Daha iyi oluşturma performansı için Jank bozma

Tom Wiltzius
Tom Wiltzius

Giriş

Web uygulamanızın animasyonlar, geçişler ve diğer küçük kullanıcı arayüzü efektleri sırasında duyarlı ve sorunsuz olmasını istiyorsunuz. Bu efektlerin takılmadan oynatılmasını sağlamak, "yerel" bir hisle hantal ve kaba bir his arasında fark yaratabilir.

Bu makale, tarayıcıda oluşturma performansı optimizasyonunu kapsayan bir makale serisinin ilk makalesidir. Başlangıç olarak, sorunsuz animasyonun neden zor olduğunu ve bunu başarmak için ne yapılması gerektiğini, ayrıca birkaç kolay en iyi uygulamayı ele alacağız. Bu fikirlerin çoğu, Nat Duca ile bu yıl Google I/O'da verdiğimiz "Jank Busters" (video) konuşmasında sunulmuştu.

V-sync'i kullanıma sunuyoruz

PC'de oyun oynayanlar bu terimi biliyor olabilir ancak web'de çok yaygın değildir: Dikey senkronizasyon nedir?

Telefonunuzun ekranı genellikle (ancak her zaman değil) saniyede yaklaşık 60 kez düzenli aralıklarla yenilenir. Dikey senkronizasyon (veya dikey senkronizasyon), yalnızca ekran yenilemeleri arasında yeni kareler oluşturma işlemini ifade eder. Bunu, ekran arabelleğine veri yazan işlem ile bu verileri ekrana koymak için okuyan işletim sistemi arasındaki bir yarış durumu olarak düşünebilirsiniz. Arabelleğe alınan kare içeriklerinin, yenileme sırasında değil, bu yenilemeler arasında değişmesini isteriz. Aksi takdirde monitör, bir karenin yarısını ve diğerinin yarısını gösterir ve "yırtılma" oluşur.

Sorunsuz bir animasyon elde etmek için ekran her yenilendiğinde yeni bir karenin hazır olması gerekir. Bunun iki önemli sonucu vardır: kare zamanlaması (yani, karenin ne zaman hazır olması gerektiği) ve kare bütçesi (yani, tarayıcının bir kare oluşturması için ne kadar süreye ihtiyacı olduğu). Bir kareyi tamamlamak için ekran yenilemeleri arasındaki süreyi (60 Hz ekranda yaklaşık 16 ms) kullanmanız gerekir ve son kare ekrana yansıtılır yansıtılmaz bir sonraki kareyi üretmeye başlamak istersiniz.

Zamanlama Her Şeydir: requestAnimationFrame

Birçok web geliştiricisi, animasyon oluşturmak için 16 milisaniyede bir setInterval veya setTimeout kullanır. Bu, çeşitli nedenlerle sorun teşkil eder (birazdan daha ayrıntılı olarak bahsedeceğiz). Ancak özellikle dikkat edilmesi gerekenler şunlardır:

  • JavaScript'ten gelen zamanlayıcı çözünürlüğü yalnızca birkaç milisaniye düzeyindedir.
  • Farklı cihazların yenileme hızları farklıdır

Yukarıda bahsedilen kare zamanlama sorununu hatırlayın: Bir sonraki ekran yenilemesi gerçekleşmeden önce, JavaScript, DOM değiştirme, düzen, boyama vb. ile tamamlanmış bir animasyon karesine ihtiyacınız vardır. Düşük zamanlayıcı çözünürlüğü, animasyon karelerinin bir sonraki ekran yenilemesinden önce tamamlanmasını zorlaştırabilir ancak ekran yenileme hızlarındaki varyasyon, sabit bir zamanlayıcıyla bunu imkansız hale getirir. Zamanlayıcı aralığı ne olursa olsun, bir karenin zamanlama penceresinden yavaşça uzaklaşır ve bir kareyi düşürürsünüz. Bu durum, zamanlayıcı milisaniyelik doğrulukla tetiklense bile gerçekleşir. Ancak geliştiricilerin bulunduğu gibi, bu durum gerçekleşmez. Zamanlayıcı çözünürlüğü, makinenin pilde olup olmadığına bağlı olarak değişir, arka plandaki sekmelerin kaynakları tüketmesinden etkilenebilir vb. Bu durum nadir olsa bile (ör. milisaniyelik bir sapma nedeniyle her 16 karede bir) saniyede birkaç kare atladığınızı fark edersiniz. Ayrıca, hiçbir zaman gösterilmeyecek kareler oluşturmak için de çalışma yapıyorsunuzdur. Bu da uygulamanızda başka şeyler yaparken harcayabilceğiniz güç ve CPU süresini boşa harcar.

Farklı ekranların yenileme hızları farklıdır: 60 Hz yaygındır ancak bazı telefonlarda 59 Hz, bazı dizüstü bilgisayarlarda düşük güç modunda 50 Hz, bazı masaüstü monitörlerde 70 Hz kullanılır.

Oluşturma performansını tartışırken saniyedeki kare sayısına (FPS) odaklanmaya eğilimliyiz ancak varyans daha da büyük bir sorun olabilir. Gözlerimiz, animasyonda zamanlaması kötü ayarlanmış animasyonlar nedeniyle oluşabilecek küçük ve düzensiz duraklamaları fark eder.

Doğru zamanlanmış animasyon kareleri elde etmenin yolu requestAnimationFrame kullanmaktır. Bu API'yi kullandığınızda tarayıcıdan bir animasyon karesi istersiniz. Tarayıcınız yakında yeni bir çerçeve oluşturacak olduğunda geri çağırma işleviniz çağrılır. Bu durum, yenileme hızı ne olursa olsun gerçekleşir.

requestAnimationFrame'ün diğer güzel özellikleri de vardır:

  • Arka plandaki sekmelerdeki animasyonlar duraklatılır. Böylece sistem kaynakları ve pil ömründen tasarruf edilir.
  • Sistem, ekranın yenileme hızında oluşturma işlemini yapamazsa animasyonlar yavaşlatılabilir ve geri çağırma işlemi daha seyrek olarak (ör. 60 Hz ekranda saniyede 30 kez) oluşturulabilir. Bu, kare hızını yarıya düşürürken animasyonu tutarlı tutar. Yukarıda da belirtildiği gibi, gözlerimiz kare hızından çok daha fazla varyasyona uyum sağlar. Sabit 30 Hz, saniyede birkaç kare atlayan 60 Hz'den daha iyi görünür.

requestAnimationFrame hakkında her yerde bilgi veriliyor. Bu konuda daha fazla bilgi için Creative JS'deki bu makaleyi inceleyebilirsiniz. Ancak pürüzsüz animasyon için önemli bir ilk adımdır.

Kare Bütçesi

Her ekran yenilemesinde yeni bir kare hazır olmasını istediğimizden, yeni kare oluşturmayla ilgili tüm işlemleri yenilemeler arasında yapmak zorundayız. 60 Hz'lik bir ekranda bu, tüm JavaScript'i çalıştırmak, düzeni gerçekleştirmek, boyamak ve tarayıcıda kareyi göstermek için gereken diğer her şeyi yapmak üzere yaklaşık 16 ms'lik bir süremiz olduğu anlamına gelir. Bu, requestAnimationFrame geri çağırma işlevinizin içindeki JavaScript'in çalışmasının 16 ms'den uzun sürmesi durumunda, yatay senkronizasyon için zamanında bir kare üretme şansınızın olmadığı anlamına gelir.

16 ms çok uzun bir süre değildir. Neyse ki Chrome'un Geliştirici Araçları, requestAnimationFrame geri çağırma işlevi sırasında kare bütçenizi aşıp aşmadığınızı belirlemenize yardımcı olabilir.

Geliştirici Araçları zaman çizelgesini açıp bu animasyonun çalışırken kaydını alarak animasyon yaparken bütçeyi çok aştığımızı hemen görebiliriz. Zaman çizelgesinde "Kareler"e geçip şuna göz atın:

Çok fazla düzen içeren bir demo
Çok fazla düzen içeren bir demo

Bu requestAnimationFrame (rAF) geri çağırma işlevleri 200 ms'den uzun sürüyor. Bu, 16 ms.de bir kareyi işaretlemek için çok uzun bir süredir. Bu uzun rAF geri çağırmalarından birini açarak içinde neler olduğunu görebilirsiniz. Bu durumda, çok fazla düzen olduğunu görebilirsiniz.

Paul'in videosunda, yeniden düzenlemenin nedeni (scrollTop değerini okuma) ve bu durumun nasıl önleneceği hakkında daha fazla bilgi verilmektedir. Ancak buradaki nokta, geri aramayı inceleyip ne kadar uzun sürdüğünü araştırabilmenizdir.

Düzeni çok daha azaltılmış güncellenmiş bir demo
Daha az yer kaplayan bir düzene sahip güncellenmiş bir demo

16 ms kare sürelerine dikkat edin. Çerçevelerdeki boş alan, daha fazla çalışmanız (veya tarayıcının arka planda yapması gereken işi yapmasına izin vermeniz) için gereken yerdir. Bu boş alan iyi bir şeydir.

Takılmanın Diğer Kaynağı

JavaScript destekli animasyonlar çalıştırmaya çalışırken en büyük sorun, diğer öğelerin rAF geri çağırma işlevinizi engellemesi ve hatta çalışmasını tamamen durdurmasıdır. rAF geri çağırma işleviniz basit ve yalnızca birkaç milisaniyede çalışsa bile diğer etkinlikler (yeni gelen bir XHR'yi işleme, giriş etkinliği işleyicilerini çalıştırma veya zamanlayıcıda planlanmış güncellemeleri çalıştırma gibi) aniden gelebilir ve herhangi bir süre boyunca yield vermeden çalışabilir. Mobil cihazlarda bu etkinliklerin işlenmesi bazen yüzlerce milisaniye sürebilir. Bu süre zarfında animasyonunuz tamamen duraklar. Bu animasyon kesintilerine jank diyoruz.

Bu tür durumları önlemek için sihirli bir çözüm yoktur ancak başarıya ulaşmanızı sağlayacak birkaç mimari en iyi uygulama vardır:

  • Giriş işleyicilerinde çok fazla işlem yapmayın. Çok fazla JS çalıştırmak veya örneğin bir onscroll işleyici sırasında sayfanın tamamını yeniden düzenlemeye çalışmak, çok sık karşılaşılan bir sarsıntı sorununa neden olur.
  • Mümkün olduğunca fazla işlem (yani, çalışması uzun zaman alacak her şey) rAF geri çağırma işlevinize veya Web Çalışanları'na gönderin.
  • rAF geri çağırmasına iş gönderirseniz her karede yalnızca biraz işlem yapmanız için işi parçalara ayırmayı deneyin veya önemli bir animasyon sona erene kadar erteleyin. Böylece kısa rAF geri çağırmalarını çalıştırmaya ve sorunsuz bir şekilde animasyon yapmaya devam edebilirsiniz.

İşlemenin giriş işleyicileri yerine requestAnimationFrame geri çağırma işlevlerine nasıl itileceğini kapsayan mükemmel bir eğitim için Paul Lewis'in requestAnimationFrame ile daha basit, daha güçlü ve daha hızlı animasyonlar başlıklı makalesine göz atın.

CSS Animasyonu

Etkinliğinizde ve rAF geri çağırmalarında hafif JS'den daha iyi ne olabilir? JS yok.

Daha önce, rAF geri çağırmalarınızın kesintiye uğramasını önlemek için kesin bir çözüm olmadığını söylemiştik. Ancak bu geri çağırmalara tamamen ihtiyaç duymamak için CSS animasyonunu kullanabilirsiniz. Özellikle Android için Chrome'da (ve diğer tarayıcılar benzer özellikler üzerinde çalışıyor) CSS animasyonları, tarayıcı JavaScript çalışıyor olsa bile bunları genellikle çalıştırabilmesi gibi çok arzu edilen bir özelliğe sahiptir.

Yukarıdaki bölümde, takılmayla ilgili olarak dolaylı bir ifade vardır: Tarayıcılar aynı anda yalnızca bir işlem yapabilir. Bu tam olarak doğru değildir ancak şu varsayımı yapmak iyi bir çalışma yöntemidir: Tarayıcı herhangi bir zamanda JS çalıştırabilir, sayfa düzeni oluşturabilir veya boyama yapabilir ancak aynı anda yalnızca bir işlem yapabilir. Bu durum, Geliştirici Araçları'nın Zaman Çizelgesi görünümünde doğrulanabilir. Bu kuralın istisnalarından biri, Android için Chrome'daki (ve yakında masaüstü Chrome'daki) CSS animasyonlarıdır.

Mümkün olduğunda CSS animasyonu kullanmak hem uygulamanızı basitleştirir hem de JavaScript çalışırken bile animasyonların sorunsuz şekilde çalışmasını sağlar.

  // see http://paulirish.com/2011/requestanimationframe-for-smart-animating/ for info on rAF polyfills
  rAF = window.requestAnimationFrame;

  var degrees = 0;
  function update(timestamp) {
    document.querySelector('#foo').style.webkitTransform = "rotate(" + degrees + "deg)";
    console.log('updated to degrees ' + degrees);
    degrees = degrees + 1;
    rAF(update);
  }
  rAF(update);

Düğmeyi tıklarsanız JavaScript 180 ms boyunca çalışır ve takılmalara neden olur. Ancak bu animasyonu CSS animasyonlarıyla yürütürseniz artık takılma yaşanmaz.

(Bu makalenin yazıldığı sırada CSS animasyonunun yalnızca Android için Chrome'da takılma olmadan çalıştığını, masaüstü Chrome'da çalışmadığını unutmayın.)

  /* tools like Modernizr (http://modernizr.com/) can help with CSS polyfills */
  #foo {
    +animation-duration: 3s;
    +animation-timing-function: linear;
    +animation-animation-iteration-count: infinite;
    +animation-animation-name: rotate;
  }

  @+keyframes: rotate; {
    from {
      +transform: rotate(0deg);
    }
    to {
      +transform: rotate(360deg);
    }
  }

CSS animasyonlarını kullanma hakkında daha fazla bilgi için MDN'deki bu makaleyi inceleyin.

Özet

Özetlemek gerekirse:

  1. Animasyon oluştururken her ekran yenilemesi için kare oluşturmak önemlidir. Vsync'd animasyon, uygulamanın verdiği his üzerinde büyük bir olumlu etki yaratır.
  2. Chrome ve diğer modern tarayıcılarda vsync'li animasyon elde etmenin en iyi yolu CSS animasyonu kullanmaktır. CSS animasyonunun sunduğundan daha fazla esnekliğe ihtiyacınız olduğunda en iyi teknik, requestAnimationFrame tabanlı animasyondur.
  3. rAF animasyonlarının sorunsuz çalışması için diğer etkinlik işleyicilerin rAF geri çağırma işlevinizi engellemediğinden emin olun ve rAF geri çağırma işlevlerini kısa tutun (<15 ms).

Son olarak, vsync'li animasyon yalnızca basit kullanıcı arayüzü animasyonları için değil, Canvas2D animasyonu, WebGL animasyonu ve hatta statik sayfalarda kaydırma için de geçerlidir. Bu serinin bir sonraki makalesinde, bu kavramları göz önünde bulundurarak kaydırma performansını ayrıntılı olarak inceleyeceğiz.

İyi animasyonlar dileriz.

Referanslar