Eklentimizin Performansını Nasıl %200 Artırdık?

Geçtiğimiz 2 Şubat 2017’de, Ann Taylor CodeInWP’de 12 Things We Need to See From WordPress Page Builders in 2017 + Who Already Gets It Right başlıklı bir makale yazdı. Bir yılı aşkın süredir Page Builder Sandwich’i sürekli geliştiriyoruz: araştırmalarımızı yapıyor, müşterilerimizi dinliyoruz; bu yüzden Page Builder Sandwich’in CodeInWP’nin “Who Got it Right” makalesine gireceğini düşünmüştük. Ancak performans nedeniyle giremedik. ?

Biz de bunu Ann’e sorduk ve kendisi şöyle yanıt verdi:

…Ücretsiz sürümle biraz denemeler yaptım. Çok şey vadediyor: çok sayıda modül, oldukça sezgisel bir arayüz, harika bir WYSIWYG deneyimi, her şey kolayca sürüklenip bırakılıyor. Ancak sayfada yaklaşık 10 modül olduğunda, düşük düzenleme hızı nedeniyle üzerinde çalışmaya devam etmek zorlaştı…

Dolayısıyla ona göre Page Builder Sandwich’in performans sorunları vardı. Ve biz de geri bildirimleri her zaman dikkate aldığımız için bu durumu inceledik.

Performansı Test Etme

PBS’in performansını kontrol etmek için bir yola ihtiyacımız vardı; PBS çoğunlukla Javascript aracılığıyla tarayıcıda çalıştığı için, Chrome’un Timeline profilleyicisi bu iş için doğru araçtı. Zaman çizelgesi profilleyicisi, çalışma zamanında bir web sayfasında hangi işlemlerin gerçekleştiğini tespit edip ölçebilir; bu da hız darboğazlarını bulmak için mükemmeldir.

Öncelikle en basit adımı test etmemiz gerektiğini düşündük; faremizi birden fazla öğe ve satır üzerinde gezdirmek. Basit, değil mi? Bu kadar basit bir şeyde performans sorunlarıyla karşılaşmayacağımızdan emindik. Ne yazık ki yanılmışız.

Kayıt düğmesine bastıktan sonra fareyi ekranda yaklaşık 2 saniye gezdirdik ve ardından durdur düğmesine bastık.  İşte profilleyiciden elde edilen sonuç zaman çizelgesi:

Bir de ne görelim! Performans darboğazlarıyla dopdolu!

Grafikte pek çok şey oluyor, ancak en bariz sorun içinde çok fazla mor alan bulunması (ki bu normalde olmamalıdır). Bunun da ötesinde, bu mor alanlar gözle görülür şekilde oldukça geniş ve hepsi grafiğin zirvesine ulaşıyor gibi görünüyor. Ayrıca grafiğin her zaman dolu olduğunu ve mor alanların neredeyse hiçbir zaman sıfıra inmediğini de fark edeceksiniz. Tarayıcı sürekli bir şeylerle meşgul gibi görünüyor ve neredeyse hiç dinlenmiyor.

Peki tüm bu mor alanlar gerçekte ne anlama geliyor?

Profilleyici bize bunların reflow olarak adlandırıldığını gösterdi. İşte Google Developer sitesinden:

Reflow, belgedeki öğelerin konumlarını ve geometrilerini yeniden hesaplamak için web tarayıcısının yürüttüğü işlemin adıdır.. reflow, tarayıcıda kullanıcıyı engelleyen bir işlemdir.

Dolayısıyla bu, farenizi PBS içinde gezdirmenin tonlarca reflow tetiklediği anlamına gelir. Daha da kötüsü, her reflow’un hesaplamalarını tamamlayabilmesi için tarayıcıyı anlık olarak duraklatmasıdır. Daha fazla reflow, daha fazla tarayıcı duraklaması anlamına gelir ve deneyim daha da takılmalı (janky) hale gelir. Daha basit bir ifadeyle, çok sayıda mor alan = kötü.

