ลดขนาดไฟล์ให้เล็กจิ๋วด้วย Bundle Analyzer (Page Speed Optimize 2)
จากบทความที่แล้วที่เราแก้ปัญหา First Contentful Paint ช้า ด้วยการ build page เป็น static site genertationไปแล้ว ในบทความนี้จะพูดถึงการลดขนาด first load file size
ปัญหาอีกข้อที่ทำให้ First Contentful Paint แย่คือ Large First load file Size
อธิบายคือ file javascript
ของเราที่ server Frontend ต้องส่งให้ browser ในครั้งแรกนั้นใหญ่เกินไป ทำให้การ render page ทำได้ช้าลง
จากรูปภาพด้านบน เป็นสรุป size ของ page แต่ละหน้าที่ next.js แสดงให้ดูหลัง build bundle เสร็จ โดยที่ column ขวาสุดจะแสดงให้เห็น firstload file size
ที่ browser จะได้รับไป render หน้าเว็บเป็นชุดข้อมูลแรก (สังเกตเลขสีแดง column ขวาสุดครับว่าก่อนที่เรา optimize ขนาดประมาณ 540 kB
)
และในกรอบสีแดง (first load js shared by all) คือ javascript common file
ที่จะถูก share ไปยังทุกหน้า Page ครับ
แต่หากเราดูข้อมูลแค่นี้ เรายังไม่สามารถทำการ optimize อะไรเพิ่มได้ เพราะข้อมูลน้อยเกินไป ผมจึงได้ใช้ bundle analyzer มาช่วยเพื่อให้วิเคราะห์ข้อมูลได้มากยิ่งขึ้น
npm install @next/bundle-analyzer
//package สำหรับ next.js หากใช้ตัวอื่นให้เสิช bundle-analyzer ครับ
โดยช่องสี่เหลี่ยมแต่ละกล่องจะสัมพันธ์กับความใหญ่ของ Package ยิ่ง package ไหนใหญ่ ให้เราสนใจกล่องนั้นเป็นพิเศษครับ
เมื่อเห็น analyzer แล้วหากเจอ package บางตัวที่ใหญ่เกินไปเราสามารถตัดสินใจได้ 2 แบบเพื่อลด size
- ใช้ package อื่นๆแทนที่เล็กกว่า หรือเลือกเขียนเองในบางส่วน วิธีนี้อาจต้องมีการแก้ไข code เก่าบางส่วนเพื่อให้ support package ใหม่ที่ใช้
- หากไม่มี package อื่นทดแทน หรือไม่ต้องการเปลี่ยน package ใหม่เพราะไม่ต้องการแก้ code เก่าเยอะ ก็ให้พยายามแยก package ที่ใหญ่มากๆ ออกจาก common javascript chunk เพื่อที่ไม่ให้ทุกหน้าต้องรับภาระโหลด package ไปด้วย แต่กลับไม่ได้ใช้ package นั้นครับ
ลองดูของจริงกันครับ โดยดูกล่องสีส้มที่ใหญ่สุดก่อนอันดับแรก
step 1: จัดการ moment.js
จะเห็นว่าไฟล์นี้ประกอบด้วยกล่องหลายใบซ้อนๆกันอีก โดยกล่องที่ใหญ่ที่สุดนั้นคือ node_modules
ประกอบด้วย moment ที่มี locale หลายภาษามากๆ
ซึ่งเราหลายๆคนรู้ดีกันอยู่แล้วว่า momentjs
เป็น package ที่ใหญ่มากๆ ทาง fastwork จึงตัดสินใจหา package อื่นทดแทนที่ชื่อว่า dayjs
และลบ momentjs
ออกทั้งหมดไปแล้ว
แต่ถึงอย่างนั้น ผมก็พึ่งเจอว่า ถึงเราไม่ได้เรียกใช้ moment โดยตรง แต่กลับมี package บางตัว ที่ไปเรียกใช้ moment มาอยู่ดี นั่นก็คือreact-dates
ครับ
แต่โชคยังดีที่ผมเคยอ่าน doc ของ next.js เจอว่า เค้าทำการ optimize moment ให้เราแล้ว โดยจะลบภาษาที่เราไม่ได้ใช้ออกให้ ทำให้ไฟล์นั้นเล็กลงพอสมควร เพียงแค่เราต้อง upgrade next.js version ครับ
yarn upgrade next@lastest react@lastest react-dom@lastest// รันโลด
โดยหากใครที่เริ่มสร้าง repo ใหม่ก็จะได้ feature นี้ไปทันที หรือหากใครต้องการ upgrade ลองไปปรับ code ตาม upgrade guide ได้เลยครับ อาจต้องใช้เวลาหน่อย
แต่พอ upgrade เสร็จแล้วก็คุ้มครับ ได้ feature ใหม่ๆ และ performance ที่ดีขึ้นเป็นผลพลอยได้ด้วยครับ ของ fastwork เป็นการ upgrade จาก 10 => 12 เรื่องที่ต้องแก้หลักๆคือ next.js เปลี่ยน Compiler จาก webpack เป็น Rust ที่ใช้ SWC แทน
ผมจะแก้ให้ run ได้ ทีละปมก่อนคือ upgrade จาก webpack4 => webpack5 ให้รันผ่านก่อน แล้วค่อยปรับให้ webpack5 => SWC ครับ หลังจากที่ ใช้ SWC แล้ว project compile ไวขึ้นชัดเจนทั้ง build dev mode และ build production mode
เมื่อ upgrade next.js เสร็จแล้วใส่ config ให้ optimize moment.js
// next.config.js {
...
excludeDefaultMomentLocales: true,
}* default เป็น true อยู่แล้วครับ แต่ใส่ไว้เพื่อความสบายใจ หรือหากคนอื่นมาอ่าน code จะได้ไม่งง ว่าทำไมภาษาของ moment จึงหายไป
ผลลัพธ์ของ step1 => locale ใน moment หายไป ขนาดของ package ก็เล็กลงไปด้วย
step 2: Code Spliting => แยก Package ที่ใช้กับแค่บางหน้าออกจาก common file chunk เพื่อลดภาระที่หน้าอื่นต้องโหลดไปด้วย แต่กลับไม่ได้ใช้งาน
package ที่ต้องการแยก: react-dates
, branch-sdk
โดยปกติแล้ว next.js จะทำ code spliting
ให้อยู่แล้วโดย base จาก route ในแต่ละหน้าครับ แต่เรายังสามารถทำ code spliting เพิ่มด้วยตัวเองได้โดยใช้ dynamic import
import dynamic from 'next/dynamic'
const HelloComponent = dynamic(() => import('../components/hello'))
function Home() {
return (
<div>
<Header />
<HelloComponent />
<p>HOME PAGE is here!</p>
</div>
)
}
export default Home
จากในตัวอย่างหาก HelloComponent
เรียกใช้ package react-dates
และbranch-sdk
อยู่ component เดียว เราก็ควรแยก component นี้ให้เป็น dynamic import เพื่อไม่ให้ package react-dates
และbranch-sdk
ถูก build ไปไว้ที่ common chunk ครับ
ผลลัพธ์ของ step2 => package react-date
, branch-sdk
รวมไปถึง momentjs
ที่มากับ react-date
ถูกแยกออกไปจาก common chunk เป็นที่เรียบร้อย (แยกไปอยู่ที่กล่องสีม่วงๆ 3 อันครับ)
step 3: Upgrade Firebase/Database
step นี้ค่อนข้างง่ายครับคือ upgrade firebase version จาก 8=>9 เนื่องจาก version 9 นี้ จะเป็นการ import แบบ Modular โดยจะโหลดเฉพาะ module ที่ใช้เท่านั้น
*อาจต้องมีการ refactor code นิดหน่อยครับ
เรามาดูผลลัพธ์ First Load File size กันครับ
- ก่อน Optimize: first load file size แต่ละหน้าขนาดประมาณ
540 kB
- หลัง Optimize: first load file size แต่ละหน้าขนาดประมาณ
300 kB
แถมให้อีกข้อ
→ Largest Contentful Paint ←
ปัญหาข้อนี้คือ element ที่แสดงบนหน้าเว็บที่ใหญ่ที่สุดแสดงช้าครับ ข้อนี้ส่วนใหญ่จะเกิดจากรูปภาพที่ใหญ่ที่สุดใน page นั้น render ช้าครับ
หากเราใช้ Next.js version 10
ขึ้นไป สามารถใช้ next/image
มาช่วย optimize image ของเราให้มีประสิทธิภาพดีขึ้น และอย่าลืมใส่ props priority
ลงไปเพื่อทำการ preload รูปที่ต้องการแสดงเป็นอันดับแรกด้วยครับ
หากใครใช้ next แล้ว แนะนำว่าต้องใช้ next/image
ครับ แล้วปัญหา score Largest Contentful Paint
หรือ Cumulative Layout Shift
ของรูปภาพ จะหายไปอย่างแน่นอนครับ
จริงๆแล้วการทำ Optimize web นั้นมีอีกหลายข้อที่ควรทำ แต่เนื่องจากในบทความนี้ ผมอยากแชร์สิ่งที่พึ่ง research และทำ POC มา แชร์ให้กับคนที่สนใจ เผื่อทำไปประยุกต์ใช้กับ web ตัวเองครับ หวังว่าคงเป็นประโยชน์กับหลายๆคนครับ