Suranart Niamcome
Tencent (Thailand)
Published in
7 min readApr 5, 2018

--

เมื่อเดือนสิงหาคมของปีที่ผ่านมา เว็บไซต์ Sanook.com ได้มีการปรับเปลี่ยน Technology Stack ครั้งใหญ่ ซึ่งทีมงานของเราต่างก็รู้สึกว่าได้เรียนรู้อะไรเยอะมากๆ ซึ่งหลายๆ อย่างนั้น คงจะหาไม่ได้จากการทำโปรเจกต์เล็กๆ

เมื่อ “เว็บๆ นึง” ไม่เท่ากับ “หนึ่งเว็บ”

เนื่องจากทีมงานของเราเป็นทีมที่ดูแลตัวเว็บไซต์ Sanook.com มาตลอดในช่วง 5 ปี ที่ผ่านมา เราเลยรู้ดีว่าปัญหาในแง่ของ Development และ Maintenance ของเว็บไซต์นี้คืออะไร…

จำนวนเว็บไซต์ย่อยๆ ที่ต้องดูแลนั้น มีเยอะมากๆ

เนื่องจาก Sanook.com เป็นเว็บ Portal เนื้อหาของเว็บไซต์ก็เลยมีความหลากหลายมากจนเราต้องแบ่งออกเป็นเว็บไซต์ย่อยๆ หลาย 10 เว็บไซต์ แล้วแต่ละเว็บนั้นก็มีอายุต่างกันออกไป บางเว็บไซต์อยู่มา 10 กว่าปี บางเว็บไซต์ก็เพิ่งมีมาได้ไม่กี่เดือน แน่นอนว่าภาษาและวิธีการเขียนนั้นแตกต่างกันโดยสิ้นเชิง...

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

ตัวอย่างที่เห็นได้ชัดเลยก็คือ แค่งานปรับสีทั้งเว็บให้เป็น Grayscale เว็บไซต์ทั่วไปอาจจะใช้เวลาเพียงแค่ไม่กี่นาที แต่เราใช้เวลาไปกว่า 4 ชั่วโมง…

แถมยังต้องรองรับทั้ง Mobile, Tablet และ Desktop

บางคนอาจจะสงสัยว่า แล้วมันเป็นปัญหาได้ยังไง ? ก็ทำให้เป็น Responsive Web สิ! คำตอบคือเราเคยใช้ท่านี้ไปแล้วเมื่อ 4 ปี ก่อน และมันไม่เวิร์ค…

การเลือกใช้วิธี Responsive Web ช่วยให้เราดูแลโค้ดเพียงแค่ชุดเดียวก็จริง แต่ข้อเสียก็คือ มันเหมาะกับเว็บไซต์ที่ไม่ค่อยจะซับซ้อนมากนัก ไม่งั้นสิ่งที่ผู้ใช้งานต้องโหลดไป อาจจะมีบางส่วนที่เค้าไม่ต้องการ…

จริงอยู่ที่เราสามารถเขียนโค้ดเช็คเพิ่ม เพื่อให้ผู้ใช้งานโหลดของมาเฉพาะ Device ที่ตัวเองใช้อยู่ได้ แต่ยิ่งโค้ดในส่วนนี้เยอะมากขึ้นเท่าไร ความซับซ้อนของโค้ดก็จะยิ่งเพิ่มขึ้นเท่านั้น

ก็นั่นแหละ ! ถึงบอกว่า RWD มันเหมาะกับเว็บที่ Simple …

หลังจากที่เราเรียนรู้ว่า Responsive Web ไม่ได้เหมาะกับทุกเว็บ เรากลับมาใช้ท่าเดิมๆ ซึ่งก็คือการทำเว็บแยกเวอร์ชันไปเลย ก็คือทำเวอร์ชัน Mobile แยกออกมาจากเวอร์ชัน Desktop

หลังจากที่พัฒนาเสร็จ เรารู้สึกว่าโค้ดของเรามีความซับซ้อนน้อยลงมากๆ แถมเว็บก็โหลดเร็วสุดๆ

แรกๆ ทุกอย่างดูเหมือนจะโอเค แต่เราก็พบว่าการทำเว็บแยกเวอร์ชันแบบนี้ มันก็มีข้อเสียเหมือนกัน…

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

คิดใหม่ทำใหม่!

จาก 2 ปัญหาหลักๆ ที่เรากำลังเจออยู่นั้น เราพบว่าทางออกนั้นง่ายมาก นั่นก็คือการยุบเว็บไซต์ย่อยๆ ทั้งหมด ให้เหลือเพียงแค่เว็บเดียว เวอร์ชั่นเดียว โดยที่มันจะต้อง…

“รองรับทุกขนาดหน้าจอได้... และยังโหลดเร็วอยู่ ”

อย่างที่เกริ่นไปก่อนหน้านี้แล้วว่า การทำ Responsive Web นั้น ยังไม่ตอบโจทย์ เพราะเราไม่สามารถคุม Requirement ให้ Simple ได้ ซึ่งการรวมโค้ดของเว็บเป็นสิบๆ เว็บ ไว้ด้วยกัน ก็ดูจะเป็นไอเดียที่ไม่ฉลาดเอาซะเลย ส่วนการจะใช้วิธีแยกเวอร์ชันระหว่าง Mobile กับ Desktop ก็คงจะเจอปัญหาแบบเดิมๆ

จะเห็นว่า Solution นั้นฟังดูเหมือนง่าย แต่การจะทำจริงนั้น ไม่ง่ายเลย…

ทางออกก็คือ วิธี Hybrid…

