Kung Paano Namin Itinaas ang Performance ng Aming Plugin nang 200%

Noong nakaraang Pebrero 2, 2017, isinulat ni Ann Taylor ang artikulong 12 Things We Need to See From WordPress Page Builders in 2017 + Who Already Gets It Right sa CodeInWP. Patuloy naming pinapahusay ang Page Builder Sandwich nang mahigit isang taon na ngayon: nagsasaliksik kami, nakikinig sa aming mga customer, kaya naisip naming makakapasok ang Page Builder Sandwich sa artikulo ng CodeInWP na “Who Got it Right”. Gayunpaman, hindi kami nakasama dahil sa performance. ?

Kaya tinanong namin si Ann tungkol dito, at sumagot siya:

…Sinubukan ko nang kaunti ang libreng bersyon. Napakalaki ng potensyal nito: maraming module, medyo intuitive na UI, magandang karanasan sa WYSIWYG, madaling ma-drag ang lahat. Gayunpaman, nang magkaroon ako ng humigit-kumulang 10 module sa page, naging mahirap nang magpatuloy sa paggawa gamit ito dahil sa mabagal na bilis ng pag-edit…

Kaya ayon sa kanya, may mga isyu sa performance ang Page Builder Sandwich. At dahil palagi kaming nakikinig sa feedback, sinuri namin ito.

Pagsubok sa Performance

Kailangan namin ng paraan para suriin ang performance ng PBS; dahil ang PBS ay halos tumatakbo sa browser sa pamamagitan ng Javascript, ang Timeline profiler ng Chrome ang tamang tool para sa trabaho. Kayang tukuyin at sukatin ng timeline profiler kung aling mga proseso ang nagaganap sa isang webpage habang tumatakbo ito, na perpekto para sa paghahanap ng mga bottleneck sa bilis.

Naisip naming dapat muna naming subukan ang pinakasimpleng hakbang – ang paggalaw ng mouse sa maraming elemento at hilera. Simple lang, ‘di ba? Tiwala kami na hindi kami magkakaroon ng mga isyu sa performance sa kasing-simpleng bagay na ito. Sa kasamaang-palad, nagkamali kami.

Nang pinindot namin ang record button, ginalaw namin ang mouse nang humigit-kumulang 2 segundo sa screen, pagkatapos ay pinindot ang stop button.  Narito ang naging timeline mula sa profiler:

At ayan na nga. Punong-puno ito ng mga bottleneck sa performance!

Maraming nangyayari sa graph, ngunit ang pinakahalatang isyu ay maraming purple area dito (na hindi dapat mangyari). Bukod pa riyan, kitang-kita na napakalalapad ng mga purple area na ito, at tila lahat ay umaabot sa peak ng graph. Mapapansin mo rin na palaging puno ang graph, at ang mga purple area ay halos hindi bumababa sa zero. Mukhang palaging abala ang browser sa paggawa ng kung ano, at halos hindi nagpapahinga.

Ngunit ano ba talaga ang ibig sabihin ng lahat ng purple area na iyon?

Mabuti na lamang at ipinaalam sa amin ng profiler na tinatawag ang mga ito na reflows. Narito ang depinisyon kung ano ang isang reflow mula sa Google Developer site:

Ang reflow ay ang tawag sa proseso ng web browser para sa muling pagkalkula ng mga posisyon at geometry ng mga elemento sa dokumento.. ang reflow ay isang user-blocking na operasyon sa browser.

Kaya nangangahulugan ito na ang paggalaw ng iyong mouse sa PBS ay nagti-trigger ng napakaraming reflow. Ang mas masahol pa, pansamantalang pini-pause ng bawat reflow ang browser upang matapos nito ang mga kalkulasyon nito. Ang mas maraming reflow ay nangangahulugan ng mas maraming paghinto ng browser, at ang karanasan ay nagiging mas janky. Sa mas simpleng salita, maraming purple area = masama.

