requestAnimationFrame(callback), tarayıcıya bir animasyon yapmak istediğinizi söyler ve verdiğiniz fonksiyonu bir sonraki ekran boyamasından hemen önce çağırmasını ister. Çağrı tek seferliktir: animasyonu sürdürmek için fonksiyonunuzun içinde requestAnimationFrame çağrısını tekrar yapmanız gerekir. Geri çağırmaya gelen zaman damgasını kullanarak her karede ne kadar ilerleyeceğinizi hesaplarsanız animasyonunuz 60 Hz ekranda da 144 Hz ekranda da aynı hızda akar.

Bu yazıda döngünün temel kalıbını, zaman damgasının tam anlamını, cancelAnimationFrame ile durdurmayı, setInterval ile farkları, arka plan sekmelerindeki davranışı, worker içinde kullanımı ve basit geçişler için Element.animate() alternatifini göreceksiniz. Tarayıcıya özgü kodlar MDN örneklerinden uyarlandı; hız karşılaştırmasını gösteren saf hesaplama örneği Node.js ile çalıştırıldı.

Temel döngü

MDN'nin örneği bir elemanı 2 saniye boyunca sağa kaydırır. Eleman milisaniye başına 0.1 piksel hızla gider, bu yüzden konum 0.1 * gecen ile hesaplanır ve 2000 milisaniyenin sonunda 200 piksele ulaşır. Math.min elemanın tam 200 pikselde durmasını sağlar:

javascript
const element = document.getElementById("kutu");
let start;

function step(timestamp) {
  if (start === undefined) {
    start = timestamp;
  }
  const elapsed = timestamp - start;
  const shift = Math.min(0.1 * elapsed, 200);
  element.style.transform = `translateX(${shift}px)`;

  if (shift < 200) {
    requestAnimationFrame(step); // bir sonraki kareyi iste
  }
}

requestAnimationFrame(step);
html
<div id="kutu" style="width: 50px; height: 50px; background: steelblue;"></div>

Akış şöyledir: ilk çağrı başlangıç zamanını kaydeder, sonraki her çağrı geçen süreyi hesaplar, konumu günceller ve hedefe ulaşılmadıysa yeni bir kare ister. Hedefe ulaşıldığında yeni istek yapılmaz ve döngü kendiliğinden biter. Satır içi stil vermenin ayrıntıları style ve classList yazısında anlatılıyor.

Zaman damgası tam olarak nedir?