วิธีที่เราคิดออกก็คือ การเอาข้อดีของแต่ละท่ามารวมกัน โดยเราจะยึดท่า Responsive Web เป็นพื้นฐาน คือจะใช้ CSS เข้ามาช่วยในการจัดเรียงของ รวมไปถึงการปรับการแสดงผลของ UI ให้เหมาะกับขนาดหน้าจอต่างๆ โดยที่ Markup ยังคงเหมือนเดิม

แต่ในกรณีที่ UI หรือ Requirement ของเวอร์ชั่น Mobile กับ Desktop นั้นมีความแตกต่างกันมากๆ หรือพูดง่ายๆ ก็คือ ถ้า CSS มันเอาไม่อยู่ เราก็จะเอา User Agent เข้ามาเช็คด้วย

ถ้า UI ของ Mobile กับ Desktop มีความแตกต่างกันมากๆ เราก็จะสร้าง Custom Component ขึ้นมาแทน

หรือถ้าบางเว็บย่อย มีกล่องพิเศษ ที่ชาวบ้านเค้าไม่มีกัน เราก็จะใช้วิธีเช็คจาก URL เอา ทั้งนี้ก็เพื่อที่จะหลีกเลี่ยงการโหลดของเกินความจำเป็นนั่นเอง…

เรามีการดัก URL เพื่อที่จะโหลด Custom Component มาเฉพาะเวลาที่ต้องใช้เท่านั้น

แต่การจะผสม 2 ท่า นี้ เข้าด้วยกัน เราจะต้องระวังให้มาก เพราะถ้าทำไม่ดี โอกาสที่โค้ดจะเละกว่าเดิมนั้นมีสูงงง…

ก็แค่มองทุกอย่างให้เป็น Component !

เราเปลี่ยนจากการมองหน้าเว็บเป็นหน้าๆ มามองเป็นกล่องย่อยๆ แทน แน่นอนว่าเวลา Dev เราก็จะค่อยๆ ทำไปทีละกล่อง แล้วค่อยเอาไปวางตามหน้าต่างๆ ซึ่งกล่องต่างๆ ที่จะหยิบมาวางนั้น ก็จะมีทั้งแบบที่เป็น Responsive ในตัว กับแบบ Custom ที่ทำมาเพื่อใช้บนบาง Device หรือในบางเว็บย่อยโดยเฉพาะ…

จาก PHP สู่ JavaScript

ในการเปลี่ยนมุมมองครั้งใหญ่นี้ เราเลือกที่จะเปลี่ยนภาษาที่ใช้เขียนด้วย ถึงแม้ว่า Sanook.com นั้นจะเขียนด้วยภาษา PHP มาตลอดเวลาเกือบ 20 ปี ที่ผ่านมา และ Developer ส่วนใหญ่ของเราก็ถนัดภาษานี้กันทั้งนั้น แต่เราก็ยังเลือกที่จะเปลี่ยน นั่นก็เพราะว่า…

“ ท่าที่เราคิดได้… มันเข้ากับภาษา JavaScript สุดๆ “

ถึงแม้ว่า Solution ที่เราคิดออก จะสามารถเขียนด้วย PHP เหมือนเดิมก็ได้ แต่เราคิดว่าการมอง UI แบบนี้ มันเหมาะกับภาษา JavaScript มากกว่า…

บ่อยครั้งที่ UI ของเราไม่ได้จบแค่ฝั่ง Server เพราะมันมักจะมีพวก Interaction ที่ฝั่ง Client ด้วย ซึ่งมันทำให้เราต้องมานั่งเขียน Logic คล้ายๆ กัน ทั้งภาษา PHP และ ภาษา JavaScript ซึ่งเราไม่อยากเสียเวลาไปกับเรื่องพวกนี้อีกแล้ว…

เราเปลี่ยนมาเขียนเว็บด้วย Node.js แทน เพราะมันทำให้เราสามารถใช้ภาษา JavaScript ที่ฝั่ง Server ได้ ถึงแม้ว่า Node.js อาจจะใหม่สำหรับบางคนในทีม แต่มันก็คือ JavaScript ดีๆ นี่เอง และนั่นคือสาเหตุที่ทำให้เราใช้เวลาเรียนรู้ไม่นานนัก…

สร้าง UI ด้วย React

ในส่วนของการสร้างกล่องต่างๆ ทีมของเราตัดสินใจเลือกใช้ React ด้วยเหตุผลง่ายๆ ก็คือ “Syntax ของมันสมเหตุสมผลดี”

จริงอยู่ที่การเลือกใช้ React มาทำ UI อาจจะมี Learning Curve ที่ค่อนข้างจะสูงอยู่บ้าง (จริงๆ สูงเพราะบรรดา Tools ต่างๆ ที่ต้องใช้ร่วมกับมัน) แต่เราก็เลือกที่จะใช้ React อยู่ดี เพราะเรารู้สึก​ “ถูกจริต” กับมัน มากกว่าตัวอื่นๆ

บางทีเราก็ต้องตัดสินใจเลือก โดยดูว่าตัวไหนที่เราจะอยู่กับมันได้นานๆ…

ทำเว็บคอนเทนต์… อย่าลืมเรื่อง SEO !

อีกสิ่งหนึ่งที่เราต้องเรียนรู้ก็คือเรื่องของการทำ Server-Side Rendering ที่จะช่วยให้เนื้อหาของเว็บเรา ถูก Render มาตั้งแต่ฝั่ง Server ถึงแม้ว่าเราจะเสียเวลาในการเรียนรู้มันไปเยอะพอสมควร แต่ถ้าเทียบกับสิ่งที่ได้กลับมาแล้ว มันคุ้มค่ามากๆ

