ลด blocking time ให้เหลือ 0 ด้วย Static Site Generator (Page Speed Optimize 1)
เนื่องจากที่ Fastwork มีเป้า OKR ภายในทีม Engineer ที่จะเพิ่ม speed ของเว็บให้โหลดเร็วขึ้น เพื่อช่วยทั้งในด้าน UX ที่ดีแถมยังได้คะแนน SEO เพิ่มไปในตัว
ทางทีมจึงได้แบ่งงานหลักๆที่จะ optimize
ตัวเว็บเป็น 2 ฝั่งคือฝั่ง Frontend และ Backend
ผมที่พอจะมีประสบการณ์การทำ optimize web มาบ้าง จึงได้รับมอบหมายให้ research ว่าจะมีวิธีอะไรบ้างที่ช่วยให้เว็บโหลดเร็วขึ้นจึงได้รับ task นี้มา และเห็นว่าน่าจะเป็นประโยชน์กับหลายๆคนที่สนใจ optimize web ครับ โดยในบทความนี้ผมจะเน้นไปที่ stack frontend นะครับ
Background Tech Stack ของ Fastwork
Frontend: React.js => Next.js
โดย version ก่อน upgrade ล่าสุดคือ 10 ครับ
Backend: อันนี้จะมีหลาย repo หน่อยครับมีทั้ง node.js
, golang
, nest.js
First Step with Lighthouse Score
ก่อนอื่นที่เราจะแก้ปัญหา เราต้องหาก่อนครับว่าเว็บของเรามีต้นตอของปัญหามาจากอะไรบ้าง และวัดผลมันออกมาให้ได้ เพื่อที่เราจะได้รู้ว่าสิ่งที่เราแก้ไปมันส่งผลกับเว็บของเราจริงๆ
โดยการวัดคะแนน lighthouse ก็ให้เน้นไปที่ category
performance
ครับ
ผมแนะนำว่าควรวัดคะแนน lighthouse จาก command line tool
ดีกว่า inspect tool
เพราะเราสามารถเก็บ config ต่างๆ และเก็บผล score ของเราไว้ใน repo ได้ เพื่อให้ง่ายต่อการวัดผลครับ
Problems
หลังจากที่ได้วัดคะแนน SEO ด้วย lighthouse แล้ว ปัญหาที่เจอของเว็บที่เป็นปัญหาที่สุดคือ
→ First Contentful Paint ←
การที่ browser ได้รับ first content
(ข้อมูลชุดแรกที่ตอบกลับจาก server frontend) ช้าครับ เกิดได้จากหลายกรณี แต่ถ้าใช้ Next.js ปัญหาที่เจอได้บ่อยๆคือ
- API ช้า: หน้าเว็บที่มี request มานั้น ต้องรอ api ตอบกลับก่อนส่งข้อมูลให้ browser บน frontend server
ซึ่ง api ที่เราเรียกในgetinitialprops
หรือgetserversideprops
นั้นช้า จึงทำให้เกิด blocking time ที่ server frontend และต้องรอให้ api ตอบกลับมาจึงค่อยส่งข้อมูลกลับไปให้ browser
โดยระหว่างที่กำลังรอการตอบกลับของ server frontend นี้ user ที่เปิดเว็บของเราจะเห็นแต่หน้าขาวๆที่ยังไม่มีข้อมูลอะไร ทำให้ UX ไม่ดีครับ
วิธีแก้มีหลายวิธีอยู่ ขึ้นอยู่กับความเหมาะสม
1.1 ลด response time
ของ api (ข้อนี้ต้องแก้จาก backend)
1.2 แยกการเรียก api ที่จำเป็นน้อยกว่าไปไว้ที่ client side
แทน
1.3 Build page ให้เป็น Static Site Generation
แทน Server side Rendering
ขยายความข้อ 1.3 พูดง่ายๆคือเปลี่ยน data fetching method
method ที่ next.js แนะนำให้ใช้ เพื่อ build page เก็บไว้เป็น static site
ข้อดีคือหน้าเว็บที่ถูก build เก็บไว้เป็น static จะไม่มี blocking time จาก api เลย คะแนน performance พุ่งกระฉูด
1.3.1 getStaticProps: วิธีใช้เหมือน getServersideProps, getInitialProps
เพียงแต่ api จะถูกเรียกแค่ตอน build bundle แล้ว api response จะถูก prerender เก็บไว้ใน file json
export async function getStaticProps({ params }) {
console.log("in fallback");
const { data } = await productService.getById(params.id); return {
props: {
product: data,
}, // will be passed to the page component as props
revalidate: 60,
};
}
1.3.2 getStaticProps + getStaticPaths: เพิ่มเติมจากข้อแรก หากเราต้องการ build หน้า page ที่เป็น dynamic ก็สามารถทำได้เช่นกัน ตัวอย่าง fastwork.co/product/:id
=> /product/1
, /product/2
, /product/3
เราก็สามารถใช้ method getStaticPaths
เพื่อบอกว่าเราจะ prerender หน้าไหนเก็บไว้ก่อนบ้าง
export async function getStaticPaths() { return {
paths: [
{ params: { id: "1" } },
{ params: { id: "2" } },
{ params: { id: "3" } },
],
fallback: "blocking",
};
}export async function getStaticProps({ params }) {
const { data } = await productService.getById(params.id); return {
props: {
product: data,
}, // will be passed to the page component as props
revalidate: 60,
};
}
***ความเจ๋งยังไม่หมดแค่นั้น
จากตัวอย่างด้านบน api ที่ถูก prerender เก็บเป็น static ไว้ ตอน build bundle time จะมี id: 1, 2, 3
เท่านั้น (ถูกกำหนดให้ build static จาก function getStaticPaths)
แต่หากมี user เข้า page/product/25
ก็จะทำการ fetch data แบบ serverside rendering ให้อัตโนมัติ และจะนำผลลัพธ์ที่ได้ไป generate เป็น static ไฟล์เก็บเพิ่มให้ไปเรื่อยๆ ซึ่งวิธีนี้เรียกว่า Incremental Static Regeneration
ยกตัวอย่าง
- นาย A เข้า page
/product/1
: server ตอบกลับแบบ static ทันทีเพราะ prerender/product/1
ไว้แล้ว - นาย B เข้า page
/product/30
: server จะทำการ fetch data แบบ serverside rendering เพราะ product_id: 30 ไม่ได้ถูก build เป็น static ไว้ - นาย C เข้า page
/product/30
หลังนาย B: server ตอบกลับแบบ static ทันทีเพราะหลังจากที่นาย B เข้า page:/product/30
แล้ว จะนำข้อมูลที่พึ่ง fetch มาไปเก็บเป็น static ไว้สำหรับใช้ใน request ถัดๆไป
ผลลัพธ์ bundle แบบ
Static Site Generation
ผลลัพธ์ที่ได้คือ bundle จะ build แยกเป็น html+json ไว้ โดยที่ไฟล์ json ก็คือ api response นั่นเอง
ผลลัพธ์ Lighthouse score บน web ที่ทำ POC ไว้
สำหรับบทความต่อไปจะเล่าการแก้ปัญหาของ First Contentful Paint ที่ช้า อีกข้อคือ การลด fist load file size ด้วย bundle analyzer
ครับ