Zaman damgası tam olarak nedir? Pencerelerde bu değer document.timeline.currentTime ile aynıdır., Aynı ajan üzerinde çalışan bütün pencereler (aynı kökenli pencereler ve aynı...

Geri çağırmaya tek bir argüman gelir: timestamp. Bu bir DOMHighResTimeStamp değeridir ve önceki karenin çiziminin bittiği zamanı, zaman başlangıcından (time origin) bu yana geçen milisaniye olarak verir. Ondalıklı bir sayıdır, en az 1 milisaniye hassasiyet taşır. MDN birkaç önemli ayrıntı daha verir:

  • Pencerelerde bu değer document.timeline.currentTime ile aynıdır.
  • Aynı ajan üzerinde çalışan bütün pencereler (aynı kökenli pencereler ve aynı kökenli iframe'ler) aynı zaman damgasını paylaşır; bu, farklı geri çağırmalardaki animasyonları senkronize etmeyi mümkün kılar.
  • Değer, geri çağırmanın başında performance.now() çağırmaya benzer ama asla aynı değer değildir.
  • Aynı karede birden fazla geri çağırma çalışırsa hepsi aynı zaman damgasını alır; öncekilerin harcadığı süre buna yansımaz.

Zaman damgası önceki karenin sonunu gösterdiği için, hesapladığınız yeni değer en erken bir sonraki karede ekrana yansır. Dış bir saate, örneğin ses zamanına senkronize olmak istiyorsanız ulaşabileceğiniz en yüksek hassasiyet bir kare süresidir; 60 Hz'de bu 16.67 milisaniyedir. performance.now() ve diğer ölçüm araçları Performance API yazısında anlatılıyor.

Neden zaman damgası kullanmalısınız?

Geri çağırmanın çağrılma sıklığı genellikle ekranın yenileme hızına uyar. MDN en yaygın hızın 60 Hz olduğunu, ama 75, 120 ve 144 Hz'in de yaygın kullanıldığını belirtir ve açık bir uyarı yapar: bir karede ne kadar ilerleneceğini ilk argümanla (ya da başka bir zaman kaynağıyla) hesaplamazsanız animasyon yüksek yenileme hızlı ekranlarda daha hızlı çalışır. Aşağıdaki hesap bu farkı somut gösterir. Kod tarayıcıdan bağımsız saf JavaScript olduğu için Node.js ile çalıştırıldı:

javascript
// Konumu geçen süreden hesaplayan adım fonksiyonu (MDN örneğindeki formül)
const konum = (gecenMs) => Math.min(0.1 * gecenMs, 200);

// Ekran yenileme hızını taklit eden küçük bir deneme
function dene(hz, kareSayisi) {
  const gecenMs = (kareSayisi * 1000) / hz;
  const zamanaGore = konum(gecenMs);
  const sabitAdimla = kareSayisi * 2; // her karede 2px eklemek
  return `${hz} Hz, ${gecenMs} ms: süreye göre ${zamanaGore}px, sabit adımla ${sabitAdimla}px`;
}

console.log(dene(60, 60));   // 60 Hz, 1000 ms: süreye göre 100px, sabit adımla 120px
console.log(dene(120, 120)); // 120 Hz, 1000 ms: süreye göre 100px, sabit adımla 240px
console.log(dene(144, 288)); // 144 Hz, 2000 ms: süreye göre 200px, sabit adımla 576px

Aynı bir saniyede süreye göre hesaplanan konum her ekranda 100 pikseldir; her karede sabit 2 piksel ekleyen yöntem ise 120 Hz ekranda iki kat yol alır. Bu yüzden konumu, hızı ya da opaklığı her zaman geçen süreden türetin.

Başlangıç noktasını belirlemenin üç yolu

MDN, her karedeki ilerlemenin temel alındığı "sıfır" anını belirlemek için üç yaklaşım gösterir:

YaklaşımNasılNe zaman
İlk geri çağırmada sıfırlamakzero = ilk timestampÖnerilen; bazı tarayıcılarda ilk istek ile ilk çağrı arasında birkaç kare gecikme olabilir
document.timeline.currentTimeİstekten önce zero = document.timeline.currentTimetimestamp ile aynı saatte olduğu için sıfırıncı karenin zaman damgasına eşdeğer
performance.now()zero = performance.now(), karede yeniden ölçmekBiraz daha hassasiyet için; ama geri çağırmalar arası güvenilir senkronizasyon sağlamaz
javascript
const duration = 1000;
let zero;

requestAnimationFrame(firstFrame);

function firstFrame(timestamp) {
  zero = timestamp;
  animate(timestamp);
}

function animate(timestamp) {
  const value = (timestamp - zero) / duration;
  if (value < 1) {
    element.style.opacity = value;
    requestAnimationFrame((t) => animate(t));
  } else {
    element.style.opacity = 1;
  }
}

Bu kalıp bir saniyelik doğrusal bir belirme efekti üretir. Animasyonunuz başladığı anda yeni bir değere sıçrıyorsa MDN bu yapıyı kullanmanızı özellikle önerir. Diğer iki yaklaşımda sıfır anı istekten önce belirlendiği için ilk karede değer sıfırdan biraz büyük olabilir.

cancelAnimationFrame ile durdurmak

requestAnimationFrame bir istek kimliği (request ID) döndürür. Bu değeri cancelAnimationFrame(id) fonksiyonuna verdiğinizde henüz çalışmamış geri çağırma iptal edilir. Döngülerde en sık yapılan hata, kimliği yalnızca ilk istekte saklamaktır; MDN örneği her yeni istekte kimliği güncellemenin önemini yorum satırıyla vurgular, çünkü iptal son kimlikle yapılır:

javascript
let istekId = null;

function kare(timestamp) {
  ciz(timestamp);
  istekId = requestAnimationFrame(kare); // her seferinde güncelle
}

function baslat() {
  if (istekId === null) istekId = requestAnimationFrame(kare);
}

function durdur() {
  if (istekId !== null) {
    cancelAnimationFrame(istekId);
    istekId = null;
  }
}

setInterval ile farkı

setInterval ile farkı: Çağrılma zamanı, Sıklık, Zaman bilgisi, Tekrar

MDN'nin canvas eğitimi animasyonu zaman içinde çalıştırmanın üç yolunu sayar: setInterval(), setTimeout() ve requestAnimationFrame(). Eğitim örneklerinde requestAnimationFrame kullanılır, çünkü kareyi sistem boyamaya hazır olduğunda çağırarak daha akıcı ve verimli bir animasyon sağlar. Canvas optimizasyon sayfası da animasyonlarda setInterval() yerine bu yöntemi önerir.

ÖzelliksetIntervalrequestAnimationFrame
Çağrılma zamanıBelirlediğiniz gecikmeyleBir sonraki boyamadan önce
SıklıkSabit gecikmeGenelde ekran yenileme hızına uyar
Zaman bilgisiYok, kendiniz ölçersinizGeri çağırmaya timestamp gelir
TekrarOtomatikTek seferlik, her karede yeniden istenir

Arka plan davranışı önemlidir: MDN, requestAnimationFrame() çağrılarının performans ve pil ömrü için çoğu tarayıcıda arka plan sekmelerinde ve gizli iframe'lerde duraklatıldığını belirtir. Sekmeye dönüldüğünde gelen ilk zaman damgası, önceki kareden çok sonrasını gösterebilir. Süreye dayalı hesaplarınız bu büyük sıçramayı karşılamalıdır; Math.min ile sınırlamak ya da sekme gizlendiğinde animasyonu duraklatmak iki yaygın çözümdür. Sayfanın gizlenip gösterildiğini visibilitychange olayıyla anlayabilirsiniz. Zamanlayıcıların olay döngüsündeki yeri olay döngüsü yazısında anlatılıyor.

Canvas animasyonu: temizle, çiz, tekrar iste

Canvas üzerinde bir şey çizildiğinde öyle kalır; hareket ettirmek için her karede yeniden çizmeniz gerekir. MDN'nin eğitimi bir kareyi dört adımda anlatır: canvas'ı temizle, gerekiyorsa durumu kaydet, şekilleri çiz, kaydettiysen durumu geri yükle. Gelişmiş animasyon sayfasındaki zıplayan top örneği clearRect() ile temizler, topu çizer, hız vektörünü konuma ekler ve kenara çarpınca hızın yönünü çevirir. Fare canvas'tan çıkınca cancelAnimationFrame ile döngü durur:

javascript
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let raf;

const ball = { x: 100, y: 100, vx: 5, vy: 2, radius: 25 };

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.beginPath();
  ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2, true);
  ctx.fillStyle = "blue";
  ctx.fill();

  ball.x += ball.vx;
  ball.y += ball.vy;
  if (ball.y + ball.vy > canvas.height - ball.radius || ball.y + ball.vy < ball.radius) {
    ball.vy = -ball.vy;
  }
  if (ball.x + ball.vx > canvas.width - ball.radius || ball.x + ball.vx < ball.radius) {
    ball.vx = -ball.vx;
  }
  raf = requestAnimationFrame(draw);
}