ตัว API เอง ก็มีปัญหาไม่แพ้กัน…

ในส่วนของ REST API ที่เราใช้อยู่เดิมก็มีปัญหาเยอะเช่นกัน ซึ่งปัญหาที่เรามักจะเจอกันเป็นประจำเลยก็คือ…

ข้อมูลขาด / ข้อมูลเกิน

เนื่องจากข้อมูลที่ได้จาก API เดิม หน้าตาแทบจะเหมือนกับข้อมูลที่เก็บเอาไว้ในฐานข้อมูลเลย คือจะไม่ค่อยมีการปรับแต่งข้อมูลให้พร้อมกับการนำไปใช้เท่าไหร่ เวลาจะนำไปแสดงผลที ก็อาจจะมีการดึงข้อมูลหลายครั้ง แถมในแต่ละครั้งก็อาจจะต้องรอข้อมูลจาก Request ก่อนหน้าอีกด้วย…

อีกปัญหาที่เราเจอบ่อยไม่แพ้กันเลยก็คือ ข้อมูลมันชอบมาเยอะเกินความจำเป็น คือบางทีเราต้องการแค่ไม่กี่ Field ไง แต่ API ดันส่งมาซะหมดเลย ซึ่งมันไม่ดีเลยในแง่ของ Performance

Docs ไม่มี / ไม่ละเอียด

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

เนื่องจาก API ของเรานั้นไม่ได้ใช้กันแค่ภายในทีม การทำ Docs จึงถือเป็นสิ่งที่สำคัญมาก แรกๆ เวลามีแก้อะไร เราก็มาอัพเดท Docs กัน แต่เมื่อ API มีความซับซ้อนมากขึ้นเรื่อยๆ Docs ก็เริ่มจะหลุดๆ บ้าง แถมเวลาที่เสียไปกับการเขียน รวมไปถึงการอัพเดท Docs นั้น ก็ไม่ใช่น้อยๆ เลย…

เมื่อ GraphQL เข้ามาในชีวิต

เราได้ยินชื่อของ GraphQL มานาน แต่ก็ยังไม่เคยลองใช้สักที เราตัดสินใจลองเสียเวลากับมันดู และพบว่าเจ้า GraphQL นี่ล่ะ ที่จะมาช่วยแก้ปัญหาของเรา…

จุดเด่นของ GraphQL ก็คือ มันช่วยให้เราสามารถดึงเฉพาะข้อมูลที่ต้องการจริงๆ ได้ แน่นอนว่าขนาดของข้อมูลก็จะเล็กลง แถมเรื่องของ Docs นี่ก็ตัดทิ้งไปได้เลย เพราะการที่จะสร้าง GraphQL API ได้ เราจะต้องเขียน Schema ขึ้นมาซะก่อน ซึ่งนั่นล่ะ Docs ของเรา…

แต่เมื่อได้ศึกษา GraphQL อย่างจริงจังแล้ว สิ่งที่ทำให้เราหลงรัก GraphQL สุดๆ กลับไม่ใช่เรื่องพวกนั้นเลย…

Just ‘What’ Not ‘How’

สิ่งนั้นก็คือแนวคิดที่เรียกว่า “Declarative” ของ GraphQL ที่แนะนำให้เราออกแบบ Schema โดยยึดหลักที่ว่า…

“ ทำให้ Client เอาข้อมูลไปใช้ได้ง่ายที่สุด ”

หรือพูดง่ายๆ ก็คือ เราจะทำให้ Client แค่อ้าปากรอข้อมูล ที่เหลือจะเป็นหน้าที่ของ API ทั้งหมด อย่างสมมติเราจะปรับ Format วันที่ให้กับข้อมูลใดข้อมูลหนึ่ง เราก็ควรจะส่ง Format ที่ต้องการไปให้ GraphQL Server ตั้งแต่ตอนยิง Query เลย ไม่ใช่เอาข้อมูลดิบมาทำที่ฝั่ง Client เอง (ผลพลอยได้ก็คือ Bundle ของแอปจะมีขนาดเล็กลง) ซึ่งเราพบว่าการทำแบบนี้แหละที่ส่งผลดีกับทีมเราเต็มๆ

Development Workflow ในแบบที่ควรจะเป็น

Front-End Developer แสนสบาย

ลองนึกภาพการเขียน React Component ที่แค่เอาข้อมูลที่พร้อมใช้งานแล้วจาก Props มาแปะตามจุดต่างๆ นั่นล่ะ หน้าที่ของ Front-End Developer

ทีมอื่นไม่ต้องมาเขียน Logic เดียวกันซ้ำๆ

เนื่องจาก Sanook! ไม่ได้มีแค่เว็บเพียงอย่างเดียว การทำ API ให้เป็นแบบ Declarative จึงทำให้ Developer จากทีม Mobile App ได้ประโยชน์ไปด้วย…

เสริมพลังให้ GraphQL ด้วย Apollo !

เรายังได้มีการนำ Apollo เข้ามาช่วยในการทำงานกับ GraphQL ด้วย โดยสิ่งที่เราขอให้ Apollo ช่วยนั้น หลักๆ จะเป็นเรื่องของ Experience…

Developer Experience

โดยปกติแล้ว ทีมเราจะแบ่งหน้าที่ออกเป็น 3 ส่วน ด้วยกัน คือทำ API คนนึง ทำ UI คนนึง แล้วก็ผูกข้อมูลจาก API เข้ากับ UI คนนึง โดยคนที่ทำ UI จะเป็น Front-End Designer ซึ่งจะมีความถนัดในด้าน HTML/CSS เป็นหลัก ส่วนคนที่ผูกข้อมูลนั้นจะเป็น JavaScript Developer