Kaya lumalabas na tama si Ann: talagang mayroon kaming malaking bottleneck sa performance, kaya agad na kumilos ang aming team upang ayusin ito kaagad.

Pag-aayos sa Problema Namin sa Reflow / Jank

Bago ang anupaman, kailangan muna naming magsaliksik upang malaman kung ano nga ba ang nagdudulot ng reflow na ito sa simula pa lang. Nalaman namin na lumilitaw na maraming karaniwang bagay sa Javascript ang maaaring mag-udyok sa browser na magdulot ng reflow.

Halos anumang function na maaari mong gamitin upang makuha ang lapad, taas, o posisyon ng isang elemento ng DOM ay maaaring mag-trigger ng reflow. Sumulat si Paul Irish ng isang malawak na listahan ng lahat ng mga property at function sa Javascript na maaaring gumawa nito. Ang masamang balita ay gumagamit ang Page Builder Sandwich ng marami sa mga property at function na ito, na pawang nagdudulot ng reflow:

  • elem.getBoundingClientRect()
  • window.getComputedStyle()
  • window.scrollY
  • at marami pang iba…

Solusyon #1: Pag-cache ng Magagastos na Proseso ng Reflow

Ang pinakahalatang solusyon ay ang HINDI paggamit sa mga property at function na iyon. Gayunpaman, lubos na umaasa ang PBS sa mga ito at hindi namin kayang mawala ang mga ito.

Ang natitirang solusyon sa amin ay ang bawasan hangga’t maaari ang mga tawag sa mga function na iyon. Ang mahirap na bahagi rito ay ginamit namin ang mga property at function na iyon nang maraming beses sa iba’t ibang bahagi ng PBS, at kadalasan, ginamit namin ang mga ito sa parehong elemento ng DOM nang paulit-ulit. Para ayusin ito, gumawa kami ng sarili naming caching mechanism para sa bawat problemadong property/function.

Halimbawa, ang function na getBoundingClientRect  ay nagbibigay sa amin ng kasalukuyang sukat sa pixel ng top, left, width, height, right, at bottom ng isang elemento ng DOM. Ginagamit namin ang function na ito sa lahat ng oras upang kalkulahin ang lokasyon kung saan iguguhit ang mga outline ng elemento sa PBS. Narito ang caching function na ginawa namin para dito:

// Cache: Dito nakatago ang aming mga client rect, magagamit namin ito para i-clear ang mga ito mamaya.
var elemsWithBoundingRects = [];

window.pbsGetBoundingClientRect = function( element ) {

	// Suriin kung nakuha na namin ang client rect dati.
	if ( ! element._boundingClientRect ) {

		// Kung hindi, kunin ito saka i-store para sa paggamit sa hinaharap.
		element._boundingClientRect = element.getBoundingClientRect();
		elemsWithBoundingRects.push( element );
	}
	return element._boundingClientRect;
};
​

Sa halip na tawagin ang getBoundingClientRect , ginamit na namin ngayon ang pbsGetBoundingClientRect . Ngayon, sa bawat oras na i-query ang client rect, ini-store namin ang value na iyon para sa sanggunian sa hinaharap, sa gayon ay nalilimitahan ang dami ng mga trigger ng reflow.

Kailangan din namin ng paraan para i-clear ang cache kapag naging imbalido na ang mga naka-store na client rect. Nagbabago ang mga value ng naka-store na client rect kapag nag-i-scroll ang user sa page o nagdaragdag/nagbabago/nagtatanggal ng mga elemento sa screen. Kaya gumawa kami ng function sa pag-clear ng cache na maaari naming tawagin sa tuwing kailangan namin ng mga sariwang value ng client rect:

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

Ginawa rin namin ang parehong caching mechanism na ito para sa iba pang magagastos na tawag sa function na karaniwan naming ginagamit.