canvas.addEventListener("mouseover", () => {
  raf = requestAnimationFrame(draw);
});
canvas.addEventListener("mouseout", () => {
  cancelAnimationFrame(raf);
});
html
<canvas id="canvas" width="600" height="300"></canvas>

Bu örnek, eğitimdeki haliyle her karede sabit bir hız ekler; yukarıda gördüğünüz gibi bu, yüksek yenileme hızlı ekranda topun daha hızlı hareket etmesine yol açar. Gerçek bir projede vx ve vy değerlerini saniye başına piksel olarak tanımlayıp iki kare arasındaki süreyle çarpmak daha doğrudur; MDN'nin worker örneği tam olarak bu "delta time" yaklaşımını kullanır. clearRect yerine yarı saydam bir fillRect kullanmak ise eğitimde gösterilen iz bırakma efektini üretir. Çizim komutlarının kendisi Canvas çizim rehberinde anlatılıyor.

Delta time: iki kare arasındaki süreyle hareket

Konumu başlangıçtan geçen toplam süreyle hesaplamak, başı ve sonu belli animasyonlar için idealdir. Oyunlarda ya da sonu olmayan hareketlerde ise her karede iki kare arasında geçen süreyi (delta time) bulup hızı bununla çarpmak daha pratiktir. MDN'nin worker örneği bu yöntemi kullanır: önceki zaman damgası saklanır, fark saniyeye çevrilir ve kare saniyede 50 piksel ilerletilir. Aynı mantık ana iş parçacığında şöyle yazılır:

javascript
const ctx = document.querySelector("canvas").getContext("2d");
let pos = 0;
let lastTime = 0;

function draw(currentTime) {
  if (lastTime === 0) lastTime = currentTime;
  const deltaTime = (currentTime - lastTime) / 1000; // saniye
  lastTime = currentTime;

  ctx.clearRect(0, 0, 100, 100);
  ctx.fillRect(pos, 0, 10, 10);
  pos += 50 * deltaTime; // saniyede 50 piksel
  if (pos > 100) pos = -10;

  requestAnimationFrame(draw);
}

requestAnimationFrame(draw);

İlk karede lastTime sıfır olduğu için fark sıfır kabul edilir ve kare ilk çağrıda yerinden oynamaz. Hız artık "kare başına" değil "saniye başına" tanımlandığı için ekranın yenileme hızı değişse de kare aynı sürede aynı yolu alır.

Worker içinde requestAnimationFrame

requestAnimationFrame() adanmış worker'larda da (DedicatedWorkerGlobalScope) vardır. MDN'nin koşulu şudur: worker'ın bir sahip window nesnesine bağlı olması gerekir; yani worker bir pencere tarafından ya da kendisi pencereye bağlı olan başka bir adanmış worker tarafından oluşturulmuş olmalıdır. Desteklenmeyen bir worker'da çağrı NotSupportedError fırlatır. İptal işlemi de isteği yapan worker içinde yapılmalıdır. Tipik kullanım, ana iş parçacığından transferControlToOffscreen() ile aktarılan bir OffscreenCanvas üzerinde çizim yapmaktır. Worker kurulumu ve mesajlaşma Web Workers yazısında anlatılıyor.

Basit geçişler için Element.animate()

Her animasyon için kare döngüsü yazmanız gerekmez. Element.animate(keyframes, options), Web Animations API'nin kısayoludur: yeni bir Animation oluşturur, elemana uygular, oynatır ve nesneyi döndürür. options bir süre sayısı (milisaniye) ya da duration, iterations gibi zamanlama alanları içeren bir nesne olabilir. MDN'nin örneği bir elemanı tıklanınca döndürüp küçültür:

javascript
const donus = [
  { transform: "rotate(0) scale(1)" },
  { transform: "rotate(360deg) scale(0)" },
];

const kutu = document.querySelector(".newspaper");
kutu.addEventListener("click", () => {
  const anim = kutu.animate(donus, { duration: 2000, iterations: 1 });
  anim.finished.then(() => console.log("bitti"));
});

Dönen Animation nesnesinin pause(), play(), reverse(), cancel() ve finish() yöntemleri, playbackRate özelliği ve animasyon bittiğinde çözülen finished sözü (promise) vardır. Tek bir anahtar kare verirseniz, MDN'ye göre bu varsayılan olarak bitiş durumu sayılır ve başlangıç durumu elemanın o anki hesaplanmış stilinden çıkarılır. Animation nesnesi üç olay da yayar: finish oynatma bittiğinde, cancel cancel() çağrıldığında veya animasyon başka bir durumdan boşta (idle) durumuna geçtiğinde, remove ise tarayıcı animasyonu yerini başka bir animasyon aldığı için otomatik kaldırdığında tetiklenir. Bir elemana uygulanan bütün animasyonların listesini Element.getAnimations() verir. Kural basittir: belirli başlangıç ve bitiş değerleri olan geçişler için animate(), fizik, oyun döngüsü, canvas çizimi gibi her karede hesaplama gereken işler için requestAnimationFrame kullanın. CSS tarafındaki geçiş ve kaydırmaya bağlı animasyonlar CSS serisinin konusudur.

Erişilebilirlik: hareketi azaltma tercihi