แต่หลังจากที่เราเอา Apollo เข้ามาช่วย การทำงานกับ GraphQL ก็กลายเป็นเรื่องง่าย… มันง่ายซะจนเราสามารถสอนให้คนที่ทำ UI กลายเป็นคนผูกข้อมูลด้วยเลยได้

นอกจาก Apollo จะช่วยในเรื่องของการทำ Data Binding แล้ว อีกหนึ่งฟีเจอร์ที่เข้ามาช่วยให้ Workflow ของทีมเราไหลลื่นสุดๆ เลยก็คือ การใช้ Apollo ทำ Mock Data ให้กับ API

ลองนึกภาพคนทำ UI กับคนทำ API มานั่งกาง Requirement แล้วคุยกันว่าอยากจะให้ข้อมูลจาก API ออกมาหน้าต่างอย่างไร แล้วพอช่วยกันออกแบบเสร็จ API ก็จะใช้งานได้ทันที (ถึงแม้ว่าข้อมูลที่ได้ไปจะเป็นข้อมูลจำลองก็เถอะ…) นั่นหมายความว่า คนทำ UI จะสามารถเริ่มงานได้โดยไม่ต้องรอให้ API ทำเสร็จก่อนนั่นเอง…

User Experience

เรามีการนำฟีเจอร์ Caching ของ Apollo มาช่วยในเรื่องของ UX ด้วย อย่างที่หน้าอ่านคอนเทนต์ เราจะมีการทำ Over-fetching เล็กน้อย คือจะดึงข้อมูลบางส่วนเกินมานิดนึง แล้วเก็บไว้ใน Cache หรือพูดง่ายๆ ก็คือ เรามีการ Prefetch

ข้อดีก็คือ พอถึงเวลาที่ User เค้าต้องการข้อมูลนั้น หน้าเว็บก็จะพร้อมใช้งานทันที แต่ข้อเสียก็คือ หาก User ไม่ได้ไปหน้าที่เรา Prefetch เอาไว้ มันก็จะเป็นการ “ดึงฟรี” และนี่เป็นสาเหตุที่เรายอมเสียเวลาไปกับการทำ Usability Testing เพราะเราอยากรู้ว่า พอ User เค้าเข้ามาแล้ว เค้ามักจะไปไหนต่อนั่นเอง…

แต่การเอาทุกอย่างมารวมกันนั้น ไม่ใช่เรื่องง่าย…

เนื่องจากเราเปลี่ยน Stack ของเว็บใหม่หมด ตัวละครใหม่จึงเยอะเอามากๆ การจะเอาทุกอย่างมาทำงานร่วมกันนั้นไม่ใช่เรื่องง่ายเลย…

เราเคยคิดจะหา Bolierplate ที่ใช้ Stack คล้ายๆ กันเข้ามาช่วย แต่สุดท้ายเราก็ไม่อยากจะใช้อะไรที่เราไม่ได้เข้าใจการทำงานของมัน เราเลยเลือกที่จะวางโครงสร้างทั้งหมดขึ้นมาเอง อาจจะเสียเวลาเยอะหน่อย แต่เราว่ามันน่าจะยั่งยืนกว่า…

เราพยายามไปไล่ดู Boilerplate ดังๆ ว่าเค้ามีวิธีคิดยังไง แก้ปัญหาที่เราเจออยู่อย่างไร เราพยายามทำความเข้าใจ แล้วนำข้อดีของแต่ละตัวมารวมกันจนได้ Boilerplate ที่เหมาะกับงานของเราสุดๆ ทุกอย่างเป็นไปได้สวย แต่พอผ่านไปได้สัก 3 เดือน เท่านั้นแหละ เราก็พบปัญหา…

JavaScript Fatigue

เนื่องจากเทคโนโลยี (โดยเฉพาะ JavaScript) มันเปลี่ยนแปลงเร็วมากๆ Library บางตัวออกมาได้ไม่กี่เดือน ก็มีตัวที่ดีกว่าออกมาแทนที่ หรือบางท่าเดือนนี้ดี เดือนหน้ากลายเป็น Anti-Pattern ก็มีให้เห็น…

เป็น Developer สมัยนี้ต้องทำใจ…

เราพบว่าการใช้ Boilerplate ที่สร้างขึ้นเอง ช่วยให้เราเข้าใจแก่นแท้ของ Stack นี้ได้เป็นอย่างดี แต่ข้อเสียก็คือ เราต้องมาคอยอัพเดท Boilerplate ของเราให้ทันโลกอยู่เสมอ เราก็เคยคิดนะว่า “แล้วถ้าไม่อัพเดทตามได้มั้ย… ก็ปล่อยมันไปสิ” แต่สุดท้าย… เราก็ทนปล่อยโค้ดแย่ๆ เอาไว้ไม่ได้จริงๆ …

เชื่อว่าหลายๆ คนก็คงคิดเหมือนกับเรา พอท่าใหม่ๆ มันออกมา ท่าที่เราใช้อยู่มันกลายเป็นน่าเกลียดไปเลย ซึ่งแรกๆ มันก็พอปรับตามไหวอยู่หรอก แต่พอนานๆ เข้า เราก็พบว่าเราอยากเอาเวลาไปโฟกัสที่เนื้องานมากกว่า…

Next.js น่าจะมาเร็วกว่านี้ !

