เว็บตัวเองยังทำไม่ดี จะไปรับงานลูกค้าได้ยังไง?
พูดตรงๆ — เราทำเว็บให้คนอื่นเป็นอาชีพ ถ้าเว็บของเราเองช้า หนัก แบกของเกินจำเป็น มันก็ไม่ต่างจากร้านซ่อมรถที่รถตัวเองพัง ไม่มีใครอยากจ้าง
เรื่องนี้ไม่ได้เริ่มจากใครบ่นว่าเว็บช้า แต่เริ่มจากตอน build แล้วเห็น warning สองตัวพร้อมกัน:
large-page-data warning
First Load JS shared by all อยู่ที่ 222 kB
สองบรรทัดนี้บอกตรงๆ ว่าเรากำลังยัดข้อมูลและ JavaScript ไปให้ browser มากเกินจำเป็น ตั้งแต่ก่อนคนจะได้ใช้เว็บด้วยซ้ำ
ในเว็บ Next.js แบบ Pages Router ปัญหาสองกลุ่มนี้อยู่คนละชั้น:
- Page data ใหญ่เกินไป = browser ต้อง parse ข้อมูลเยอะก่อน hydrate
- Shared JS ใหญ่เกินไป = ทุก route แบก dependency ชุดเดียวกัน แม้บางหน้าไม่ได้ใช้เลย
ถ้ารีบแก้โดยไม่แยกให้ชัดก่อน มักจะไปแตะส่วนที่เห็นง่าย แต่ไม่ใช่ส่วนที่แพงจริง สิ่งแรกที่เราทำเลยคือ เปิด build output ดูก่อน ไม่ใช่เดา
แยกปัญหาออกเป็น 2 คำถาม
ก่อนแตะโค้ด เราตั้งคำถามให้ชัดก่อน:
หน้าไหนส่งข้อมูลมากเกินที่ตัวเองใช้? — นี่คือปัญหา page-data หรือ route payload
อะไรถูกดันขึ้นไปอยู่ใน shared bundle ของทุกหน้า? — นี่คือปัญหา shared client bundle ที่กระทบ First Load JS ทั้งระบบ
การแยกแบบนี้สำคัญ เพราะถ้ามองแค่ตัวเลขรวม จะไม่รู้ว่าควรไปแก้ที่ข้อมูล ที่การโหลดโค้ด หรือที่โครงสร้างของ shared path
เฟสแรก — กด page data ให้เล็กลง
พอไล่ดูต้นตอของ large-page-data เจอ pattern ชัดเลยว่า บาง route รับข้อมูลกว้างกว่าที่ตัวเองใช้จริงเยอะมาก
เว็บเราหลายภาษา มีคอนเทนต์หลายกลุ่ม ปัญหาที่เจอคือ:
- ข้อความหลายหมวดถูกรวมอยู่ใน namespace เดียว
- route ที่ใช้แค่บางส่วน กลับรับ payload ทั้งก้อน
- locale fallback บาง case ทำให้มีข้อมูลซ้ำติดมาเกิน
ระบบยังทำงานถูกต้อง แต่มันส่งของไปเกิน สิ่งที่เราทำคือจัดขอบเขตข้อมูลใหม่ให้ตรงกับการใช้งานจริงของแต่ละหน้า:
- แยกเนื้อหาคนละบริบทออกจาก namespace กว้าง
- ให้แต่ละ route โหลดเฉพาะชุดข้อมูลที่จำเป็น
- ลดข้อมูลข้ามภาษาที่หน้านั้นไม่ได้ใช้จริง
- จัด content catalog ให้สอดคล้องกับ locale มากขึ้น
ผลลัพธ์:
| Metric |
ก่อน |
หลัง |
large-page-data warning |
มี |
หาย |
| Home page-data (TH) |
สูงกว่าเดิมมาก |
87,194 bytes |
| Home page-data (EN) |
สูงกว่าเดิมมาก |
42,936 bytes |
| Terms page-data (TH) |
สูงกว่าเดิมมาก |
125,482 bytes |
| Terms page-data (EN) |
สูงกว่าเดิมมาก |
59,720 bytes |
Warning หาย แต่สิ่งที่สำคัญกว่าคือ แต่ละ route พกข้อมูลเท่าที่จำเป็นจริงๆ แล้ว
พอ page data เบาลง ก็เห็นคอขวดตัวถัดไปชัดขึ้น
ปัญหา page-data ถูกกดลงไปแล้ว แต่ตัวเลขอีกชุดยังอยู่ — First Load JS shared by all ที่ 222 kB และ _app ที่ 199 kB
ตรงนี้คือจุดที่หลายทีมหยุด เพราะ warning หายแล้วก็รู้สึกว่า "น่าจะพอ" แต่ถ้ายังไม่ได้เปิด chunk ดูจริงๆ ก็ยังไม่รู้ว่าของหนักหลุดออกจาก critical path แล้วหรือยัง
เราเริ่มจากสมมติฐานว่า ถ้ามี UI บางส่วนที่ render ผ่าน global layout แล้วพา dependency หนักมาด้วย dependency ตัวนั้นก็จะถูกแชร์ไปทุกหน้า เราเลยเริ่มลดน้ำหนักส่วนที่อยู่ใน shared path ก่อน
แต่พอแก้รอบแรกเสร็จแล้วกลับไปดู chunk อีกรอบ พบว่า dependency หนักบางตัวยังอยู่ใน _app chunk เหมือนเดิม
แปลว่า สิ่งที่แก้ไปช่วยได้บางส่วน แต่ต้นเหตุจริงยังไม่หลุดออก
ถ้ายังไม่ได้ตรวจ artifact หลังแก้ ก็ยังบอกไม่ได้ว่าปัญหาหายแล้ว
เฟสสอง — ย้าย route-specific logic ออกจาก shared path
พอรู้ว่า bundle ยังไม่ลดจริง เราเปลี่ยนคำถามจาก "อะไรอยู่ใน layout" เป็น "อะไรถูก webpack ดันขึ้นมาแชร์ข้าม route ทั้งที่ไม่ควร"
สิ่งที่ทำ:
- ส่วนที่ควรโหลดเฉพาะบาง route ก็ให้โหลดเฉพาะตอนจำเป็น
- ลดโอกาสที่ dependency หนักของฟอร์มและ interaction เฉพาะหน้าจะถูกดันขึ้นไปเป็น shared bundle
- ใส่ loading state ที่เหมาะสมไว้ แทนที่จะยอมให้ทุกหน้าจ่ายต้นทุนตั้งแต่แรก
หลักง่ายๆ คือ ถ้าฟังก์ชันนั้นไม่ได้ใช้บนทุกหน้า ก็ไม่ควรโหลดบนทุกหน้า
ผลลัพธ์:
| Metric |
ก่อน |
หลัง |
First Load JS shared by all |
222 kB |
211 kB |
_app |
199 kB |
188 kB |
| Home route |
200 kB |
189 kB |
| Contact route |
199 kB |
188 kB |
| Job detail routes |
205 kB |
190 kB |
แต่ที่สำคัญกว่าตัวเลข คือตอนเปิด _app chunk ดูรอบสุดท้าย dependency หนักตัวที่ว่าไม่อยู่ใน shared chunk อีกแล้ว ต้นทุนถูกย้ายออกจาก critical path ได้จริง
สิ่งที่ได้เรียนรู้
Warning คือ instrumentation ฟรี — หลายทีมเห็น warning แล้วปล่อยผ่านเพราะ build ยังผ่าน แต่จริงๆ มันคือสัญญาณที่ framework บอกเราฟรีๆ ว่ามีต้นทุนบางอย่างกำลังสะสมอยู่
ปัญหา performance ส่วนใหญ่ไม่ได้มาจาก algorithm — ในเว็บเชิงคอนเทนต์และ marketing site ปัญหามักมาจากขอบเขตข้อมูลกับขอบเขตการโหลดโค้ดที่กว้างเกินจำเป็น ไม่ใช่ logic ซับซ้อน
Shared path = critical path — ทุกอย่างที่อยู่ใน global layout หรือ shared bundle มีผลต่อทุกหน้า ใส่ dependency หนักตรงนี้เท่ากับเก็บหนี้ไว้ทั้งระบบ
สมมติฐานต้องยอมถูกล้มได้ — เราเริ่มจาก hypothesis หนึ่ง แต่พอ artifact ไม่สนับสนุน ก็เปลี่ยนทันที งาน performance ที่ดีคือให้ข้อมูลจริงเป็นคนตัดสิน ไม่ใช่ยึดสมมติฐานเดิม
Optimization ต้องไม่สร้าง regression — เราไม่ได้มองแค่ bundle ลด แต่เช็คด้วยว่า build ผ่าน sitemap ยังสร้างได้ route สำคัญยัง render ปกติ warning เดิมไม่กลับมา นั่นคือ optimization แบบ production-minded
แล้วทำไมเรื่องนี้ถึงสำคัญกับธุรกิจ?
พูดตรงๆ ถ้าคุณกำลังจะจ้างบริษัทซอฟต์แวร์มาทำเว็บให้ แล้วเว็บของบริษัทนั้นเองยังช้า ยังหนัก ยังส่ง JS เกิน คุณจะมั่นใจได้แค่ไหน?
เราคิดแบบนี้เหมือนกัน เลยไม่ยอมปล่อยผ่านแม้แต่ warning เดียว เพราะทุก byte ที่เกินมีผลจริง:
- คนตัดสินเว็บภายใน 3 วินาทีแรก
- ช้าเกิน 5 วินาทีคือเสีย conversion
- คนไทยกว่า 70% ใช้มือถือเข้าเว็บ
- Google ใช้ Core Web Vitals เป็นปัจจัยจัดอันดับ
เว็บที่เร็วไม่ใช่ bonus มันคือหลักฐานว่าทีมที่สร้างมันใส่ใจในรายละเอียด
Lighthouse Scores หลังปรับปรุง