Böylece Ann’in haklı olduğu ortaya çıktı: Gerçekten de büyük bir performans darboğazımız vardı, bu yüzden ekibimiz bunu hemen düzeltmek için derhal işe koyuldu.

Reflow / Takılma Problemimizi Çözme

Her şeyden önce, en başta bu reflow’a neyin sebep olduğunu bulmak için biraz araştırma yapmamız gerekiyordu. Görünüşe göre, Javascript’teki birçok yaygın şeyin tarayıcıyı reflow yapmaya zorlayabildiğini keşfettik.

Bir DOM öğesinin genişliğini, yüksekliğini veya konumunu almak için kullanabileceğiniz neredeyse her işlev bir reflow tetikleyebilir. Paul Irish, Javascript’te bunu yapabilen tüm özelliklerin ve işlevlerin kapsamlı bir listesini yazdı. Kötü haber şu ki, Page Builder Sandwich bu özellik ve işlevlerin birçoğunu kullanıyor ve hepsi de reflow’a neden oluyor:

  • elem.getBoundingClientRect()
  • window.getComputedStyle()
  • window.scrollY
  • ve çok daha fazlası…

Çözüm #1: Maliyetli Reflow İşlemlerini Önbelleğe Alma

En belirgin çözüm, bu özellikleri ve işlevleri KULLANMAMAKTIR. Ancak PBS büyük ölçüde bunlara dayanıyor ve onlar olmadan yapamayız.

Elimizde kalan çözüm, bu işlevlere yapılan çağrıları mümkün olduğunca azaltmaktı. Buradaki zor kısım, bu özellikleri ve işlevleri PBS’in farklı yerlerinde defalarca kullanıyor olmamızdı ve çoğu zaman bunları aynı DOM öğesi üzerinde birden çok kez kullanıyorduk. Bunu düzeltmek için, sorunlu her özellik/işlev için kendi önbelleğe alma mekanizmamızı oluşturduk.

Örneğin, getBoundingClientRect  işlevi bize bir DOM öğesinin mevcut üst, sol, genişlik, yükseklik, sağ ve alt piksel ölçümlerini verir. Bu işlevi PBS’te öğe sınırlarının nereye çizileceğini hesaplamak için sürekli kullanırız. İşte bunun için oluşturduğumuz önbelleğe alma işlevi:

// Önbellek: Client rect'lerimiz burada tutulur, daha sonra bunları temizlemek için bunu kullanabiliriz.
var elemsWithBoundingRects = [];

window.pbsGetBoundingClientRect = function( element ) {

	// Client rect'i daha önce alıp almadığımızı kontrol et.
	if ( ! element._boundingClientRect ) {

		// Almadıysak, al ve gelecekte kullanmak üzere sakla.
		element._boundingClientRect = element.getBoundingClientRect();
		elemsWithBoundingRects.push( element );
	}
	return element._boundingClientRect;
};
​

getBoundingClientRect  çağırmak yerine artık pbsGetBoundingClientRect  kullandık. Artık client rect her sorgulandığında, bu değeri gelecekte başvurmak üzere saklıyoruz ve böylece reflow tetikleyicilerinin sayısını sınırlandırıyoruz.

Ayrıca, saklanan client rect’ler geçersiz hale geldiğinde önbelleği temizlemenin bir yoluna da ihtiyacımız vardı. Kullanıcı sayfayı kaydırdığında veya ekranda öğeler eklediğinde/değiştirdiğinde/kaldırdığında saklanan client rect değerleri değişir. Bu nedenle, yeni client rect değerlerine ihtiyaç duyduğumuzda çağırabileceğimiz bir önbellek temizleme işlevi oluşturduk:

var clearClientRects = function() {
	var i;
	for ( i = 0; i < elemsWithBoundingRects.length; i++ ) {
		if ( elemsWithBoundingRects[ i ] ) {
			elemsWithBoundingRects[ i ]._boundingClientRect = null;
		}
	}
	elemsWithBoundingRects = [];
};​

Aynı önbelleğe alma mekanizmasını genellikle kullandığımız diğer maliyetli işlev çağrıları için de uyguladık.

Çözüm #2: FastDom Kullanarak Okuma ve Yazma İşlemlerini Gruplama