จริงๆ ก่อนหน้าที่เราจะเลือกทำ Boilerplate เอง เราก็เคยดูๆ พวก Framework ไว้บ้างเหมือนกัน แต่ตอนนั้นยังไม่มีตัวไหนที่เราถูกใจเลย จนในที่สุด เราก็มาเจอตัวที่ค่อนข้างจะตอบโจทย์งานของเราในหลายๆ ด้าน ชื่อของมันคือ Next.js

เราตัดสินใจเปลี่ยนโครงสร้างที่เราวางขึ้นมาเองทั้งหมด มาใช้ Next.js แทน ตั้งแต่นั้นมา หากเทคโนโลยีที่เราใช้อยู่มีการเปลี่ยนแปลง เราก็แค่อัพเดทเวอร์ชันของ Next.js เท่านั้นเอง ซึ่งตลอดเวลา 1 ปี ที่ผ่านมา เรารู้สึกว่าเราคิดถูกนะ ไม่ว่า Boilerplate ตัวนั้นจะดีแค่ไหน แต่ถ้าจะเอามาทำงานจริงๆ แล้ว การเลือกใช้ Framework ที่มีการอัพเดทอย่างต่อเนื่องนั้น เหมาะสมกับงานและขนาดของทีมเรามากกว่า…

Dan Abramov แนะให้คนเลิกทำ Boilerplate แล้วหันมาสร้าง Tool อย่าง CRA หรือ Next.js แทน

จริงอยู่ที่ตอนนั้น Next.js เองก็ยังมีอะไรที่ทำให้เราหงุดหงิดอยู่บ้าง บางส่วนเรายังคิดเลยว่าเราออกจะเขียนดีกว่า Next.js ด้วยซ้ำไป แต่สุดท้ายแล้ว เราก็เลือกที่จะใช้มันอยู่ดี เพราะ Community ของ Next.js นั้นค่อนข้างจะ Active และเราคิดว่า Issue หลายๆ อย่าง กำลังจะถูกแก้ในอนาคต…

อีกสิ่งหนึ่งที่เราโอเคหลังจากที่เปลี่ยนมาใช้ Next.js ก็คือ เมื่อทีมมีสมาชิกใหม่เข้ามาเพิ่ม ถ้าเค้าเป็น Next.js อยู่แล้ว เค้าก็แทบจะเริ่มงานได้ทันทีเลย ในทางกลับกัน หากเรายังใช้โครงสร้างเดิมที่เราคิดขึ้นมาเองอยู่ น้องใหม่ก็คงจะต้องใช้เวลาทำความเข้าใจอยู่พอสมควร…

เตรียมความพร้อม ก่อนลงมือทำ…

ต้องบอกก่อนว่า ก่อนที่เราจะ “กล้า” เอา Stack นี้ มาใช้กับ Sanook.com เราได้มีการทำ Research รวมถึงการทำโปรเจกต์เล็กๆ เพื่อซ้อมมือกันอยู่เป็นปี ซึ่งมันนานจนเราค่อนข้างมั่นใจว่า Stack นี้ เราเอาอยู่แน่นอน และในที่สุด เราก็ได้ไฟเขียวให้เริ่มทำโปรเจกต์นี้เมื่อช่วงต้นปี 2017

เว็บใหญ่ขนาดนี้ เปลี่ยนทีเดียวเลยไม่ได้…

ผ่านไป 5 เดือน เราก็พร้อมที่จะ Launch โปรเจกต์นี้แล้ว แต่เพื่อเป็นการลดความเสี่ยง เราจึงตัดสินใจที่จะทยอยเปลี่ยนทีละเว็บๆ โดยจะเริ่มจากเว็บที่ไม่ได้ใหญ่มากก่อน เรายังได้มีการทำ A/B Testing ด้วย เพื่อที่จะได้มั่นใจว่าเวอร์ชันใหม่นี้ มันดีขึ้นกว่าเดิม…

จนมาถึงวันนี้ Sanook.com ก็ได้เปลี่ยนมาใช้ Stack ใหม่ เกือบจะครบ 100% แล้ว แต่ใครจะรู้ว่าระหว่างทางมันไม่ได้โรยด้วยกลีบกุหลาบเลย…

Sanook.com เวอร์ชันปัจจุบัน

ทฤษฎีกับปฏิบัติ มันช่างต่างกัน…

ตลอดระยะเวลา 1 ปี กับอีก 3 เดือน ในการทยอยเปลี่ยน Stack เรารู้สึกว่าความรู้จากใน Docs หรือในบทความที่เค้าแชร์ต่อๆ กันมา มันใช้ไม่ได้จริงเสมอไป และประสบการณ์ที่ได้มาในช่วงเตรียมความพร้อมกลับช่วยได้ไม่มากอย่างที่คิด…

บางอย่างเราคิดว่ารู้ดีแล้ว แต่จริงๆ เราไม่รู้อะไรเลย…

บางทีเราก็แคร์ Developer Experience มากเกินไป

จริงอยู่ที่การออกแบบ Schema นั้น ควรจะคำนึงถึงความสะดวกในการนำข้อมูลไปใช้มากกว่าความยากง่ายในการเขียน Resolver…

แต่ในบางครั้ง เรากลับพบว่าการออกแบบที่ตามใจ Front-End Developer มากจนเกินไปนั้น อาจส่งผลกับ Performance มากจนคาดไม่ถึง บางทีเราก็ต้องชั่งน้ำหนักให้ดี เพราะการปรับลำดับชั้นของ Schema เพียงนิดเดียว อาจทำให้ Response Time ของ API ดีขึ้นมาก โดยที่ Developer Experience เอง ก็ไม่ได้แย่ลงมากนัก…

