วิธีที่เราเพิ่มประสิทธิภาพของปลั๊กอินขึ้น 200%

เขียนโดย

ใน

,

เมื่อวันที่ 2 กุมภาพันธ์ 2017 ที่ผ่านมา Ann Taylor ได้เขียนบทความ 12 Things We Need to See From WordPress Page Builders in 2017 + Who Already Gets It Right ใน CodeInWP เราได้ปรับปรุง Page Builder Sandwich อย่างต่อเนื่องมานานกว่าหนึ่งปีแล้ว: เราทำวิจัย รับฟังความคิดเห็นจากลูกค้า ดังนั้นเราจึงคิดว่า Page Builder Sandwich น่าจะได้อยู่ในบทความ “Who Got it Right” ของ CodeInWP แต่เรากลับไม่ติดอันดับ เนื่องจากเรื่องประสิทธิภาพ ?

เราจึงถาม Ann เกี่ยวกับเรื่องนี้ และเธอได้ตอบกลับมาว่า:

…ฉันได้ลองเล่นเวอร์ชันฟรีนิดหน่อย มันดูมีแววดีมาก: มีโมดูลเพียบ UI ที่ค่อนข้างใช้งานง่าย ประสบการณ์ WYSIWYG ที่ยอดเยี่ยม ทุกอย่างสามารถลากไปมาได้ง่าย อย่างไรก็ตาม เมื่อฉันมีโมดูลประมาณ 10 โมดูลบนหน้าเว็บ มันก็เริ่มทำงานต่อไปได้ยากขึ้นเพราะความเร็วในการแก้ไขที่ช้าลง…

ดังนั้นตามที่เธอกล่าว Page Builder Sandwich จึงมีปัญหาด้านประสิทธิภาพ และเนื่องจากเราคอยรับฟังคำติชมเสมอ เราจึงเข้าไปตรวจสอบเรื่องนี้

การทดสอบประสิทธิภาพ

เราต้องการวิธีตรวจสอบประสิทธิภาพของ PBS เนื่องจาก PBS ส่วนใหญ่ทำงานบนเบราว์เซอร์ผ่าน Javascript เครื่องมือ Timeline profiler ของ Chrome จึงเป็นเครื่องมือที่เหมาะสมกับงานนี้ โดยตัว timeline profiler สามารถระบุและวัดผลได้ว่ากระบวนการใดกำลังทำงานอยู่ในหน้าเว็บระหว่างรันไทม์ ซึ่งเหมาะมากสำหรับการค้นหาคอขวดด้านความเร็ว

เราคิดว่าเราควรทดสอบขั้นตอนที่เรียบง่ายที่สุดก่อน นั่นคือการเลื่อนเมาส์ผ่านเอลิเมนต์และแถวต่างๆ ฟังดูง่ายใช่ไหม? เรามั่นใจว่าจะไม่เจอปัญหาด้านประสิทธิภาพกับอะไรง่ายๆ แบบนี้ แต่น่าเสียดายที่เราคิดผิด

ทันทีที่เรากดปุ่มบันทึก เราก็เลื่อนเมาส์ไปมาบนหน้าจอประมาณ 2 วินาที แล้วกดปุ่มหยุด นี่คือผลลัพธ์ไทม์ไลน์จากตัว profiler:

และแล้วก็ปรากฏว่า… มันเต็มไปด้วยคอขวดด้านประสิทธิภาพ!

มีหลายสิ่งหลายอย่างเกิดขึ้นในกราฟ แต่ปัญหาที่ชัดเจนที่สุดคือมีพื้นที่สีม่วงอยู่เป็นจำนวนมาก (ซึ่งไม่ควรจะเป็นเช่นนั้น) ยิ่งไปกว่านั้น พื้นที่สีม่วงเหล่านี้ยังกว้างมากอย่างเห็นได้ชัด และดูเหมือนจะแตะจุดสูงสุดของกราฟทั้งหมด คุณจะสังเกตเห็นได้เช่นกันว่ากราฟเต็มอยู่ตลอดเวลา และพื้นที่สีม่วงแทบจะไม่เคยลดลงเหลือศูนย์เลย ดูเหมือนว่าเบราว์เซอร์จะยุ่งอยู่กับการทำอะไรบางอย่างตลอดเวลาและแทบไม่ได้พักเลย

