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ı istersiniz. Bu efektlerin takılma olmadan çalışması, "yerel" bir his ile hantal ve kaba bir his arasındaki farkı belirleyebilir.

Bu, tarayıcıda oluşturma performansı optimizasyonunu ele alan bir dizi makalenin ilkidir. Öncelikle, akıcı animasyonun neden zor olduğunu ve bunu başarmak için neler yapılması gerektiğini ele alacağız. Ayrıca, birkaç kolay en iyi uygulamadan bahsedeceğiz. Bu fikirlerin çoğu, Nat Duca ile birlikte bu yıl Google I/O'da yaptığımız "Jank Busters" (video) adlı konuşmada sunulmuştu.

V-sync ile tanışın

PC oyuncuları bu terime aşina olabilir ancak web'de yaygın değildir: V-sync nedir?

Telefonunuzun ekranını düşünün: Düzenli aralıklarla, genellikle (her zaman değil) saniyede yaklaşık 60 kez yenilenir. V-sync (veya dikey senkronizasyon), yalnızca ekran yenilemeleri arasında yeni kareler oluşturma işlemidir. Bu durumu, verileri ekran arabelleğine yazan işlem ile bu verileri ekrana yansıtmak için okuyan işletim sistemi arasındaki bir yarışma durumu olarak düşünebilirsiniz. Arabelleğe alınan kare içeriklerinin bu yenilemeler sırasında değil, yenilemeler arasında değişmesini isteriz. Aksi takdirde monitör, bir karenin yarısını ve başka bir karenin yarısını göstererek "yırtılmaya" neden olur.

Sorunsuz bir animasyon elde etmek için her ekran yenileme işleminde yeni bir karenin hazır olması gerekir. Bunun iki önemli sonucu vardır: kare zamanlaması (yani karenin ne zamana kadar hazır olması gerektiği) ve kare bütçesi (yani tarayıcının bir kare üretmek için ne kadar süresi olduğu). Bir kareyi tamamlamak için yalnızca ekran yenilemeleri arasındaki süreye (~60 Hz ekranda 16 ms) sahipsiniz ve son kare ekrana yerleştirilir yerleştirilmez bir sonraki kareyi üretmeye başlamak istiyorsunuz.

Zamanlama her şeydir: requestAnimationFrame

Birçok web geliştiricisi, animasyon oluşturmak için her 16 milisaniyede bir setInterval veya setTimeout kullanır. Bu durum çeşitli nedenlerden dolayı sorun teşkil eder (birazdan daha ayrıntılı olarak ele alacağız) ancak özellikle endişe verici olanlar şunlardır:

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

Yukarıda bahsedilen kare zamanlaması sorununu hatırlayın: Bir sonraki ekran yenileme gerçekleşmeden önce JavaScript, DOM manipülasyonu, 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 yenilemeden önce tamamlanmasını zorlaştırabilir ancak ekran yenileme hızlarındaki farklılık, sabit bir zamanlayıcıyla bunu imkansız hale getirir. Zamanlayıcı aralığı ne olursa olsun, bir karenin zamanlama aralığından yavaşça çıkarsınız ve sonunda bir kareyi atlarsınız. Bu durum, zamanlayıcı milisaniye hassasiyetinde çalışsa bile gerçekleşir. Ancak zamanlayıcı bu hassasiyette çalışmaz (geliştiricilerin keşfettiği gibi). Zamanlayıcı çözünürlüğü, makinenin pille mi yoksa prize takılı olarak mı çalıştığına bağlı olarak değişir ve arka plan sekmelerinin kaynakları tüketmesinden etkilenebilir. Bu durum nadir olsa bile (ör. bir milisaniye farkla her 16 karede bir) farkı görürsünüz: Saniyede birkaç kare atlarsınız. Ayrıca, hiçbir zaman gösterilmeyen kareler oluşturmak için de çalışırsınız. Bu da uygulamanızda başka şeyler yapmak için harcayabileceğiniz gücü ve CPU süresini boşa harcar.