Apollo ไม่ได้เกิดมาเพื่อทำให้เว็บเร็วขึ้น

บางคนอาจจะคิดว่า Apollo ช่วยทำให้เว็บเราโหลดเร็วขึ้น แต่เรื่องนี้มันก็ไม่ได้จริงเสมอไป จริงอยู่ที่เมื่อเวลาผ่านไป Store ของ Apollo จะฉลาดขึ้นเรื่อยๆ จนถึงขั้นทำให้ไม่เกิด Request ใหม่ เพราะทุกอย่างมันถูก Cache ไว้หมดแล้ว

แต่จริงๆ แล้ว Apollo มีระบบ Caching ก็เพื่อที่จะทำให้การแสดงผลข้อมูลเป็นไปอย่างเที่ยงตรง แม่นยำ หรือพูดง่ายๆ ก็คือ ข้อมูลชุดเดียวกัน จะต้อง Sync กันอยู่เสมอ ถึงแม้ว่าจะแสดงอยู่คนละหน้าก็ตาม นั่นแปลว่ามันไม่ได้ถูกออกแบบมาเพื่อให้เร็วตั้งแต่แรกอยู่แล้ว…

ซึ่งจะสอดคล้องกับการที่เราพบว่า การดึงข้อมูลจาก GraphQL API โดยไม่ผ่าน Apollo นั้น เร็วกว่าอย่างเห็นได้ชัด ก็แหงล่ะ… มันไม่ต้องผ่านการทำ Normalization ก่อนนี่นา…

เรายังพบอีกว่า ยิ่ง Store ของ Apollo โตขึ้นมากเท่าไร การทำ Normalization ก็ยิ่งใช้เวลาเยอะมากขึ้นเท่านั้น ซึ่งอาการนี้จะเห็นได้ชัดกับ Device ที่ไม่ได้มีความเร็วมากนักอย่างโทรศัพท์มือถือ รวมไปถึง Device ที่ยังใช้ Web Browser เก่าๆ

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

Over-Fetching ก็ไม่ได้แย่เสมอไปนะ…

โดยปกติแล้ว ถ้าคอนเทนต์ไหนมีภาพประกอบเยอะ เราก็จะทำหน้าอัลบั้มภาพแยกออกมาต่างหาก แล้วก็ใส่ Link ทางเข้าเอาไว้ที่ด้านล่างของตัวคอนเทนต์…

กล่องอัลบั้มภาพที่อยู่ใต้คอนเทนต์ จะมี Link ไปยังหน้าอัลบั้มภาพแบบเต็ม
หน้าอัลบั้มภาพ

ถึงแม้ว่า 2 หน้านี้ จะแสดงข้อมูลต่างกัน แต่เราก็เลือกที่จะออกแบบโดยใช้ Query เดียวกันเป๊ะๆ ซึ่งการทำแบบนี้จะมีข้อดีตรงที่การเปลี่ยนหน้าไปมาระหว่าง 2 หน้านี้ จะไม่มี Request เกิดขึ้นเลย เนื่องจากข้อมูลทั้งหมดอยู่ใน Store ของ Apollo แล้ว ที่เรากล้าตัดสินใจแบบนี้ ก็เพราะเรารู้มาว่าหลังจากที่ User อ่านคอนเทนต์เสร็จ เค้าก็มักจะเข้าไปดูหน้าอัลบั้มภาพต่อ…

แต่ถ้าสมมติเค้าเข้ามา แล้วไม่ได้กดเข้าไปดูหน้าอัลบั้มภาพล่ะ ? ไม่เป็นไร… เราก็ถือซะว่าเค้าเข้ามาช่วยทำ Cache ในส่วนของ API ให้กับคนอื่นๆ ที่จะเข้ามาดูหน้าอัลบั้มภาพโดยตรงละกัน ซึ่งสำหรับเว็บ Sanook.com แล้ว ก็ถือว่ามี User ที่เข้าตรงมาเลยแบบนี้เยอะอยู่…

เราเรียนรู้ว่า การบริหาร Over-fetching ดีๆ นั้น สามารถลด Request ที่จะถูกส่งไปยัง GraphQL Server ได้เยอะมากๆ เลย การยอมดึงข้อมูลเกินความจำเป็นมานิดนึง เพื่อแลกกับ Request ที่ลดลงมหาศาล เราว่ามันคุ้มค่านะ…

พยายาม Reuse ของใน Cache

ที่หน้าอ่านคอนเทนต์ของ Sanook.com จะมีกล่องที่เอาไว้แสดงเนื้อหาล่าสุดของหมวดนั้นๆ อยู่ แต่ถ้าคอนเทนต์ที่ User กำลังเปิดอ่านอยู่นั้น ใหม่จนไปเข้าข่ายที่จะแสดงอยู่ในกล่องนี้ เราก็จะต้อง Exclude คอนเทนต์นั้นๆ ออกด้วย เพราะเราไม่อยากจะแนะนำคอนเทนต์ซ้ำนั่นเอง…

คอนเทนต์ที่แสดงในกล่องนี้ จะต้องไม่ใช่คอนเทนต์ที่ User กำลังอ่านอยู่

เราใช้วิธีออกแบบ Query ให้รองรับการ Exclude คอนเทนต์ด้วย ID ทุกอย่างดูจะไปได้สวย แต่อยู่มาวันนึง เรากลับรู้สึกว่าโค้ดตรงนี้มันทะแม่งๆ