แต่พื้นที่สีม่วงทั้งหมดนั้นหมายความว่าอย่างไรกันแน่?

ตัว profiler ใจดีพอที่จะบอกเราว่าสิ่งเหล่านี้เรียกว่า reflows และนี่คือนิยามของ reflow จากเว็บไซต์ Google Developer:

Reflow คือชื่อของกระบวนการในเว็บเบราว์เซอร์สำหรับการคำนวณตำแหน่งและรูปทรงเรขาคณิตของเอลิเมนต์ต่างๆ ในเอกสารใหม่อีกครั้ง.. reflow เป็นการดำเนินการที่บล็อกการใช้งานของผู้ใช้ (user-blocking operation) ในเบราว์เซอร์

ดังนั้นหมายความว่าการเลื่อนเมาส์ไปมาใน PBS จะกระตุ้นให้เกิด reflow จำนวนมหาศาล สิ่งที่แย่ไปกว่านั้นคือ reflow แต่ละครั้งจะหยุดการทำงานของเบราว์เซอร์ชั่วขณะเพื่อให้คำนวณเสร็จสิ้น ยิ่งมี reflow มากขึ้นก็หมายความว่าเบราว์เซอร์จะหยุดชั่วคราวบ่อยขึ้น และประสบการณ์การใช้งานก็จะยิ่งกระตุก (janky) มากขึ้นไปอีก พูดง่ายๆ ก็คือ พื้นที่สีม่วงเยอะ = แย่

ปรากฏว่า Ann พูดถูก: เรามีคอขวดด้านประสิทธิภาพครั้งใหญ่จริงๆ ทีมงานของเราจึงลงมือจัดการเพื่อแก้ไขปัญหานี้ทันที

การแก้ไขปัญหา Reflow / การกระตุกของเรา

ก่อนอื่น เราจำเป็นต้องค้นคว้าเพื่อหาคำตอบว่าอะไรเป็นสาเหตุที่ทำให้เกิด reflow นี้ตั้งแต่แรก เราพบว่าตามข้อเท็จจริงแล้ว สิ่งทั่วไปหลายอย่างใน Javascript สามารถกระตุ้นให้เบราว์เซอร์ทำให้เกิด reflow ได้

เกือบทุกฟังก์ชันที่คุณสามารถใช้เพื่อรับค่าความกว้าง ความสูง หรือตำแหน่งของ DOM element สามารถทำให้เกิด reflow ได้ทั้งสิ้น Paul Irish ได้เขียนรายการโดยละเอียดของพร็อพเพอร์ตี้และฟังก์ชันทั้งหมดใน Javascript ที่ทำให้เกิดสิ่งนี้ ข่าวร้ายก็คือ Page Builder Sandwich ใช้พร็อพเพอร์ตี้และฟังก์ชันเหล่านี้จำนวนมาก ซึ่งทั้งหมดล้วนทำให้เกิด reflow:

  • elem.getBoundingClientRect()
  • window.getComputedStyle()
  • window.scrollY
  • และอื่นๆ อีกมากมาย…

วิธีแก้ปัญหาที่ 1: การแคชกระบวนการ Reflow ที่สิ้นเปลืองทรัพยากร

วิธีแก้ปัญหาที่ชัดเจนที่สุดคือการไม่ใช้พร็อพเพอร์ตี้และฟังก์ชันเหล่านั้น อย่างไรก็ตาม PBS พึ่งพาสิ่งเหล่านี้อย่างมาก และเราไม่สามารถทำงานได้หากไม่มีสิ่งเหล่านี้

ทางออกเดียวที่เราเหลืออยู่คือลดการเรียกใช้ฟังก์ชันเหล่านั้นให้ได้มากที่สุด ส่วนที่ยากก็คือเราใช้พร็อพเพอร์ตี้และฟังก์ชันเหล่านั้นหลายครั้งในส่วนต่างๆ ของ PBS และส่วนใหญ่แล้ว เราเรียกใช้กับ DOM element เดียวกันหลายครั้ง เพื่อแก้ไขปัญหานี้ เราจึงสร้างกลไกการแคชของเราเองสำหรับแต่ละพร็อพเพอร์ตี้/ฟังก์ชันที่มีปัญหา

