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