ใช่แล้ว… เราพบว่าที่กล่องนี้จะมีการยิง Query ใหม่ทุกครั้งที่มีการเปลี่ยนหน้าอ่านคอนเทนต์ จริงอยู่ว่า “ก็มันเปลี่ยนหน้าแล้วอ่ะ… ยิงใหม่จะเป็นไรไป…” แต่อย่าลืมว่า สิ่งเดียวที่กล่องนี้เปลี่ยนไปก็คือ คอนเทนต์ที่จะถูก Exclude ออก…

เพื่อที่จะแก้ปัญหานี้ เราเลือกที่จะหยิบ Over-Fetching มาใช้อีกครั้ง เราลบการ Exclude ที่ Query ออก แล้วใช้วิธีเพิ่ม Limit ขึ้นมา 1 เพื่อที่จะดึงคอนเทนต์ให้เกินมา 1 อัน แล้วค่อยนำผลลัพธ์ที่ได้มา Exclude คอนเทนต์ที่ User กำลังอ่านอยู่ออก จากนั้นก็ค่อยตัดคอนเทนต์ให้เหลือแค่จำนวนที่จะนำไปแสดงในกล่อง…

จะเห็นว่าวิธีนี้ ทำให้ Query ของกล่อง “เนื้อหาล่าสุด” มีหน้าตาเหมือนกัน ไม่ว่า User จะอ่านคอนเทนต์ไหนอยู่ก็ตาม แน่นอนว่าพอแต่ละหน้าใช้ Query เดียวกัน เราก็จะสามารถดึงเนื้อหาในส่วนนี้ได้จาก Store ของ Apollo และมันก็ทำให้ Throughput ของ GraphQL Server ลดลงไปเกือบครึ่ง…

ระวังจะโหลดของที่ไม่จำเป็นมาโดยไม่รู้ตัว

เรารู้ดีว่าใน Bundle ควรจะมีแต่ของที่จำเป็นจริงๆ เท่านั้น แต่เมื่อได้ลองใช้ Tool อย่าง webpack-bundle-analyzer ตรวจสอบดูแล้ว เราก็พบว่ามีอยู่จุดนึง ที่เรายังพลาดไป…

มันคือการที่เราเก็บพวก Title, Description, Keywords ของหน้าต่างๆ เอาไว้ในไฟล์เดียวกัน เพื่อที่จะหาได้สะดวก เวลาจะปรับอะไร…

แต่เราลืมไปว่าเว็บย่อยของเรามี 10 กว่าเว็บ พอนานเข้า ไฟล์นี้ก็บวมขึ้นเรื่อยๆ และที่สำคัญ… โค้ดพวกนี้มันเข้าไปอยู่ใน Bundle หลัก นั่นแปลว่า User จะต้องมาโหลดข้อมูล Meta ของทุกๆ เว็บ ถึงแม้ว่าเค้าจะเข้ามาดูเพียงแค่หน้าเดียวก็ตาม ซึ่งจริงๆ แล้ว มันควรจะกระจายไปตาม Chunk ของ Route ต่างๆ มากกว่า…

webpack-bundle-analyzer ช่วยบอกเราว่า Bundle ใหญ่เพราะอะไร ?

ไม่ควรทำ Code-Splitting ที่ระดับ Route (เท่านั้น)

โดยปกติแล้ว Next.js จะทำ Code-Splitting โดยการแยก Chunk ตาม Route ให้เราโดยอัตโนมัติ ซึ่งวิธีนี้มันก็โอเคในระดับหนึ่ง…

แต่เนื่องจากโจทย์ของโปรเจกต์นี้ค่อนข้างท้าทาย คือเราจะต้อง Handle 10 กว่าเว็บ ที่มี Requirement บางส่วนไม่เหมือนกัน ด้วย Codebase เดียวกัน เราพบว่า Code-Splitting ที่ Next.js ทำมาให้นั้น ยังไม่ดีพอ…

ตัวอย่างที่เห็นชัดๆ เลยก็คือ มันจะมีหน้าอ่านคอนเทนต์ของเว็บย่อยนึง ที่จะมีกล่องเนื้อหาพิเศษที่เว็บย่อยอื่นๆ ไม่มี แล้วไอ้กล่องที่ว่านี้ โค้ดมันก็ดันมีการเรียกใช้ Library ใหญ่ๆ ซะด้วยสิ…

จะเห็นว่าในกรณีนี้ ถ้าเราแยก Chunk ด้วย Route แล้วละก็ Chunk ของหน้าอ่านคอนเทนต์จะต้องใหญ่ผิดปกติแน่นอน และที่สำคัญ User ทุกคนจะต้องมาโหลดโค้ดของกล่องที่ว่านี้ ไม่ว่ามันจะแสดงหรือไม่ก็ตาม…

เราเลยตัดสินใจแยก Chunk ของกล่องเจ้าปัญหานั้น ออกมาจาก Chunk ของหน้าอ่านคอนเทนต์ ด้วยฟีเจอร์ Dynamic Import ของ Next.js

การนำ Dynamic Import มาใช้เพื่อแยก Chunk พิเศษออกมาแบบนี้ นอกจากเราจะเอามาใช้กับบางเว็บย่อยที่มีฟีเจอร์พิเศษแล้ว เรายังเอามาใช้ภายในเว็บย่อยเดียวกัน ที่ Component ของ Desktop กับ Mobile มีความแตกต่างกันมากจนไม่สามารถ Reuse ได้อีกด้วย ทีนี้ Mobile User ก็จะไม่ต้องมาโหลดโค้ดสำหรับ Desktop อีกต่อไปแล้ว…

