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

Tom Wiltzius
Tom Wiltzius

Giriş

Animasyonlar, geçişler ve diğer küçük kullanıcı arayüzü efektlerini yaparken web uygulamanızın duyarlı ve düzgün çalışmasını istersiniz. Bu efektlerin sorunsuz olmasını sağlamak, "doğal" bir his ile beceriksiz ve cilasız bir efekt arasındaki farkı belirleyebilir.

Bu, tarayıcıda oluşturma performansı optimizasyonuyla ilgili bir dizi makalenin ilkidir. Başlangıç olarak, akıcı animasyonun neden zor olduğunu ve bunu başarmak için neler gerektiğini ele alacağız. Ayrıca, basit birkaç en iyi uygulamayı da ele alacağız. Bu fikirlerin birçoğu ilk olarak, Nat Duca ile birlikte bu yılki Google I/O konuşmasında (video) "Jank Busters" adlı bir konuşmada sunuldu.

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ı göz önünde bulundurun: Düzenli aralıklarla, genellikle (ancak 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 uygulamasını belirtir. Bunu, ekran arabelleğine veri yazan işlem ile ekrana yerleştirmek için bu verileri okuyan işletim sistemi arasındaki bir yarış durumu gibi düşünebilirsiniz. Arabelleğe alınan çerçeve içeriklerinin bu yenilemeler arasında değil, bu yenilemeler arasında değişmesini isteriz. Aksi takdirde, monitör bir karenin yarısını diğerinin yarısını gösterir ve bu da "yırtılma"ya neden olur.

Animasyonların akıcı olması için, ekran her yenilendiğinde hazır olacak yeni bir kareye ihtiyacınız vardır. Bunun iki önemli etkisi vardır: Kare zamanlaması (karenin hazır olması gerektiğinde) ve çerçeve bütçesi (tarayıcının ne kadar sürede bir kare üretmesi gerektiği). Bir kareyi tamamlamak için sadece ekran yenilemeleri arasında zamanınız vardır (60 Hz ekranda yaklaşık 16 ms) ve bir sonraki kare ekrana yerleştirilir yerleştirilmez sonraki kareyi oluşturmaya 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 nedenlerden dolayı (bir dakika içinde daha ayrıntılı konuşacağız) ancak özellikle endişe verici bir konu olan bir sorun:

  • JavaScript'ten zamanlayıcı çözümlemesi, yalnızca birkaç milisaniye sürer
  • Farklı cihazların farklı yenileme hızları vardır

Yukarıda bahsedilen kare zamanlama sorununu hatırlayın: Bir sonraki ekran yenilenmeden önce hazır olmanız için tamamlanmış bir animasyon karesine ihtiyacınız vardır. Bu animasyon karesinin herhangi bir JavaScript, DOM işleme, düzen, boyama vb. ile tamamlanmış olması gerekir. Düşük zamanlayıcı çözünürlüğü, bir sonraki ekran yenilemesinden önce animasyon karelerinin tamamlanmasını zorlaştırabilir, ancak ekran yenileme hızlarının değiştirilmesi sabit bir zamanlayıcı ile bunu imkansız hale getirir. Zamanlayıcı aralığı ne olursa olsun, bir kare için zamanlama aralığından yavaşça kayarak bir kare bırakırsınız. Bu durum, zamanlayıcı milisaniyelik doğrulukla etkinleşse bile meydana gelmez (geliştiricilerin keşfettiği gibi). Zamanlayıcı çözünürlüğü, makinenin pilde mi yoksa takılı mı olduğuna bağlı olarak değişir, arka plan sekmelerinin yoğunlaşmadan kaynaklanmasından etkilenebilir. Bu durum nadir olsa bile (örneğin, her 16 karede bir, bir milisaniyelik hata atladığınız için) şunu fark edeceksiniz: saniyede birkaç kare atlar. Aynı zamanda hiç görüntülenmeyen kareler oluşturmak için de iş yapacaksınız. Bu da uygulamanızda başka şeylerle uğraşmak için harcayabileceğiniz güç ve CPU zamanı israfına yol açıyor.

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

Oluşturma performansını ele alırken genellikle saniyedeki kare sayısına (FPS) odaklanırız. Ancak sapma daha da büyük bir sorun olabilir. Gözlerimiz, kötü zamanlanmış bir animasyonda ortaya çıkabilecek 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 çerçevesi istersiniz. Tarayıcı yakında yeni bir kare oluşturacağında geri çağırma işleminiz çağrılır. Bu işlem, yenileme hızı ne olursa olsun gerçekleşir.

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

  • 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ı daraltabilir ve geri çağırma işlemini daha seyrek (örneğin, 60 Hz'lik bir ekranda saniyede 30 kez) yapabilir. Bu yöntem, kare hızını yarıya düşürse de animasyonun tutarlılığını korur. Yukarıda belirtildiği gibi, gözlerimiz kare hızından çok sapmaya daha duyarlıdır. Sabit bir 30 Hz, saniyede birkaç kare atlayan 60 Hz'den daha iyi görünür.

requestAnimationFrame zaten her yerde tartışılmış olduğundan, daha fazla bilgi için bu reklam öğesi JS'deki gibi makalelere göz atabilirsiniz. Ancak bu, animasyonu sorunsuz hale getirmek için önemli bir ilk adımdır.

Çerçeve Bütçesi

Her ekran yenilemesinde yeni bir karenin hazır olmasını istediğimizden, yeni bir kare oluşturmak üzere tüm işi yapmak için yenilemeler arasında yalnızca zaman kalır. 60 Hz ekranda, tüm JavaScript'i çalıştırmak, düzeni gerçekleştirmek, boyamak ve tarayıcının çerçeveyi çıkarmak için yapması gereken diğer her şeyi tamamlamak için yaklaşık 16 ms süremiz olduğu anlamına gelir. Bu, requestAnimationFrame geri çağırmanızdaki JavaScript'in çalışması 16 ms'den uzun sürerse v-sync için zamanda bir kare üretmeyi umut etmediğiniz anlamına gelir!

16 ms, uzun bir süre değildir. Neyse ki Chrome'un Geliştirici Araçları, requestAnimationFrame geri çağırma işlemi sırasında çerçeve bütçenizi tüketip tüketmediğinizi takip etmenize yardımcı olabilir.

Geliştirici Araçları zaman çizelgesinin açılması ve bu animasyonu uygulamalı olarak kaydetmesi, animasyon oluştururken bütçeyi oldukça aştığımızı gösteriyor. Zaman Çizelgesi'nde "Kareler"e geçip göz atın:

Çok fazla düzene sahip bir demo
Çok fazla düzene sahip bir demo

Bu requestAnimationFrame (rAF) geri çağırma işlemleri 200 ms'den uzun sürüyor. Bu değer, 16 ms'de bir kareyi işaretlemek için çok uzun bir aralıktır. Bu uzun RAF geri aramalarından birinin açılması, içeride neler olup bittiğini ortaya çıkarır: Bu örnekte, çok fazla düzen.

Polat'ın videosunda, geçişin belirli nedeni (scrollTop başlıklı) ve bundan nasıl kaçınılacağı hakkında daha ayrıntılı bilgiler veriliyor. Ancak burada önemli olan, geri çağırmaya derinlemesine dalabilmeniz ve nelerin bu kadar uzun sürdüğünü araştırabilmenizdir.

Çok daha sade bir düzene sahip güncellenmiş bir demo
Çok daha azaltılmış düzene sahip güncellenmiş bir demo

16 ms. kare sürelerine dikkat edin. Çerçevelerdeki bu boş alan, daha fazla çalışmanız (veya tarayıcının arka planda yapması gerekeni yapmasına izin vermeniz) gereken olanağı sağlar. Bu boş alan iyi bir şey.

Jank'ın Diğer Kaynağı

JavaScript destekli animasyonlar çalıştırmaya çalışırken yaşanan en büyük sorun, başka şeylerin rAF geri çağırmanıza engel olabilmesi ve hatta çalışmasının engellenebilmesidir. rAF geri çağırmanız verimli olsa 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 bir zamanlayıcıda programlanmış güncellemeler çalıştırma gibi) aniden gelip herhangi bir sonuç vermeden çalıştırılabilir. Mobil cihazlarda bazen bu etkinliklerin işlenmesi yüzlerce milisaniye sürebilir. Bu sırada animasyonunuz tamamen durur. Bu animasyon kesintilerine jank diyoruz.

Bu durumlardan kaçınmak için sihirli bir madde yoktur, ancak mimari açıdan başarıya ulaşmak için yararlanabileceğiniz birkaç en iyi uygulama vardır:

  • Giriş işleyicilerde çok fazla işlem yapmayın. Çok sayıda JS yapmak veya (ör. bir kaydırma işleyicisi) sırasında sayfanın tamamını yeniden düzenlemeye çalışmak, çok kötü donukluğun çok yaygın bir nedenidir.
  • rAF geri çağırmanıza veya Web Workers'a mümkün olduğunca fazla işlem gönderin (çalışması uzun sürecek her şeyi okuyun).
  • Çalışmanızı rAF geri çağırma (ARAF) geri çağırma işlemine aktarırsanız, her kareyi yalnızca küçük bir kısmını işleyecek veya önemli bir animasyon tamamlanana kadar geciktirecek şekilde parçalamaya çalışın.

İşlemenin giriş işleyicileri yerine requestAnimationFrame geri çağırmalarına nasıl aktarılacağını ele alan harika bir eğitim için Paul Lewis'in Leaner, Meaner, Faster Animations with requestAnimationFrame başlıklı makalesine bakın.

CSS Animasyonu

Etkinliğinizde ve rAF geri aramalarında basit JS'den daha iyi olan nedir? JS yok.

Daha önce, rAF geri aramalarınızı kesintiye uğratmamak için sihirli bir değnek olmadığını söylemiştik. Ancak bunları tamamen ortadan kaldırmak için CSS animasyonu kullanabilirsiniz. Özellikle Android için Chrome'da (ve diğer tarayıcılar benzer özellikler üzerinde çalışmaya devam ederken) CSS animasyonları, JavaScript çalışıyor olsa bile tarayıcının genellikle bunları çalıştırabilmesini sağlayan çok istenen özelliğe sahiptir.

Yukarıdaki bölümde jank ile ilgili örtülü bir ifade bulunmaktadır: Tarayıcılar aynı anda yalnızca bir işlem yapabilir. Bu kesin olmamakla birlikte, şu durumlarda işe yarar bir varsayımdır: Tarayıcı herhangi bir zamanda JS çalıştırabilir, düzen yapabilir veya boyama yapabilir, ancak bunları aynı anda yalnızca bir tane gerçekleştirebilir. Bunu, Geliştirici Araçları'nın Zaman Çizelgesi görünümünde doğrulayabilirsiniz. Bu kuralın istisnalarından biri, Android için Chrome'da (ve yakında masaüstü Chrome'da henüz olmasa da) 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 süreyle çalışır ve bu da duraklamaya neden olur. Ancak bunun yerine bu animasyonu CSS animasyonlarıyla yönlendirirsek olumsuzluk artık oluşmaz.

(Bu yazı yazılırken, CSS animasyonunun masaüstü Chrome'da değil, yalnızca Android için Chrome'da sorunsuz olduğunu 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 ile ilgili bunun gibi makalelere bakın.

Özet

Kısacası:

  1. Animasyon yaparken her ekran yenilemesi için çerçeve oluşturmak önemlidir. Vsync'in animasyonu, uygulamanın tarzı üzerinde çok olumlu bir etki yaratır.
  2. Chrome'da ve diğer modern tarayıcılarda vsync'in animasyonunu almanın en iyi yolu CSS animasyonunu kullanmaktır. CSS animasyonunun sağladığından daha fazla esnekliğe ihtiyaç duyduğunuzda, en iyi teknik requestAnimationFrame tabanlı animasyondur.
  3. rAF animasyonlarını sağlıklı ve mutlu tutmak için diğer etkinlik işleyicilerin rAF geri çağırma işlemini engellemediğinden emin olun ve rAF geri çağırma işlemlerini kısa (<15 ms) tutun.

Son olarak, vsync'in animasyonu 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 dizinin bir sonraki makalesinde, bu kavramları göz önünde bulundurarak kaydırma performansını inceleyeceğiz.

İyi animasyonlar!

Referanslar