Solusyon #2: Pagsasama-sama ng mga Read & Write Gamit ang FastDom

Ang pagpapatupad ng solusyon #1 ay nagpataas sa performance ng PBS, ngunit hindi pa ito sapat.

Sa pag-Google pa nang kaunti, nakakita kami ng isang artikulo ni Wilson Page tungkol sa Pag-iwas sa ‘layout thrashing’. Sabi ni Wilson:

Ang Layout Thrashing (reflow) ay nagaganap kapag ang JavaScript ay marahas na nagsu-sulat, pagkatapos ay nagbabasa, mula sa DOM nang paulit-ulit na nagdudulot ng mga reflow sa dokumento.

Lumalabas na hindi sapat na limitahan ang iyong mga tawag sa mga partikular na property at function na nagti-trigger ng reflow. Kapag nagsagawa ka ng maraming magkakasunod na “read” at “write” sa DOM, nagti-trigger din ito ng reflow sa browser. Narito ang isang karaniwang senaryo, diretso mula sa site ni Wilson:

// Read
var h1 = element1.clientHeight;

// Write (nagpapawalang-bisa sa layout)
element1.style.height = (h1 * 2) + 'px';

// Read (nagti-trigger ng reflow)
var h2 = element2.clientHeight;

// Write (nagpapawalang-bisa sa layout)
element2.style.height = (h2 * 2) + 'px';

// Read (nagti-trigger ng reflow)
var h3 = element3.clientHeight;

// Write (nagpapawalang-bisa sa layout)
element3.style.height = (h3 * 2) + 'px';

// Magre-reflow ang dokumento sa dulo ng frame​

Ang halimbawa sa itaas ay nagti-trigger ng 3 reflow dahil ang mga sukat ng buong page ay nawawalan ng bisa pagkatapos ng isang write. Ang iminumungkahing solusyon para dito ay muling ayusin ang code upang pagsama-samahin ang lahat ng read at write nang sa gayon ay 1 reflow lang sa browser ang ma-trigger ng kabuuan:

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

// Write (nagpapawalang-bisa sa layout)
element1.style.height = (h1 * 2) + 'px';
element2.style.height = (h2 * 2) + 'px';
element3.style.height = (h3 * 2) + 'px';

// Magre-reflow ang dokumento sa dulo ng frame​

Dahil sa snippet sa itaas, nagmumukhang madali ang pag-aayos sa problema. Ngunit sa totoo lang, sa PBS, ang pagbabasa at pagsusulat ng mga bagay na nagti-trigger ng reflow ay nangyayari sa maraming iba’t ibang lugar sa maraming iba’t ibang oras sa buong life-cycle ng PBS. Hindi praktikal ang muling pag-aayos ng libu-libong linya ng Javascript code.

Sa kabutihang-palad para sa amin, nagbigay din si Wilson ng solusyon sa kanyang artikulo. Gumawa siya ng isang maliit na utility script upang makatulong sa pagsasama-sama ng mga read at write na iyon, na tinatawag na FastDom.

Narito ang ilang code mula sa Page Builder Sandwich na gumagamit ng FastDom at kasama rin ang aming Solusyon #1 sa itaas:

// Pagsama-samahin ang mga read mula sa iba pang thread.
fastdom.measure( function() {

	// Gamitin ang aming naka-cache na client rect.
	var rect = window.pbsGetBoundingClientRect( elem );

	// Pagsama-samahin ang mga write mula sa iba pang thread.
	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 ) );​

Ang ginagawa ng FastDom ay pinagsasama-sama nito ang lahat ng tawag sa fastdom.measure  at lahat ng tawag sa fastdom.mutate  at isinasagawa ang mga ito nang asynchronously, na epektibong pinapangkat ang lahat ng read at write upang mabawasan ang lahat ng reflow sa browser.

Solusyon #3: Magastos ang Pag-access sa mga Style Object