Farklı ekranların farklı yenileme hızları vardır: 60 Hz yaygındır ancak bazı telefonlar 59 Hz'dir, bazı dizüstü bilgisayarlar düşük güç modunda 50 Hz'e düşer, bazı masaüstü monitörler 70 Hz'dir.

Oluşturma performansını ele alırken saniyedeki kare sayısına (FPS) odaklanma eğilimindeyiz ancak varyans daha da büyük bir sorun olabilir. Gözlerimiz, kötü zamanlanmış bir animasyonun üretebileceği küçük, düzensiz aksaklıkları 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ı yeni bir kare oluşturmak üzereyken geri çağırma işleviniz çağrılır. Bu durum, yenileme hızından bağımsız olarak gerçekleşir.

requestAnimationFrame başka güzel özelliklere de sahiptir:

  • Arka plan sekmelerindeki animasyonlar duraklatılarak sistem kaynakları ve pil ömrü korunur.
  • Sistem, ekranın yenileme hızında oluşturma işlemini gerçekleştiremiyorsa animasyonları kısıtlayabilir ve geri çağırmayı daha az sıklıkta (ör. 60 Hz ekranda saniyede 30 kez) üretebilir. Bu işlem kare hızını yarıya indirse de animasyonun tutarlı kalmasını sağlar. Yukarıda belirtildiği gibi, gözlerimiz kare hızından çok varyansa uyum sağlar. Sabit bir 30 Hz, saniyede birkaç kare kaçıran 60 Hz'den daha iyi görünür.

requestAnimationFrame her yerde tartışılan bir konu olduğundan, bu konuda daha fazla bilgi için creative JS'nin bu makalesi gibi makalelere bakın. Ancak bu, sorunsuz animasyon için önemli bir ilk adımdır.

Kare bütçesi

Her ekran yenilemesinde yeni bir karenin hazır olmasını istediğimizden, yeni bir kare oluşturmak için gereken tüm işlemleri yapmak için yalnızca yenilemeler arasındaki süre vardır. 60 Hz'lik bir ekranda bu, tüm JavaScript'i çalıştırmak, düzeni, boyamayı ve tarayıcının kareyi çıkarmak için yapması gereken diğer her şeyi yapmak için yaklaşık 16 ms'miz olduğu anlamına gelir. Bu, requestAnimationFrame geri çağırma işlevinizdeki JavaScript'in çalışması 16 ms'den uzun sürerse dikey senkronizasyon için zamanında kare oluşturma şansınız olmadığı anlamına gelir.

16 milisaniye çok kısa bir süre. Neyse ki Chrome'un Geliştirici Araçları, requestAnimationFrame geri çağırma sırasında kare bütçenizi aşıp aşmadığınızı takip etmenize yardımcı olabilir.

Geliştirici Araçları zaman çizelgesini açıp bu animasyonun kaydını aldığımızda, animasyon oluştururken bütçeyi fazlasıyla aştığımızı hemen görebiliriz. Zaman çizelgesinde "Kareler"e geçip şunlara 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ğırmaları 200 ms'den uzun sürüyor. Bu, her 16 milisaniyede bir kare oluşturmak için çok uzun bir süre. Bu uzun rAF geri çağırmalarından birini açtığınızda, içeride neler olup bittiğini görebilirsiniz. Bu örnekte, çok sayıda düzen işlemi yapılıyor.

Paul'un videosunda, yeniden düzenlemenin belirli nedeni (scrollTop okunuyor) ve bundan nasıl kaçınılacağı hakkında daha fazla bilgi verilmektedir. Ancak burada önemli olan, geri çağırmayı inceleyip işlemin neden bu kadar uzun sürdüğünü araştırabilmenizdir.

Çok daha küçük bir düzene sahip güncellenmiş bir demo
Çok daha küçük bir düzene sahip güncellenmiş bir demo