Çözüm #1’i uygulamak PBS’in performansını artırdı, ancak yeterli olmadı.

Google’da biraz daha araştırma yaparken, Wilson Page’in ‘Düzen çalkantısını’ (layout thrashing) önleme üzerine yazdığı bir makaleye rastladık. Wilson şöyle diyor:

Düzen Çalkantısı (reflow), JavaScript’in DOM’a art arda şiddetli bir şekilde yazıp ardından okuması ve birden çok kez belge reflow’larına neden olması durumunda meydana gelir.

Görünüşe göre, reflow tetikleyen belirli özelliklere ve işlevlere yaptığınız çağrıları sınırlandırmak yeterli değil. DOM üzerinde art arda birden fazla “okuma” ve “yazma” işlemi gerçekleştirdiğinizde, bu durum da bir tarayıcı reflow’unu tetikler. İşte doğrudan Wilson’ın sitesinden tipik bir senaryo:

// Okuma
var h1 = element1.clientHeight;

// Yazma (düzeni geçersiz kılar)
element1.style.height = (h1 * 2) + 'px';

// Okuma (reflow tetikler)
var h2 = element2.clientHeight;

// Yazma (düzeni geçersiz kılar)
element2.style.height = (h2 * 2) + 'px';

// Okuma (reflow tetikler)
var h3 = element3.clientHeight;

// Yazma (düzeni geçersiz kılar)
element3.style.height = (h3 * 2) + 'px';

// Çerçeve sonunda belge reflow'a uğrar​

Yukarıdaki örnek 3 reflow tetikler çünkü bir yazma işleminden sonra tüm sayfanın boyutları geçersiz kılınır. Bunun için önerilen çözüm, tüm işlemin yalnızca 1 tarayıcı reflow’u tetiklemesi için tüm okuma ve yazma işlemlerini bir araya toplayacak şekilde kodu yeniden düzenlemektir:

// Okuma
var h1 = element1.clientHeight;
var h2 = element2.clientHeight;
var h3 = element3.clientHeight;

// Yazma (düzeni geçersiz kılar)
element1.style.height = (h1 * 2) + 'px';
element2.style.height = (h2 * 2) + 'px';
element3.style.height = (h3 * 2) + 'px';

// Çerçeve sonunda belge reflow'a uğrar​

Yukarıdaki kod parçacığı sorunu çözmeyi kolay gösteriyor. Ancak gerçekte, PBS’te reflow tetikleyen şeyleri okuma ve yazma işlemleri PBS’in yaşam döngüsü boyunca birçok farklı yerde ve birçok farklı zamanda gerçekleşir. Binlerce satırlık Javascript kodunu yeniden düzenlemek uygulanabilir değildir.

Şansımıza Wilson makalesinde buna da bir çözüm sunmuştu. Bu okuma ve yazmaları bir araya getirmeye yardımcı olmak için FastDom adında küçük bir yardımcı komut dosyası oluşturmuştu.

İşte Page Builder Sandwich’ten FastDom’u kullanan ve yukarıdaki Çözüm #1’imizi de içeren bazı kodlar:

// Diğer iş parçacıklarından gelen okumaları bir araya topla.
fastdom.measure( function() {

	// Önbelleğe alınmış client rect'imizi kullan.
	var rect = window.pbsGetBoundingClientRect( elem );

	// Diğer iş parçacıklarından gelen yazmaları bir araya topla.
	fastdom.mutate( function() {
		elem.style.top = ( rect.top + window.pbsScrollY() ) + 'px';
		elem.style.height = rect.height + 'px';
		elem.style.left = rect.left + 'px';
		elem.style.width = rect.width + 'px';
	}.bind( this ) );
}.bind( this ) );​

FastDom’un yaptığı şey, tüm fastdom.measure  çağrılarını ve tüm fastdom.mutate  çağrılarını bir araya getirmek ve bunları eşzamansız olarak yürüterek tarayıcıdaki tüm reflow’ları azaltmak için tüm okuma ve yazmaları etkili bir şekilde gruplamaktır.