Nalaman din namin sa tulong ng jsPerf na mas mabilis ang ilang function na kumukuha ng style kaysa sa iba.

Sa PBS, karaniwan naming kailangang makuha ang kasalukuyang value ng isang partikular na style ng isang elemento. Halimbawa, karaniwan naming ginagamit ito upang makuha ang top margin:

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

Ngunit, nalaman namin na ang paggawa nito ay higit na mas mabilis:

var marginTop = elem.style.marginTop;​

Gayunpaman, magkaibang-magkaiba ang elem.style.marginTop  at getComputedStyle . Ang una ay nagbibigay lamang sa amin ng inline styling ng elemento, at hindi isinasaalang-alang ang anumang stylesheet; at ang pangalawa ay isinasaalang-alang ang parehong inline style at stylesheet.

Bukod pa rito, kahit na naka-cache ang resulta ng getComputedStyle , talagang mabagal pa rin ito. Lumalabas na ang mismong pag-access sa style object ang mabagal na proseso.

Sa kabutihang-palad para sa amin, nagdaragdag ang PBS ng mga inline style sa mga elemento, at dahil mas nangingibabaw ang mga inline style (kadalasan) kaysa sa mga stylesheet, magkakaroon ng mga sitwasyon kung saan magiging sapat na ang pagkuha ng value ng inline style. Kaya kung pagsasamahin natin ang dalawang pamamaraan sa itaas, makakagawa tayo ng code na tumatakbo nang mas mabilis:

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

Ang mga Resulta ng mga Pagpapahusay sa Code

Ang aming 3 solusyon sa itaas ay nangailangan ng maraming pananaliksik, pagsubok, at aktwal na pagpapatupad (at naantala ang paglabas ng aming feature na Fonts), ngunit sa huli ay naging sulit ang lahat.

Bumabalik sa aming unang pagsubok ng paggalaw ng mouse sa screen nang 2 segundo. Narito ang lumang pangit/janky na timeline:

At narito ang hitsura ng aming bagong timeline pagkatapos ng mga pagpapahusay sa performance:

Tulad ng makikita mo, mas kaunti ang mga kulay purple sa graph. At may kapansin-pansing pagkakaiba sa dami ng mga prosesong isinasagawa ng browser. Mas manipis din ang mga parihabang bagay sa ibaba, na nangangahulugang mas mabilis na pinapatakbo ang mga proseso kaysa dati.

Kung ikukumpara mo ang dami ng matatangkad/malalapad na purple area sa luma at bagong graph, nakakakita tayo ng napakalaking 200% pagtaas sa performance! Nakakakita rin kami ng mga pagtaas sa performance sa pangkalahatan. Hindi na masama!

Mga Mahahalagang Punto & Natutunan

Sa totoo lang, hindi namin naisip na may mga isyu sa performance ang PBS bago ito binanggit ni Ann. Pangunahin dahil gumagamit kami ng mga makinang nakasentro sa development at hindi ganoon kahalata ang mga bottleneck sa performance. Sa pagbabalik-tanaw, dapat ay isa sa mga nangungunang priyoridad ang pagsusuri sa performance. Ngunit kahit papaano ngayon, mula sa bersyon 4.3, naasikaso na ito. ?

Para sa mga nagnanais na pahusayin ang sarili nilang performance ng WordPress plugin, lalo na kung gumagamit ka rin ng mabigat na Javascript, narito ang aking mga natutunan mula sa karanasang ito:

  1. Limitahan/i-cache ang iyong mga tawag sa mga property/function na nagti-trigger ng reflow. Gamitin ito bilang gabay: What forces layout/reflow. The comprehensive list.
  2. Pagsama-samahin ang mga “read” at “write” sa mga style ng DOM. Gamitin ang FastDom upang gawin ito para sa iyo.
  3. Gamitin ang Timeline profiler ng Chrome, at tiyaking kakaunti ang mga reflow (mga kulay purple) hangga’t maaari.