การทำ SSR นั้น ค่อนข้างช้า…

ก่อนที่จะเอางานขึ้นจริง เราก็ได้มีการทำ Performance Testing โดยการจำลองสถานการณ์ในกรณีที่มี User เข้ามาอ่านคอนเทนต์หลายๆ อัน พร้อมๆ กัน โดยที่ยังไม่เปิดใช้ระบบ Cache

เราพบว่า Response Time ของ Web Server เวอร์ชันที่ใช้ Stack ใหม่นั้น ช้ากว่า Stack เดิมที่เป็น PHP อยู่พอสมควรเลย ส่วนสาเหตุหลักก็ไม่ใช่ใครที่ไหน… มันคือการทำ Server-Side Rendering ด้วย React นั่นเอง…

เราพยายามหาทางทำให้ SSR เร็วขึ้น เริ่มด้วยการลองอัพเดท Node.js ให้เป็นเวอร์ชัน 8 ก่อน ซึ่งผลที่ได้นั้น ถือว่าน่าพอใจมากๆ เพราะ Response Time นั้น ลดลงกว่า 30% เลยทีเดียว จากนั้นเราก็ลองอัพเดท React ให้เป็นเวอร์ชัน 16 ซึ่งเวอร์ชันนี้เค้าเคลมไว้ว่าจะช่วยทำให้ SSR เร็วขึ้น ซึ่งพออัพแล้วมันก็เร็วขึ้นจริงๆ แต่เพียงแค่ 10% เท่านั้น…

สุดท้ายแล้ว เราพบว่าไม่ว่าจะพยายาม Optimize ขนาดไหน มันก็ยังช้ากว่า Stack เดิมอยู่ดี แต่ถึงยังไง เราก็โอเคกับมันนะ ถึงแม้ว่าการเปลี่ยนมา Render หน้าเว็บแบบ Component จะช้ากว่าการ Render หน้าเว็บแบบ String อยู่พอสมควร แต่ในแง่ของการพัฒนานั้น มันสะดวกกว่ามากๆ เลย…

ส่วนเรื่องช้า เรามองว่าเราสามารถเอาระบบ Cache เข้ามาช่วยได้ อาจจะทำแค่ระดับ Page หรือถ้าอยากให้เร็วขึ้นไปอีก เราก็ยังสามารถทำที่ระดับ Component ด้วยก็ได้…

Persisted Queries มีดีกว่าที่คิด

เนื่องจากเราไม่อยากให้คนมาป่วน GraphQL API ของเรา เราจึงเอาแนวคิด Persisted Queries มาใช้…

เรารู้อยู่แล้วว่า Persisted Queries ช่วยให้ API ของเราปลอดภัยจากการถูกโจมตีมากขึ้น เพราะ GraphQL Server จะยอมรับเฉพาะ Query ที่เรา Allow ไว้เท่านั้น นอกจากนั้น การทำ Persisted Queries ยังจะช่วยลด Bandwidth ที่ API ใช้ไปได้อย่างมหาศาล เนื่องจาก Query ยาวๆ จะถูกยุบให้เหลือเพียงแค่ ID สั้นๆ เท่านั้น…

Client จะส่งมาเฉพาะ Query ID ที่มีแต่ Server เท่านั้น ที่จะรู้ว่ามันคือ Query อะไร

แต่ผลพลอยได้ของการทำ Persisted Queries ก็คือ เราสามารถเปลี่ยนจาก Method POST มาใช้ Method GET แทนได้ เนื่องจาก Query มันไม่ได้ยาวเหมือนแต่ก่อนแล้ว และพอเป็น GET การทำ Cache ให้กับ Query ก็จะสะดวกและมีประสิทธิภาพมากขึ้น เนื่องจากเราจะสามารถทำ Cache ที่ระดับก่อนที่จะถึงตัว GraphQL Server ได้แล้ว…

ยังมีอีกหลายสิ่งให้เราเรียนรู้…

จริงๆ แล้ว การปรับหลายๆ อย่าง ที่ว่ามานี้ แทบจะเกิดขึ้นหลังจากที่เราเอางานขึ้น Production ไปแล้วทั้งหมด ซึ่งถ้าเอากราฟของ Performance เมื่อตอน Launch ใหม่ๆ กับตอนนี้มาเทียบกัน ก็คงต้องบอกว่าคนละเรื่อง! ถึงบอกไง… ว่าก่อนหน้านี้เราไม่ได้รู้อะไรเลย…

แต่อย่าลืมว่าทั้งหมดทั้งมวลนี้ มันคือการแก้ปัญหาที่เราเจอ… ด้วยคนที่เรามี… วิธีแก้ที่เราเลือกใช้นั้น มันก็ไม่ได้ดีไปซะหมดหรอก… มันก็แค่เหมาะสมกับธุรกิจของเรา, Server ของเรา, ทีมของเรา ก็เท่านั้นเอง ที่เล่ามาทั้งหมดนี้ เราอยากให้คนโฟกัสไปที่ Why ไม่ใช่ How

ถึงแม้ว่าตอนนี้ อะไรๆ จะดูดีขึ้นเยอะแล้ว แต่เราก็ยังคิดว่า เรายังสามารถทำให้ Sanook.com ดีขึ้นกว่านี้ได้ ยังมีอีกหลายอย่างเลย ที่เราอยากจะปรับปรุง เราอยากให้เว็บนี้มันเร็ว เราอยากให้เว็บนี้มันใช้ง่าย…

เพราะในฐานะ Developer การเห็นคนใช้ Product ที่เราทำ มันมีความสุขที่สุดแล้ว…

--

--