16 ms'lik kare sürelerine dikkat edin. Karelerdeki bu boş alan, daha fazla iş yapmak (veya tarayıcının arka planda yapması gereken işleri yapmasına izin vermek) için sahip olduğunuz boşluktur. Bu boşluk iyi bir şeydir.

Diğer takılma kaynakları

JavaScript destekli animasyonları çalıştırmaya çalışırken yaşanan en büyük sorun, diğer işlemlerin rAF geri çağırma işlevinizi engelleyebilmesi ve hatta çalışmasını tamamen durdurabilmesidir. rAF geri çağırmanız yalın olsa ve yalnızca birkaç milisaniye içinde ç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 çalışabilir. Mobil cihazlarda bu etkinliklerin işlenmesi bazen yüzlerce milisaniye sürebilir ve bu süre boyunca animasyonunuz tamamen durur. Bu animasyon aksaklıklarına takılma diyoruz.

Bu durumları önlemenin sihirli bir yolu yoktur ancak başarıya ulaşmak için uygulayabileceğiniz birkaç mimari en iyi uygulama vardır:

  • Giriş işleyicilerde çok fazla işlem yapmayın. Örneğin, bir onscroll işleyicisi sırasında çok fazla JS yapmak veya tüm sayfayı yeniden düzenlemeye çalışmak, çok kötü bir takılmanın yaygın bir nedenidir.
  • Mümkün olduğunca fazla işlemi (çalışması uzun sürecek her şeyi) rAF geri çağırma işlevinize veya Web Çalışanları'na aktarın.
  • rAF geri çağırmasına iş gönderirseniz her karede yalnızca küçük bir miktar işleyecek şekilde parçalara ayırmayı veya önemli bir animasyon bitene kadar geciktirmeyi deneyin. Bu sayede kısa rAF geri çağırmaları çalıştırmaya ve sorunsuz bir şekilde animasyon oluşturmaya devam edebilirsiniz.

İşlemleri giriş işleyiciler yerine requestAnimationFrame geri çağırmalarına aktarma hakkında mükemmel bir eğitim için Paul Lewis'in requestAnimationFrame ile Daha Yalın, Daha Güçlü, Daha Hızlı Animasyonlar başlıklı makalesine göz atın.

CSS animasyonu

Etkinlik ve rAF geri çağırmalarınızda hafif JS'den daha iyi ne olabilir? JS yok.

Daha önce rAF geri çağırmalarınızın kesintiye uğramasını önlemenin kesin bir yolu olmadığını söylemiştik ancak CSS animasyonunu kullanarak rAF geri çağırmalarına olan ihtiyacı tamamen ortadan kaldırabilirsiniz. Özellikle Android için Chrome'da (ve diğer tarayıcılar benzer özellikler üzerinde çalışıyor), CSS animasyonları, JavaScript çalışıyor olsa bile tarayıcının genellikle bunları çalıştırabileceği çok istenen bir özelliğe sahiptir.

Yukarıdaki takılma bölümünde örtük bir ifade vardır: Tarayıcılar aynı anda yalnızca bir işlem yapabilir. Bu tam olarak doğru olmasa da iyi bir çalışma varsayımıdır: Tarayıcı herhangi bir zamanda JS çalıştırabilir, düzen oluşturabilir veya boyama yapabilir ancak aynı anda yalnızca birini 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 CSS animasyonlarıdır (ve yakında masaüstü Chrome'da da geçerli olacaktır).

Mümkün olduğunda CSS animasyonu kullanmak hem uygulamanızı basitleştirir hem de JavaScript çalışırken bile animasyonların sorunsuz ç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ılmaya neden olur. Ancak bu animasyonu CSS animasyonlarıyla çalıştırırsak takılma artık gerçekleşmez.

(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 makale gibi makaleleri inceleyin.

Özet

Kısaca özetlemek gerekirse:

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

Son olarak, dikey senkronizasyonlu animasyon yalnızca basit kullanıcı arayüzü animasyonları için geçerli değildir. 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ı inceleyeceğiz.

İyi animasyonlar!

Referanslar