MDN, Web Animations API sayfasında yanıp sönen ve titreşen animasyonların dikkat sorunları yaşayan kişiler için sorun olabileceğini, bazı hareket türlerinin vestibüler bozukluk, epilepsi ve migreni tetikleyebileceğini hatırlatır. Animasyonu duraklatma veya kapatma imkânı sunmanızı ve prefers-reduced-motion medya sorgusuna saygı göstermenizi önerir. JavaScript tarafında bu tercihi okuyup animasyonu başlatmadan önce kontrol etmek iyi bir alışkanlıktır.

Sık yapılan hatalar

Sık yapılan hatalar: Geri çağırmada yeni istek yapmayı unutmak., Her karede sabit adım eklemek., Kimliği güncellememek., Döngüyü iki kez başlatmak.
  • Geri çağırmada yeni istek yapmayı unutmak. Çağrı tek seferliktir; döngü bir kare sonra durur.
  • Her karede sabit adım eklemek. Yüksek yenileme hızlı ekranlarda animasyon hızlanır; geçen süreyi kullanın.
  • Kimliği güncellememek. cancelAnimationFrame eski bir kimlikle çağrılırsa döngü durmaz.
  • Döngüyü iki kez başlatmak. Başlat düğmesine iki kez basılınca iki paralel döngü çalışır; bir bayrak veya kimlik kontrolü kullanın.
  • Sekme dönüşündeki büyük zaman sıçramasını hesaba katmamak. Geçen süreyi sınırlayın ya da gizliyken duraklatın.
  • for döngüsüyle animasyon yapmaya çalışmak. MDN'nin belirttiği gibi canvas sonuçları betik bitince görünür; bir döngü içinde kare kare görüntü elde edemezsiniz.

Sık Sorulan Sorular

requestAnimationFrame saniyede kaç kez çalışır?

Sabit bir sayı yoktur; sıklık genelde ekranın yenileme hızına uyar. 60 Hz ekranda yaklaşık saniyede 60, 120 Hz ekranda yaklaşık 120 kez çalışır ve arka plan sekmelerinde duraklatılabilir. Bu yüzden kare sayısına değil zamana göre hesap yapın. MDN, çağrıların arka plan sekmelerinin yanında gizli iframe'lerde de duraklatıldığını belirtir; gömülü bir animasyon görünmediği sürece ilerlemiyorsa sebep bu olabilir.

Geri çağırmada performance.now() mı timestamp mi kullanmalıyım?

MDN'nin önerisi zaman damgasıdır: aynı karedeki bütün geri çağırmalara aynı değer gelir ve animasyonlar birbiriyle senkron kalır. performance.now() biraz daha hassasiyet verebilir ama MDN bunun değişken ve küçük bir kazanç olduğunu, geri çağırmaları güvenilir biçimde senkronize etmediğini belirtir.

Animasyonu ekran dışındayken durdurmak mantıklı mı?

Evet. Görülmeyen bir canvas animasyonunu çalıştırmak boşa iş yapmaktır. Elemanın görünürlüğünü IntersectionObserver ile izleyip görünmüyorken cancelAnimationFrame çağırabilirsiniz.

Video karelerini işlerken setTimeout kullanılabilir mi?

MDN'nin canvas ile video işleme örneği her kareyi setTimeout(..., 0) ile yeniden zamanlar, ama gerçek bir uygulamada bu zamanlamanın büyük olasılıkla videonun kare hızına göre yapılacağını not eder. Bir video karesinin canvas'a çizilmeye hazır olduğundan emin olmak için MDN'nin resim kullanma eğitimi drawImage() çağrısını requestVideoFrameCallback() içinde yapmayı önerir.

Birden fazla animasyonu nasıl senkron tutarım?

Aynı karede çalışan bütün requestAnimationFrame geri çağırmaları aynı zaman damgasını alır; MDN'ye göre bu değer aynı kökenli pencereler ve iframe'ler arasında da paylaşılır. Her animasyonun ilerlemesini bu ortak zaman damgasından hesaplarsanız, farklı fonksiyonlarda yazılmış olsalar bile birbirleriyle uyumlu ilerlerler.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Web API öğrenme rehberi · Canvas API çizim rehberi · Canvas görsel ve piksel işlemleri · Performance API · Intersection Observer · JavaScript olay döngüsü