ตัวอย่างเช่น ฟังก์ชัน getBoundingClientRect จะให้การวัดค่าพิกเซลปัจจุบันของ top, left, width, height, right และ bottom ของ DOM element เราใช้ฟังก์ชันนี้ตลอดเวลาเพื่อคำนวณตำแหน่งที่จะวาดเส้นขอบของเอลิเมนต์ใน PBS นี่คือฟังก์ชันการแคชที่เราสร้างขึ้นสำหรับสิ่งนี้:

// แคช: ค่า client rects ของเราจะถูกเก็บไว้ที่นี่ เราสามารถใช้ตัวนี้เพื่อล้างข้อมูลในภายหลัง
var elemsWithBoundingRects = [];

window.pbsGetBoundingClientRect = function( element ) {

	// ตรวจสอบว่าเราเคยดึงค่า client rect มาก่อนหรือไม่
	if ( ! element._boundingClientRect ) {

		// ถ้าไม่ ให้ดึงค่าแล้วจัดเก็บไว้เพื่อใช้ในอนาคต
		element._boundingClientRect = element.getBoundingClientRect();
		elemsWithBoundingRects.push( element );
	}
	return element._boundingClientRect;
};
​

แทนที่จะเรียกใช้ getBoundingClientRect ตอนนี้เราเปลี่ยนมาใช้ pbsGetBoundingClientRect แทน ตอนนี้ทุกครั้งที่มีการสอบถาม client rect เราจะเก็บค่านั้นไว้สำหรับการอ้างอิงในอนาคต ซึ่งจะช่วยจำกัดจำนวนการกระตุ้นให้เกิด reflow

เรายังต้องการวิธีล้างแคชเมื่อ client rects ที่จัดเก็บไว้ใช้การไม่ได้แล้วด้วย ค่าของ client rect ที่จัดเก็บไว้จะเปลี่ยนแปลงเมื่อผู้ใช้เลื่อนหน้าเว็บ หรือเพิ่ม/เปลี่ยนแปลง/ลบเอลิเมนต์ในหน้าจอ ดังนั้นเราจึงสร้างฟังก์ชันล้างแคชที่เราสามารถเรียกใช้ได้ทุกเมื่อที่ต้องการค่า client rect ใหม่:

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

เรายังได้ใช้กลไกการแคชแบบเดียวกันนี้กับการเรียกใช้ฟังก์ชันอื่นๆ ที่สิ้นเปลืองทรัพยากรซึ่งเรามักจะใช้อยู่เป็นประจำด้วย

วิธีแก้ปัญหาที่ 2: การจัดกลุ่ม Reads & Writes เข้าด้วยกันโดยใช้ FastDom

การใช้วิธีแก้ปัญหาที่ 1 ช่วยเพิ่มประสิทธิภาพของ PBS ได้ แต่ก็ยังไม่เพียงพอ

เมื่อค้นหาใน Google เพิ่มเติมอีกเล็กน้อย เราก็พบบทความโดย Wilson Page เรื่อง Preventing ‘layout thrashing’ โดย Wilson กล่าวว่า:

Layout Thrashing (reflow) เกิดขึ้นเมื่อ JavaScript เขียน แล้วอ่าน จาก DOM อย่างรุนแรงและซ้ำๆ หลายครั้ง ทำให้เอกสารเกิด reflow

เห็นได้ชัดว่า การจำกัดการเรียกใช้พร็อพเพอร์ตี้และฟังก์ชันเฉพาะที่กระตุ้นให้เกิด reflow นั้นไม่เพียงพอ เมื่อคุณทำ “reads” (อ่าน) และ “writes” (เขียน) บน DOM ต่อเนื่องกันหลายครั้ง มันก็จะกระตุ้นให้เกิด browser reflow เช่นกัน นี่คือสถานการณ์ทั่วไปจากเว็บไซต์ของ Wilson โดยตรง:

// อ่าน
var h1 = element1.clientHeight;

// เขียน (ทำให้เลย์เอาต์ใช้งานไม่ได้)
element1.style.height = (h1 * 2) + 'px';

// อ่าน (กระตุ้น reflow)
var h2 = element2.clientHeight;

// เขียน (ทำให้เลย์เอาต์ใช้งานไม่ได้)
element2.style.height = (h2 * 2) + 'px';

// อ่าน (กระตุ้น reflow)
var h3 = element3.clientHeight;

// เขียน (ทำให้เลย์เอาต์ใช้งานไม่ได้)
element3.style.height = (h3 * 2) + 'px';