ตัวเลขจาก Lighthouse บนหน้า Insights ของเว็บเราเอง หลังจากทำงานรอบนี้เสร็จ — Performance 100, Accessibility 95, Best Practices 96, SEO 100
สรุป
เคสนี้เริ่มจาก warning เล็กๆ ใน build ที่หลายทีมมองข้าม เราเลือกไม่ปล่อยผ่าน เพราะถ้ายังทำให้ดีกว่านี้ได้ ก็ไม่มีเหตุผลที่จะหยุด
- ลด page-data ให้แต่ละ route รับเฉพาะสิ่งที่ต้องใช้
- ลด shared JS ให้โค้ดเฉพาะบางหน้าหลุดออกจาก critical path ของทุกหน้า
ผลสุดท้ายไม่ใช่แค่ตัวเลขที่ดูดีขึ้น แต่คือเว็บที่แบกของน้อยลงอย่างมีเหตุผล พร้อม scale ต่อโดยไม่พกต้นทุนแฝง และเป็นหลักฐานว่าเราใส่ใจ performance ของงานตัวเอง ก่อนจะไปพูดเรื่องนี้กับลูกค้า
หากทีมคุณกำลังเจอ warning แบบเดียวกัน หรือเว็บเริ่มโตจน build output ดูผิดปกติ — ปรึกษาทีม Enersys ได้ เราแก้ performance จากต้นเหตุ ไม่ใช่แค่กดคะแนนให้ดูดี
แหล่งข้อมูล