Çözüm #3: Stil Nesnelerine Erişmek Maliyetlidir

Ayrıca jsPerf’in yardımıyla bazı stil alma işlevlerinin diğerlerinden daha hızlı olduğunu keşfettik.

PBS’te genellikle bir öğenin belirli bir stilinin geçerli değerini almamız gerekir. Örneğin, üst kenar boşluğunu almak için normalde şunu kullanırız:

var styles = window.getComputedStyle( elem );
var marginTop = styles.marginTop;​

Ancak bunu yapmanın çok daha hızlı olduğunu gördük:

var marginTop = elem.style.marginTop;​

Bununla birlikte, elem.style.marginTop  ve getComputedStyle  birbirinden çok farklıdır. İlki bize yalnızca öğenin satır içi (inline) stilini verir ve herhangi bir stil sayfasını dikkate almaz; ikincisi ise hem satır içi stilleri hem de stil sayfalarını dikkate alır.

Dahası, getComputedStyle  sonucu önbelleğe alındığında bile hala gerçekten yavaştı. Anlaşılan o ki, stil nesnesinin kendisine erişmek yavaş olan işlemdir.

Şansımıza, PBS öğelere satır içi stiller ekler ve satır içi stiller (çoğu zaman) stil sayfalarından öncelikli olduğundan, satır içi stil değerini almanın yeterli olacağı durumlar olacaktır. Dolayısıyla yukarıdaki iki yöntemi birleştirirsek, çok daha hızlı çalışan bir kod elde edebiliriz:

var marginTop = elem.style.marginTop;
if ( ! marginTop ) {
	var styles = window.getComputedStyle( elem );
	marginTop = styles.marginTop;
}​

Kod İyileştirmelerinin Sonuçları

Yukarıdaki 3 çözümümüz çok fazla araştırma, test ve fiili uygulama gerektirdi (ve Yazı Tipleri özelliğimizin yayınlanmasını geciktirdi), ancak sonunda her şeye değdi.

Fareyi 2 saniye boyunca ekranda hareket ettirdiğimiz ilk teste geri dönersek. İşte eski çirkin/takılmalı zaman çizelgesi:

Ve işte performans iyileştirmelerinden sonra yeni zaman çizelgemizin görünümü:

Gördüğünüz gibi grafikte daha az mor kısım var. Ve tarayıcı tarafından gerçekleştirilen işlem miktarında gözle görülür bir fark mevcut. Alttaki dikdörtgen yapılar da daha ince, bu da işlemlerin eskisinden daha hızlı yürütüldüğü anlamına geliyor.

Eski ve yeni grafiklerdeki yüksek/geniş mor alanların miktarını karşılaştırırsanız, tamı tamına %200’lük bir performans artışından bahsediyoruz! Ayrıca genel olarak da performans artışları görüyoruz. Hiç de fena değil!

Çıkarılan Dersler ve Öğrenilenler

Dürüst olmak gerekirse Ann konuyu açana kadar PBS’in performans sorunları olduğunu düşünmüyorduk. Bunun temel nedeni, geliştirme odaklı makineler kullanıyor olmamız ve performans darboğazlarının o kadar belirgin olmamasıydı. Geçmişe baktığımızda, performansı kontrol etmek en önemli önceliklerden biri olmalıydı. Ama en azından şimdi, sürüm 4.3 itibarıyla bu sorun çözüldü. ?

Kendi WordPress eklentisinin performansını artırmak isteyenler için, özellikle de yoğun Javascript kullanıyorsanız, bu deneyimden çıkardığım dersler şunlar:

  1. Reflow tetikleyen özelliklere/işlevlere yaptığınız çağrıları sınırlandırın/önbelleğe alın. Bunu bir rehber olarak kullanın: What forces layout/reflow. The comprehensive list.
  2. DOM stillerine yönelik “okuma” ve “yazma” işlemlerini bir arada gruplayın. Bunu sizin yerinize yapması için FastDom kullanın.
  3. Chrome’un Timeline profilleyicisini kullanın ve mümkün olduğunca az reflow (mor kısım) olduğundan emin olun.