// เอกสารเกิด reflow เมื่อสิ้นสุดเฟรม​

ตัวอย่างด้านบนจะกระตุ้นให้เกิด reflow ถึง 3 ครั้ง เนื่องจากขนาดของทั้งหน้าจะใช้การไม่ได้หลังจากการเขียน วิธีแก้ไขที่แนะนำสำหรับกรณีนี้คือการจัดลำดับโค้ดใหม่เพื่อรวมการอ่านและการเขียนทั้งหมดเข้าด้วยกัน เพื่อให้กระบวนการทั้งหมดกระตุ้น browser reflow เพียงครั้งเดียวเท่านั้น:

// อ่าน
var h1 = element1.clientHeight;
var h2 = element2.clientHeight;
var h3 = element3.clientHeight;

// เขียน (ทำให้เลย์เอาต์ใช้งานไม่ได้)
element1.style.height = (h1 * 2) + 'px';
element2.style.height = (h2 * 2) + 'px';
element3.style.height = (h3 * 2) + 'px';

// เอกสารเกิด reflow เมื่อสิ้นสุดเฟรม​

โค้ดสั้นๆ ด้านบนทำให้การแก้ปัญหาดูเหมือนง่าย แต่ในความเป็นจริง ใน PBS การอ่านและการเขียนสิ่งที่กระตุ้นให้เกิด reflow นั้นเกิดขึ้นในหลายที่และหลายเวลาในวงจรการทำงานของ PBS การจัดระเบียบโค้ด Javascript หลายพันบรรทัดใหม่จึงเป็นไปไม่ได้ในทางปฏิบัติ

โชคดีสำหรับเราที่ Wilson ได้ให้วิธีแก้ปัญหาไว้ในบทความของเขาด้วย เขาได้สร้างสคริปต์ยูทิลิตี้ขนาดเล็กขึ้นมาเพื่อช่วยในการจัดกลุ่มการอ่านและการเขียนเข้าด้วยกัน เรียกว่า FastDom

นี่คือโค้ดบางส่วนจาก Page Builder Sandwich ที่ใช้ FastDom และรวมถึงวิธีแก้ปัญหาที่ 1 ของเราข้างต้นด้วย:

// รวบรวมการอ่านจากเธรดอื่นเข้าด้วยกัน
fastdom.measure( function() {

	// ใช้ client rect ที่เราแคชไว้
	var rect = window.pbsGetBoundingClientRect( elem );

	// รวบรวมการเขียนจากเธรดอื่นเข้าด้วยกัน
	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 ทำคือการจัดกลุ่มการเรียกใช้ fastdom.measure ทั้งหมด และการเรียกใช้ fastdom.mutate ทั้งหมดเข้าด้วยกัน แล้วดำเนินการแบบอะซิงโครนัส ซึ่งเป็นการจัดกลุ่มการอ่านและการเขียนทั้งหมดเข้าด้วยกันอย่างมีประสิทธิภาพเพื่อลดการเกิด reflow ทั้งหมดในเบราว์เซอร์

วิธีแก้ปัญหาที่ 3: การเข้าถึง Style Objects นั้นสิ้นเปลืองทรัพยากร

เรายังพบด้วยความช่วยเหลือจาก jsPerf ว่าฟังก์ชันในการดึงค่าสไตล์บางฟังก์ชันทำงานได้เร็วกว่าฟังก์ชันอื่นๆ

ใน PBS โดยทั่วไปเราจำเป็นต้องดึงค่าปัจจุบันของสไตล์เฉพาะของเอลิเมนต์ ตัวอย่างเช่น โดยปกติเราจะใช้คำสั่งนี้เพื่อดึงค่า top margin:

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

แต่เราพบว่าการทำแบบนี้เร็วกว่ามาก:

var marginTop = elem.style.marginTop;​

อย่างไรก็ตาม elem.style.marginTop และ getComputedStyle นั้นแตกต่างกันมาก แบบแรกจะให้เฉพาะ inline styling ของเอลิเมนต์เท่านั้น และไม่ได้คำนึงถึงสไตล์ชีตใดๆ ส่วนแบบที่สองจะคำนึงถึงทั้ง inline styles และสไตล์ชีต

ยิ่งไปกว่านั้น แม้จะแคชผลลัพธ์ของ getComputedStyle ไว้แล้ว มันก็ยังช้ามากอยู่ดี ปรากฏว่าการเข้าถึงออบเจ็กต์สไตล์โดยตรงนั้นเป็นกระบวนการที่ช้า

โชคดีสำหรับเราที่ PBS มีการผนวก inline styles เข้ากับเอลิเมนต์ และเนื่องจาก inline styles มีลำดับความสำคัญสูงกว่าสไตล์ชีต (โดยส่วนใหญ่) จึงมีหลายกรณีที่การดึงค่าสไตล์แบบอินไลน์เพียงอย่างเดียวก็เพียงพอแล้ว ดังนั้นหากเรานำสองวิธีข้างต้นมารวมกัน เราก็จะได้โค้ดที่ทำงานได้เร็วกว่ามาก:

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

ผลลัพธ์ของการปรับปรุงโค้ด

วิธีแก้ปัญหาทั้ง 3 ข้อข้างต้นของเราต้องผ่านการวิจัย การทดสอบ และการนำไปใช้จริงอย่างมาก (และทำให้ฟีเจอร์ Fonts ของเราต้องเลื่อนการเปิดตัวออกไป) แต่ในท้ายที่สุดมันก็คุ้มค่ามาก

ย้อนกลับไปที่การทดสอบเบื้องต้นของเราในการเลื่อนเมาส์ไปมาบนหน้าจอเป็นเวลา 2 วินาที นี่คือไทม์ไลน์เก่าที่ดูแย่และกระตุก:

และนี่คือหน้าตาของไทม์ไลน์ใหม่ของเราหลังจากการปรับปรุงประสิทธิภาพ:

อย่างที่คุณเห็น มีสิ่งที่เป็นสีม่วงในกราฟน้อยลง และมีความแตกต่างที่เห็นได้ชัดในปริมาณกระบวนการที่เบราว์เซอร์กำลังประมวลผล สี่เหลี่ยมผืนผ้าด้านล่างก็บางลงด้วย ซึ่งหมายความว่ากระบวนการต่างๆ ทำงานได้เร็วกว่าเดิม

หากคุณเปรียบเทียบปริมาณพื้นที่สีม่วงที่สูง/กว้างในกราฟเก่าและกราฟใหม่ เราพบว่ามีประสิทธิภาพเพิ่มขึ้นถึง 200% เลยทีเดียว! เรายังเห็นประสิทธิภาพที่เพิ่มขึ้นโดยรวมทั้งหมดอีกด้วย ไม่เลวเลยทีเดียว!

ข้อคิดและสิ่งที่ได้เรียนรู้

พูดตามตรง เราไม่ได้คิดว่า PBS มีปัญหาด้านประสิทธิภาพมาก่อนจนกระทั่ง Ann ยกเรื่องนี้ขึ้นมา สาเหตุหลักเป็นเพราะเราใช้เครื่องที่เน้นสำหรับการพัฒนา และคอขวดด้านประสิทธิภาพก็ไม่ได้เห็นได้ชัดเจนขนาดนั้น เมื่อมองย้อนกลับไป การตรวจสอบประสิทธิภาพน่าจะเป็นหนึ่งในสิ่งสำคัญอันดับแรกๆ แต่อย่างน้อยตอนนี้ ตั้งแต่เวอร์ชัน 4.3 เป็นต้นไป ปัญหานี้ก็ได้รับการดูแลแก้ไขแล้ว ?

สำหรับผู้ที่ต้องการปรับปรุงประสิทธิภาพปลั๊กอิน WordPress ของตนเอง โดยเฉพาะอย่างยิ่งหากคุณมีการใช้ Javascript หนักๆ นี่คือข้อคิดที่ได้จากประสบการณ์ครั้งนี้:

  1. จำกัด/แคชการเรียกใช้พร็อพเพอร์ตี้/ฟังก์ชันที่กระตุ้นให้เกิด reflow ใช้สิ่งนี้เป็นแนวทาง: What forces layout/reflow. The comprehensive list.
  2. จัดกลุ่ม “reads” และ “writes” ของสไตล์ DOM เข้าด้วยกัน ใช้ FastDom เพื่อทำสิ่งนี้แทนคุณ
  3. ใช้ Timeline profiler ของ Chrome และตรวจสอบให้แน่ใจว่าเกิด reflow (ส่วนสีม่วง) ให้น้อยที่สุดเท่าที่จะเป็นไปได้