BLOG

ทำยังไง...? เว็บเราโหลดเร็วขึ้น 7 เท่า

จากโหลด 14 วินาที เหลือไม่ถึง 2 วินาที เราทำอะไรไปบ้าง?

สวัสดีครับ หายหน้ากันไปหลายเดือนเลย กว่าจะได้เขียน แต่ว่าจริงๆ ก็มีร่างเอาไว้ประมาณสองถึงสามบทความนะ แต่ว่าเขียนไม่จบซะที :) เมื่อวันที่ 12 กพ มีโอกาสที่ตอนบ่ายมีเวลา เลยร่างรอโพสนี้รอไว้อีก แล้วก็มานั่งเติมภาพกับลิงค์ได้วันนี้

ที่ต้องเขียนออกมาให้อ่านกันเนี่ย เพราะเป็นประสบการณ์ที่ดีมาก ในการที่ผมปรับแต่ง (Optimize) ทำให้เว็บเร็วขึ้นและมีตัวชี้วัดของเว็บที่ดีขึ้้น เลยอยากที่จะเอามาแชร์ให้ฟังกัน เชื่อว่าหลายๆ ท่านที่ติดตามเรา หลายท่านเป็นเจ้าของธุรกิจ หรือมีส่วนในการตัดสินใจ หรือไม่ก็มีส่วนเกี่ยวข้องกับเรื่องนี้ ได้อ่านแล้วคงจะได้มีโอกาสเอาไปประยุกต์ใช้แน่ๆ ก็ในโพสนี้จะพยายามหลีกเลี่ยงศัพท์เทคนิคให้ได้มากที่สุด หรือใส่ลิงค์ไว้ให้อ่านต่อนะว่ามันคืออะไร

อะไรคือจุดเริ้่มต้นที่คิดจะ Optimize เว็บ?

สาเหตุที่ผมต้องกลับมาสนใจเรื่องการ Optimize เว็บ หลังจากเคยมีการ Optimize ไปแล้วเมื่อสองปีก่อน ตอนที่เว็บเวอร์ชั่นนี้เปิดใช้งาน ก็เพราะว่าช่วงนี้เรากำลังมีแผนจะเปิดหน้าร้านที่ประเทศเนปาล (ซะที) หลังจากที่เน้นขายออนไลน์มาก่อนหน้านี้ ซึ่งเราได้ยินเสียงบ่นจากลูกค้าส่วนนึงก็คือ เว็บเราช้ามาก เพราะนอกจากข้อมูลจะวิ่งมาไกลแล้ว เว็บเรายังไม่ได้รับการปรับปรุงประสิทธิภาพ (Optimize) อย่างเพียงพอ ทำให้การโหลดหน้าเว็บครั้งแรกช้ามากๆ ประมาณ 20-30 วินาทีเลย กว่าจะแสดงผลขึ้นมา

จริงๆ หน้า CloudFlare ก็เตือนอยู่นานแล้วว่าเว็บแกโหลด 14 วินาทีเลยนะ กว่าจะมีอะไรขึ้นมา

พอรู้แล้วว่ามีปัญหา และหลังจากค้นคว้าจนพอได้แนวทางแล้ว ผมก็ปรับปรุงตั้งแต่คะแนน 0 จนมาได้ 70 อย่างในตอนนี้ ซึ่งจะเห็นว่าภายใน 1.3 วินาที ผู้ใช้สามารถมองเห็นหน้าเว็บและข้อมูลตอนต้นได้แล้ว (First Contentful Paint)

ประสิทธิภาพของเว็บ มีผลกับธุรกิจได้อย่างไร?

ก็ปฏิเสธไม่ได้อยู่แล้วว่าธุรกิจหลายอย่างปัจจุบันนี้ ช่องทางออนไลน์ถือเป็นแชนเนลหนึ่งในการขายสินค้าหรือบริการ และลูกค้าก็คาดหวังว่า จะสามารถติดต่อกับธุรกิจได้ ผ่านทางเว็บไซต์ และลูกค้าหลายท่านยังชอบที่จะติดต่อผ่านเว็บไซต์ หรือ LINE มาคุย มากกว่าที่จะโทรคุยเสียอีก ถ้าเว็บช้า ก็เหมือนว่าเราไปเปิดร้านอยู่ในซอกหลืบลึกๆ เดินทางไปยากๆ (เอ๊ะ นี่มันร้าน LEVEL51 เลยนี่นะ :P)

ก็มีผลสำรวจจาก https://www.thinkwithgoogle.com/ และ https://wpostats.com/ อยู่ตามนี้

  • 53% ของคนที่เปิดเว็บ จะปิดเว็บทิ้ง ถ้าเกิดว่า ใช้เวลาโหลดเกิน 3 วินาที และถ้าใช้เวลาเกินกว่า 1 วินาทีก่อนที่จะแตะปุ่มหรือเลื่อนจอได้ คนเข้าเว็บจะคิดว่าเว็บเสียด้วย และก็จะงง ไม่ได้ทำอะไรต่อ
  • เมื่อเว็บใช้เวลาโหลดนานขึ้น จาก 1 วินาที เป็น 7 วินาที อัตรา Bounce Rate (เข้าหน้าเดียวแล้วปิดเว็บ) แย่ลงถึง 113%
  • และเมื่อหน้าเว็บมีความซับซ้อนมาก จาก 400 ชิ้น เป็น 6,000 ชิ้น โอกาสที่จะเกิด Conversion ลดลง 95%
  • Walmart (คล้ายๆ Big C บ้านเรา) พบว่า ทุกๆ 0.1 วินาที ที่เว็บโหลดเร็วขึ้น ยอดขายเพิ่มขึ้น 1% นี่แสดงว่า เว็บเราเร็วขึ้น 13 วินาที ปีนี้ก็ต้องหวังยอดขาย เพิ่ม 130% แล้วสิ อิอิ
  • BBC พบว่า หน้าเว็บที่ช้าลง 1 วินาที ทำให้คนเข้าเว็บ หายไป 10%

ก็เห็นได้ชัดเจนเลยว่า ความเร็วของเว็บไซต์ มีผลกับตัวชี้วัดต่างๆ ในเชิงคุณภาพของเว็บไซต์โดยตรง

ซึ่งตัวชี้วัดที่โดยมากเราจะต้องสนใจ ก็คือ

  • Unique Visitor - จำนวนคนเข้าชม - ก็ถ้าเว็บเราใช้เวลามากกว่า 3 วินาทีในการแสดงผล คนก็จะออกจากเว็บไปเลย
  • Page View - จำนวนการเปิดหน้าเว็บ - ถ้าคนที่เข้ามาดู ทนรอจนจบหน้าได้ ก็มักจะไม่ดูหน้าอื่นต่ออีกถ้าเว็บช้า
  • Bounce Rate - อัตราการเข้าชมที่คนเข้าเว็บแล้วปิดเว็บทิ้งโดยไม่ไปหน้าอื่นต่อ ก็คือ คนเข้ามาดู แล้วปิดเลย
  • Conversion Rate - อัตราการที่คนเข้าชมเว็บไซต์ ทำกิจกรรมที่เราคาดหวัง เช่น กด Subscribe, เปิดดูโปรโมชั่น หรือ เพิ่มสินค้าลงในตะกร้า เป็นต้น ซึ่งถ้าคนที่เข้ามาดูหน้าเดียวแล้วปิด หรือไม่ทันจะได้ดูก็ปิดแล้ว โอกาสที่จะเกิด Conversion ก็เป็นไปได้ยาก จริงไหมละ?

รู้ว่ามีผลขนาดนั้น...ทำไมถึงมา Optimize เอาป่านนี้ (เว็บเวอร์ชั่นนี้ 2 ปีกว่าแล้ว)

แน่นอนว่า คนที่ทำเว็บ ก็เทสเว็บตัวเองก่อน บนเครื่องที่ใช้ทำเว็บแล้วทั้งนั้นแหละ และถ้ามันช้ารับไม่ได้จริงๆ ก็คงจะมีการแก้ไขไปก่อนแล้ว พอมีเสียงบ่นจากลูกค้ามาว่า เว็บช้า เรามาลองกดเอง เอ๊ะ มันก็ไม่ได้ช้านี่นา เป็นปัญหาระดับโลกที่เรียกว่า It Works on My Machine

และปัญหา It Works on my Machine นี่เองก็คือสาเหตุที่คนทำเว็บอย่างผม ไม่ได้สังเกตเห็นถึงความช้า - เพราะว่าเวลาเทสเว็บ เราเทสในเครื่อง ไม่ก็มือถือเครื่องเดิม บางทีก็จะลืม Clear Cache (ลบ History) ออก ทำให้เว็บมันก็ไม่ได้ดูช้าอะไร และสำคัญคือ ผมเป็นคนทำ ก็จะมี Bias ว่า "เราทำดีแล้ว ไม่เห็นจะช้าเลย" การที่ขาดตัวชี้วัด ที่เป็น Quantitative หรือ ถูกให้คะแนน ทำให้วัดผลออกมาไม่ได้ ว่ามันช้า หรือว่า เร็วแค่ไหน

ซึ่งหลังจากที่ได้ Optimize เว็บและถูกให้คะแนนเรียบร้อย ก็ทำให้เก็ทตาสว่างขึ้นมาได้อีกจุดหนึ่งว่า เราควรจะต้องทำเว็บเพื่อเอาใจ "ผู้ใช้หน้าใหม่" (New User) เป็นหลัก

จากสถิติแล้ว เว็บของเรา มีผู้ชมกลับเข้ามากดสเปคเล่นเยอะมากถึง 40% เราเลยควรจะเอาใจคนกลุ่มนี้ก็จริง แต่คนกลุ่มนี้คือคนที่ได้ผ่านการอดทดรอโหลดครั้งแรกมาแล้ว น่าจะมีความสนใจมากๆ ซึ่ง Browser ก็จะทำการ Cache หรือเก็บไฟล์ที่เกี่ยวข้องกับเว็บเอาไว้แล้ว ทำให้ไม่ส่งผลต่อประสบการณ์ของคนกลุ่มนี้เท่าไหร่ แต่สำหรับผู้ใช้ที่หลงเข้ามาดูด้วยความสงสัย ถ้าเว็บโหลดช้าเกิน 3 วินาทีจนเขาปิดเว็บเราไปซะก่อนที่จะโหลดจบ ก็เท่ากับเราเสียโอกาสที่จะได้แนะนำตัวให้คนกลุ่มนี้รู้จักไปเลยนะ

และทำไม เว็บถึงได้ช้า?

เราก็ต้องมาทำความเข้าใจกับ Demographic หรือ หน้าตาลักษณะของคนใช้เว็บกันก่อน คงจะไม่ต้องเถียงกันเลยว่า ตอนนี้ทุกคนแทบจะใช้เว็บกันผ่านมือถือทั้งนั้น ไม่ได้ใช้ผ่านคอมพิวเตอร์ ดังนั้น เราก็ต้องทราบข้อมูลเกี่ยวกับคนใช้มือถือกันก่อน

  • คนบนโลกส่วนมาก ใช้มือถือ ราคาประมาณ 200 USD หรือประมาณ 6,200 บาท ที่มา (https://www.idc.com/) ซึ่งก็จะเป็นรุ่นกลางๆ ไม่ได้ช้ามากจนเกินไป แต่ก็ไม่ได้เร็วเวอร์แบบ Note 10 หรือ iPhone X แบบนี้ ลองมองย้อนดูตัวเอง ผมเองก็ไม่เคยซื้อมือถือราคาเกิน 10,000 เลย เหมือนกัน...เพราะหมดตังค์ไปกับการแต่งคอมพ์หมดแล้ว~
  • 75% ของคนบนโลก ยังไม่ได้ใช้ 4G จากข้อมูลเมื่อปี 2016 (ที่มา https://www.gsma.com/ )
  • ข้อมูลขนาด 1MB จะต้องใช้เวลาโหลดอย่างน้อย 5 วินาที ผ่านเครือขาย 3G (ที่มา https://www.webpagetest.org/)
  • เว็บทั่วไป ใช้เวลาประมาณ 19 วินาที ในการโหลดผ่าน 3G และ 14 วินาที ผ่าน 4G (ที่มา: https://doubleclick-advertisers.googleblog.com/)
  • เว็บทั่วไป ใช้เวลามากกว่า 7 วินาที ในการแสดงผลเนื้อหาขุึ้นมา

จากข้อมูล เราก็พอจะสรุปได้ว่า เหตุผลที่เว็บนั้น "ช้า" ก็เพราะว่า มือถือที่จะใช้เปิดเว็บของคน 75% บนโลกนี้ จะเป็นมือถือกลุ่ม Middle-Low End ซึ่งมีประสิทธิภาพของ CPU ระดับกลางๆ ไม่ได้ใช้ 4G และความเร็วของเครือข่ายที่ใช้กันโดยทั่วไป ยังอยู่ในระดับเพียงแค่ประมาณ 200KB/s หรือประมาณ 1.6Mbps เท่านั้น ยังไม่นับรวมถึงผู้ใช้งาน ที่ใช้แบบ Fair Use ที่ได้ความเร็วแค่ 16-32KB/s หรือต้องใช้เวลา 32-64 วินาที ในการโหลดข้อมูลขนาด 1 MB!

(สถิติทั้งหมด ดูมาจากเว็บ: https://www.flyingvgroup.com/blog/2018/04/12/how-faster-mobile-site-speed-builds-faster-business-success/ ที่สรุปเอาไว้ให้ จริงแล้วหลายๆ เว็บก็ให้ข้อมูลชุดนี้เหมือนกัน เพราะว่าเป็นข้อมูลที่ Google ยกมาอ้างอิงนั่นเอง)

ที่ว่าช้านี่ ช้าแค่ไหน? - สิ่งสำคัญคือต้องมี "คะแนน"

การจะบอกว่าเว็บช้าหรือเร็ว จะใช้แค่ความรู้สาึกไม่ได้ เพราะว่ามันวัดผลเพื่อการพัฒนาไม่ได้ และจะดูแค่ Latency หรือเวลาที่ข้อมูลวิ่งจาก Server มาถึงเครื่องผู้ชมเว็บก็ไม่ได้ ผมจึงเลือกหาเครื่องมือ ที่มีการ "ให้คะแนน" เป็นหลัก

เครื่องมือที่ใช้ก่อนหน้านี้เลย คือเว็บไซต์ชื่อว่า pingdom โดยในช่วงที่ปล่อยเว็บเวอร์ชั่นนี้ออกมา ก็ได้มาทำการตรวจประสิทธิภาพกับ pingdom และพัฒนาจากคะแนน 10-20 จนมาเป็น 60 กว่าๆ แล้ว

สำหรับท่านที่กำลังจะ Optimize เว็บ ก็แนะนำให้ดูคะแนนจาก pingdom ประกอบด้วยก็ดีเหมือนกัน เพราะว่า pingdom จะให้คำแนะนำในเชิงของ การแก้ไขร่วมกับฝั่ง Server ซึ่งจะเป็นไปในแนวทางของการแก้ไขในด้าน Network คือการลดปริมาณ Request หรือ ขนาดของ Request ไปที่ Server และประิมาณข้อมูลที่ตอบกลับมา (ส่วนที่เห็น Request 500+ นี่ มาจาก Widget Facebook ล้วนๆ เลยจ๊ะ)

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

พอค้นคว้ามากขึ้น เลยทำให้ได้รู้จักกับ Lighthouse ซึ่งมันก็อยู่ใน Developer Tools ของ Google Chrome อยู่แล้วนนั่นแหละ รู้สึกโง่ดักดานมากที่ไม่เคยลองกดมาดูเลย :P วิธีเข้าไปใช้งาน Lighthouse ก็เพียงแค่ กด F12 แล้วกดไปที่ Tab Audits จากนั้นกด Generate Report เท่านั้นเอง

หมายเหตุ: เนื่องจากผมชอบใช้ Microsoft หน้าจอที่แสดง เป็นของ Microsoft Edge Canary ซึ่งใช้เอนจิ้นของ Google Chrome จึงมี Lighthouse อยู่ด้วย แต่หน้าตาและผลที่ได้ อาจจไม่เหมือนกันนะ เพราะอาจจะคนละ Build กัน (ของ Edge Caranry ใหม่กว่า)

เนื่องจากไม่ได้เก็บภาพของเว็บ LEVEL51 ไว้ก่อนแก้ เลยขออนุญาตเอาจากเว็บ https://www.matichon.co.th/ ซึ่งได้รับคำแนะนำคล้ายกับเว็บ LEVEL51 มาให้ชมกันเป็นตัวอย่างนะ ลองเข้าเว็บ จากนั้นกด F12 แล้วกด Generate Report ตามไปด้วยกันก็ได้ ขอเบลอภาพออก เพราะว่ามันจะมีเนื้อหาข่าวอยู่

โดยตัว Lighthouse ก็จะแนะนำเรามาเป็นข้อๆ ว่า มีอะไรบ้างที่สามารถทำได้ พร้อมกับลิงค์ Learn More ให้อ่านด้วยว่า หลักการและเหตุผลของการทดสอบนี้ มาจากอะไร (ส่วนถ้าอยากอ่านทั้งหมด ไปดูที่นี่) โดยสิ่งที่ Lighthouse แนะนำกับเว็บ LEVEL51 และเว็บมติชนคล้ายกันเลย (...คือโดนเกือบทุกข้อใน Opportunities) ก็คือ

  • Defer offscreen images (ลองอ่าน) - ถ้าหน้าเว็บเรามีไฟล์ภาพประกอบจำนวนมาก Browser จะทำการโหลดข้อมูลภาพเหล่านั้นโดยอัตโนมัติรอไว้ ทันทีที่มันได้รับคำสั่ง ทำให้ภาพที่ต้องไถไปอีกนานกว่าจะเจอ ก็ได้รับการโหลดมาพร้อมกันด้วย สำหรับเรื่องนี้ มีผลไปจนถึงค่าใช้จ่ายด้าน Bandwidth ของเว็บเองด้วยนะ เพราะว่าภาพทั้งหมดของหน้านั้น จะต้องถูกส่งไปทั้งที่อาจจะไม่ได้ถูกเรียกขึ้นมาใช้งาน และก็มีผลต่อผู้ที่เข้าเว็บเราด้วย เพราะว่า ถ้าเข้าผ่านมือถือ ก็ต้องเสียเงินในการโหลดข้อมูลมาเหมือนกัน
  • Eliminate Render-Blocking Resources (ลองอ่าน), Minimize main-thread Work (ลองอ่าน), Reduce Impact of 3rd Party Code, Reduce JavaScript execution time - ก็คือ Script บางตัว กิน CPU มาก จะต้องหาเทคนิคในการหลัีกเลี่ยงมัน หรือ ให้มันเริ่มทำงานช้าที่สุด เอาหน้าเว็บเราขึ้นมาก่อน ซึ่งสำหรับของ LEVEL51 นั่นก็คือ Widget ของ Facebook และ Google Analytics ที่เอาไว้เก็บสถิติเว็บเองนั่นแหละ!!!
  • Efficiently encode images, Properly size images เป็นการเตือนว่า มีภาพบางภาพที่มันใหญ่เกินไป ผมเองก็เพิ่งพบว่า มีภาพที่ใช้เป็นหน้าปกของบล็อก ที่เผลอใส่ PNG 32-bit ขนาดใหญ่สุด ไฟล์ขนาดเกือบ 3MB ไปเหมือนกัน :'( ขอโทษคร๊าบบบ
  • Serve Image in Next-Gen Format - เป็นการโฆษณากลายๆ ให้ใช้ภาพแบบใหม่ เรียกว่า Webp ซึ่ง Google Chrome รองรับ และมีขนาดเล็กกว่า JPG และสำคัญคือเล็กกว่าภาพแบบ PNG อย่างมาก ซึ่งเราจะต้องใช้ PNG ที่มีขนาดใหญ่กับบางภาพ ที่ต้องการให้ไม่มีพื้นหลังสีขาว อย่างเช่นภาพตัวเครื่องโน๊ตบุ้ค วางอยู่บนพื้นหลังที่เลื่อนได้

​จะเห็นว่าความแตกต่างระหว่างการใช้ Lighthouse กับการวัดด้วย pingdom ก็คือ Lighthouse เน้นไปที่ประสบการณ์ของการใช้งาน (User Experience) เป็นหลัก จะเห็นได้ว่าตัวชี้วัดข้อแรกเลยคือ First Contentful Paint (FCP) หรือ เวลาที่ผู้ใช้งาน สามารถมองเห็นหน้าเว็บที่กดโหลด เรียกว่า Above the fold (ATF)

ภาพจาก: https://web.dev/extract-critical-css/

พอทราบถึงปัญหาแล้ว ลองมาดูแนวทางการแก้ไขปัญหาของผมกันบ้าง

ทำการโหลดภาพ เท่าที่จำเป็นเท่านั้น

จุดสำคัญจุดแรกที่ Lighthouse แนะนำให้เราแก้ไข ก็คือทำการโหลดรูปต่างๆ ทีหลัง (Lazy Image Load)

เพราะว่ารูปพวกนี้ มันจะไปแย่ง Bandwidth การโหลดไฟล์ที่สำคัญกับการแสดงผลหน้าเว็บของเรา และก็ทำให้เราประหยัด Bandwidth ไปด้วย ถ้าเกิดว่าผู้ใช้งานไม่ได้เลื่อนลงไปถึงภาพที่จะใช้ ภาพพวกนี้แหละ เป็นตัวแปรสำคัญที่สุดสำหรับเวลาการโหลดหน้าเว็บ เนื่องจากมันมีขนาดใหญ่กว่า Script/Stylesheet (CSS) หลายเท่า เราจึงเลือกที่จะเริ่มจากการทำ Lazy Image Load ก่อนเลย ส่วนถ้าสงสัยว่า Lazy Image Load เป็นอย่างไร ทำแล้วจะออกมาเป็นหน้้าตาแบบนี้

สำหรับการปรับแต่งในจุดนี้ Lighthouse ได้แนะนำถึง Script ที่จะทำหน้าที่ Load ไฟล์ภาพ เท่าที่จำเป็นมาให้ด้วย สามารถตามไปอ่านได้ทางนี้

แต่สำหรับ LEVEL51 เรามีความต้องการที่ซับซ้อนกว่าอีกนิดนึง เลยตัดสินใจ เขียนเป็น Angular Directive ขึ้นมาเอง แทนที่จะเลือกใช้ของสำเร็จรูป เนื่องจากเว็บเราเป็น Angular ทั้งหมดอยู่แล้ว ก็ขอแปะโค๊ดเอาไว้ให้ดูตรงนี้เลยแล้วกัน เผื่อใช้อ้างอิงได้ จริงๆ ทางที่เร็วกว่าคือใช้ Intersection Observer API ซึ่งประมาณ 90% ของผู้ใช้อินเตอร์เน็ตสามารถใช้ได้แล้ว แต่ว่าผมยังเลือกใช้ scroll ไปก่อน เนื่องจากยังทำไม่เป็น :P

นอกจานี้ สำหรับการปรับแต่งนี้มีการแก้ไขฝั่ง Server ให้ทำการแก้ไขข้อมูล HTML ที่อ่านออกมาจากฐานข้อมูล ให้มาใช้ Directive นี้อัตโนมัติด้วย โดยการใช้ Library ชื่อว่า HtmlAgilityPack (ปกติเขามักเอาไว้ทำตัวดูดข้อมูลเว็บกัน) แล้วเก็บใส่ RAM บน Server ไว้ เพื่อที่จะได้ไม่ต้องไปไล่แก้ข้อมูลเว็บอีกรอบ และก็เผื่อว่าคราวหน้าเราจะมีการเปลี่ยนแปลงอะไรอีกในอนาคต จะได้มีต้นฉบัับเก็บไว้ และก็ไม่ต้องนั่ง Migrate ข้อมูลอีกให้เสียเวลา เนื่องจากหน้าเว็บ LEVEL51 นั้นมีไม่ถึง 100 หน้า ข้อความที่เป็นตัวอักษรทั้งหมดรวมกันคงมีขนาดรวมกันคงจะไม่ถึง 10MB อยู่แล้ว

  module.directive('ncbImgdefer', function ($http, $datacontext) {
   
  var imgList = [];
   
  var existingTimeout = null;
  function fixIsotope() {
   
  if (existingTimeout != null) {
  window.clearTimeout(existingTimeout);
  existingTimeout = null;
  }
   
  existingTimeout = window.setTimeout(function () {
   
  $("[isotope]").isotope('layout');
  window.dispatchEvent(new Event('resize')); //required by some plugins
   
  }, 400);
   
  };
   
  function loadImageIfAppeared($element, referencePoint) {
   
  var src = $element.attr("ncb-imgdefer");
  var isReize = src.indexOf("/") == 0;
  var offSet = $element.offset();
   
  if (referencePoint > offSet.top) {
   
  if (isReize) {
   
  $element.imgElement.src = "/__resizeh/" + $element.attr("key");
  }
  else {
   
  $element.imgElement.src = src;
  }
   
  return false;
  }
   
  return true;
  }
   
  if (window.scrollWatch == null) {
   
  window.scrollWatch = function () {
   
  if (imgList.length == 0) {
  return;
  }
   
  var remaining = [];
  var bottom = document.documentElement.scrollTop + window.innerHeight;
   
  imgList.forEach(function ($element) {
   
  if (loadImageIfAppeared($element, bottom)) {
   
  remaining.push($element);
  }
  });
   
  imgList = remaining;
   
  if (imgList.length == 0) {
  fixIsotope();
  }
  };
   
  window.onscroll = window.scrollWatch;
  window.setTimeout(window.scrollWatch, 400);
  };
   
  function link($scope, $element, attrs) {
   
  var imgElement = $element[0];
  if ($element.is("img") == false) {
   
  imgElement = $('<img/>')[0];
  imgElement.$backgroundTarget = $element;
  }
   
  $element.imgElement = imgElement;
   
  var fallback = $element.attr("ncb-imgdefer");
   
  imgElement.onload = function () {
   
  if (imgElement.$backgroundTarget != null) {
   
  imgElement.$backgroundTarget.css("background-image", "url('" + imgElement.src + "')");
  $(imgElement).remove();
   
  return;
  }
   
  fixIsotope();
  };
   
  imgElement.onerror = function () {
   
  fixIsotope();
   
  if (imgElement.src != fallback && $element.attr("fallback") != "1") {
   
  $element.attr("fallback", "1");
  imgElement.src = fallback;
   
  imgElement.onload = function () {
   
  fixIsotope();
  imgElement.updated = true;
   
  if (imgElement.$backgroundTarget != null) {
   
  imgElement.$backgroundTarget.css("background-image", "url('" + imgElement.src + "')");
  $(imgElement).remove();
   
  return;
  }
   
  };
  }
   
  };
   
  // element is above fold and should show image now
  if ($element.parents("[abovefold]").length > 0 || $element.is("[abovefold]")) {
   
  loadImageIfAppeared($element, 99999999);
  return;
  }
   
  imgList.push($element);
   
  };
   
   
  return {
  restrict: 'A',
  link: link,
  scope: false
  };
  });
view raw ncb-imgdefer.js hosted with ❤ by GitHub

ใช้รูปแบบ WebP พร้อมกับปรับขนาดรูป

สำหรับไฟล์ภาพชนิด WebP จะเป็นภาพแบบใหม่ ที่รองรับใน Browser รุ่นใหม่ๆ ยกเว้น Safari ตัวเดียว ซึ่งมีขนาดที่เล็กเมื่อเทียบกับไฟล์ JPG โดยที่คุณภาพใกล้เคียงกัน และยังรองรับ Alpha Channel คือทำพื้นหลังใสได้ สามารถใช้แทน PNG ได้ด้วย

มีผลทดสอบจาก Keycdn ที่พบว่าขนาดไฟล์ลดลงอย่างน่าตกใจมาก บางรูปเหลือเพียงแค่ 1 ใน 10 จากขนาดเดิมเลยทีเดียว จึงน่าสนใจมากที่เราจะนำเอา WebP มาใช้

แต่ครั้นการที่จะไปเปลี่ยนภาพทั้งเว็บ ให้เป็น WebP คงจะเป็นไปไม่ได้ เราก็เลยทำการสร้างโมดูลไว้บน Server ที่จะทำการแปลงรูปภาพ จากต้นฉบับที่เป็น JPG, PNG ให้เป็น WebP อัตโนมัติ ถ้าเกิดว่า Browser นั้นรองรับ โดยใช้ Library ImageProcessor และผมมองว่าเป็นโอกาสดีที่ผมจะแก้ไขระบบบย่อรูป ที่มีใช้อยู่แล้วไปด้วยในตัว ของเดิมเขียนเองโดยใช้วิธีแบบเก่าซึ่งช้าและไม่ปลอดภัย

จากนั้นผมประยุกต์ใช้โมดูลแปลงภาพ WebP นี้ ร่วมกับ Directive ที่เราสร้างไว้ตอนที่ทำ Lazy Image Load ด้วย โดยการก็เพิ่มโค๊ดให้ตัว Directive ทำการเรียกไปที่ Module ที่ทำการแปลงภาพ WebP ของเราแทนที่จะเป็นการเรียกไปที่ไฟล์ต้นฉบับโดยตรง พร้อมกับส่ง Parameter ไปที่ Server เพื่อบอกขนาดรูปที่เหมาะสมกับหน้าจอด้วย โดยโมดูลก็จะแปลงภาพเป็น WebP พร้อมกับย่อขนาดให้เหมาะสมกับหน้าจอโดยอัตโนมัติ และเราก็อาศัย CloudFlare ให้ช่วยทำการ Cache รูปที่แปลงแล้ว ให้เราอีกทีโดยที่ไม่ต้องทำเองและไม่ต้องเสียที่เก็บไฟล์ WebP แยกต่างหากด้วย :) ที่สำคัญคือ ผมยังสามารถเซฟรูปได้ตามปกติตอนที่ทำเว็บ ไม่ต้องนั่งแปลงเป็น WebP ให้เสียเวลา

กระจาย Bundle และทำการ Pre-load

ในสมัยช่วงก่อนที่ผมทดสอบเว็บด้วย Pingdom ซึ่งเน้นการทดสอบที่ประสิทธิภาพการดาวน์โหลดข้อมูล Pingdom จะแนะนำว่า ให้ลดจำนวน Request ที่จะต้องส่งหา Server เป็นหลัก

ก็เป็นไปได้ว่า Browser ในสมัยนู้น ยังไม่ได้ทำการโหลดข้อมูลพร้อมกัน เพราะลองนึกๆ ดู ในช่วงก่อนก็จะมีทั้งเทคนิคและเครื่องมือเกี่ยวกับการลด Request ออกมาเยอะมาก และแม้แต่ Google เอง ก็ใช้เทคนิคการลดจำนวน Request นี้ด้วยเหมือนกัน ถ้าลองดูที่เว็บ https://katz.co/tag/google-sprite/ก็คือ Google จะเอารูปทั้งหมดที่เว็บต้องใช้ รวมกันไว้เป็นไฟล์เดียว เรียกว่า ทำ Image Sprite เป็นความพยายามทำให้เว็บโหลดเร็ว ตั้งแต่เมื่อ 10 ปีก่อนแล้วเลยละ

และอีกอย่างที่เราจะนิยมใช้ก็คือ bundle ซึ่งก็คือ เอาไฟล์ที่ Browser ต้องโหลด มาจับรวมกันให้กลายเป็นไฟล์เดียว เว็บ LEVEL51 เอง ก็ทำแบบนี้

<script src="/__bundle.js"></script>

(จริงๆ ก็ต้องทำ minification ด้วย เพื่อลดขนาดเพิ่มไปอีก แต่ LEVEL51 ใช้ CloudFlare ซึ่งจะทำการ Minify CSS, JavaScript ให้เองอัตโนมัติอยู่แล้ว เราเลยไม่ต้องทำเอง)

แต่จุดตายของการทำ Bundle, Image Sprite ก็คือ ข้อมูลที่ส่งมานั้น จะส่งมาเป็นคราวเดียวก้อนใหญ่ๆ ทำให้ Browser ต้องรอข้อมูลทั้งหมดมาถึงก่อน จึงจะสามารถเริ่มทำงาน หรือแสดงผลได้ ทำงานในทีนี้ก็คือ เริ่มให้ JavaScript ต่างๆ ทำงาน เช่น ส่งสถิติด้วย Google Analytics, แปะโฆษณา, ระบบ Gallery หรือ Lightbox เป็นต้น และเริ่มเอา Stylesheet มาใช้งานกับหน้าเว็บ ถ้าเข้าเว็บ LEVEL51 ในช่วงก่อน จะเห็นว่า หน้าจอ Browser ค้างที่สีขาวๆ ประมาณ 5-10 วินาที ก่อนที่จะเริ่มมีอะไรแสดงผลออกมา

ก็หลังจากที่ผมทำการแยก Bundle แล้ว เทคนิคถัดมาก็คือทำการใช้ Tag link กับ Attribute preload คู่กัน โดยเทคนิคนี้ ได้มาจากคำแนะนำของ Lighthouse เองนั่นแหละ

<link rel="preload" href="/Site/css/style.css" as="style" onload="this.onload=null;this.rel='stylesheet'">

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

  • เปลี่ยน rel จาก stylesheet เป็น preload
  • เพิ่ม Attribute as="style"
  • เพิ่ม Script แบบ inline ไปว่า เมื่อโหลดเสร็จแล้ว ให้เปลี่ยน Attribute rel กลับเป็น stylesheet เหมือนเดิม

คนคิดมันไอเดียเจ๋งมากอะ เหอๆๆๆ แต่ไม่แน่ใจเหมือนกันว่าจะกระทบกับคนที่ใช้ Browser เก่าๆ ขนาดไหน เพราะว่า preload นั้น เริ่มรองรับตั้งแต่ปี 2016 เป็นต้นมา...แต่ถ้ายังใช้ Browser เก่าขนาดนั้น ก็อาจจะไม่สนใจใช้เครื่องเราก็ได้นะ >. <

[UPDATE] สำหรับเทคนิคนี้ ค่อนข้างมั่นใจว่าตอนนั้นได้ทดสอบกับ FireFox แล้วนะในตอนนั้น แต่ว่ามีลูกค้าแจ้งเข้ามาว่า ใน FireFox ตัวปัจจุบันนั้น ไม่สามารถใช้ได้ Style ไม่ติด และเราโหลด FireFox มาเมื่อวัันที่ 27 มีนาคม ก็พบว่า มันใช้ไม่ได้เหมือนกัน คือ rel ไม่ยอมเปลี่ยนจาก preload เป็น stylesheet เราเลยแก้ไขโดยการใส่คำสั่ง jQuery นี้ลงไปใน document.ready ซึ่งจะทำงาน หลังจากที่ทุกอย่างโหลดเสร็จแล้ว แบบนี้

$("link[as='style']").attr("rel", "stylesheet");

การทำ Preload ก็มีจุดตายอีกเหมือนกัน! - เช่นเดียวกับการ Optimize ทุกอย่าง ก็คือถ้าเรา Optimize มากเกินไป (Over-optimize) บางทีมันอาจจะส่งผลในแง่ลบก็ได้ อย่างเช่น เราไปมุ่งเน้นกรณีใดกรณีหนึ่งมากจนเกินไป จนไม่ได้สนใจความเป็นไปได้อื่น พอดีว่าผมมองเห็น ตรงหัวข้อที่เขียนว่า Avoid chaining critical requests ที่บอกว่า Browser ต้องโหลดอะไรบ้าง จึงจะแสดงหน้าเว็บเราได้

ทำให้ผมเกิดไอเดียว่า ถ้าอย่างนั้นเราก็ไป preload มันซะให้หมดเลยสิ...ผลปรากฏว่า มันทำให้คะแนนยิ่งแย่ลง ถ้าให้ผมคาดเดา ก็น่าจะเพราะว่า Browser ใช้ Bandwidth ไปกับการ Preload Resource พวกนี้ แทนที่จะมาโหลดหน้าเว็บหลัก กับไฟล์ stylesheet ที่เราจะใช้นั่นแหละ

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

โหลด CSS และ Script ทีหลังด้วย Timeout

เทคนิค Timeout นี้ เราเรียกกันเองในบริษัทฯ ว่าเป็นเทคนิค "Yo Timeout" คือตอนสมัยที่ทำ App มือถือ, Universal Windows App และ Website อยู่ เรามักจะมีปัญหา Race Condition ก็คือ เราพยายามเขียนให้โปรแกรมมันทำหลายๆ อย่างพร้อมกัน โหลดข้อมูลหลายอย่างพร้อมกัน (เรียกว่า แตก Thread) สุดท้ายมันก็เลยแย่งกันกลายเป็นช้ากว่าเดิม แสดงผลผิดบ้าง หรือเกิดบั๊กบ้าง

ก่อนที่จะค้นพบเทคนิคนี้ เราก็ไปพยายามนั่งแก้ Race Condition โดยการพยายามจัดลำดับนู่นนี่ แต่น้องโย่ มีวิธีการที่ชาญฉลาดมาก ก็คือ ใส่ Timeout หมายถึง ตั้งให้มันมีหน่วงเวลา (Delay) ก่อนที่จะเริ่มทำ และน้องโย่เขาก็ใช้วิธีนั่งสังเกต แล้วก็ใส่หน่วงเวลาเอาเลยว่าอันนี้จะหน่วงเท่าไหร่ ซึ่งในความรู้สึก มันดูเหมือนไม่น่าจะใช้ได้นะ เหมือนน้องแค่มั่วเลขหน่วงเวลาลงไป แต่ก็ช่วยแก้ไขปัญหาได้จริงๆ

ผมก็เลยเอา Yo Timeout มาใช้ในการปรับแต่งนี้ด้วย ทีนี้ ต้องเริ่มมีการคิดมากขึ้นอีกหน่อย ว่าอะไรที่สามารถดาวน์โหลดภายหลังได้ ซึ่งสำหรับกรณีของ CSS ของ LEVEL51 ก็คือ ไฟล์ CSS ของ Plugin ต่างๆ นั่นเอง

ถ้าเราไปดู Script ของ Google, Facebook ที่ให้เรามาแปะบนหน้าเว็บ ก็จะทำคล้ายๆ กันคือ ใช้ Script สร้าง Tag script ขึ้นมาอีกที เราก็เลยจะใช้เทคนิคลักษณะเดียวกัน แต่ว่าเพิ่ม window.setTimout เข้าไปด้วย ก่อนที่จะเอา Script ใส่กลับเข้ามาในหน้า Html ของเรา

ขอเอาโค๊ดที่ผมทำ ใส่ Gist มาให้ดู เผื่อเป็นไอเดีย หรือก็อปปี้ไปใช้เลยก็ได้

  var delayStack = 100;
   
  window.deferCSS = function (url) {
   
  var link = document.createElement('link');
  link.rel = 'stylesheet';
  link.href = url;
   
  link.type = 'text/css';
   
  delayStack += 10;
   
  window.setTimeout(function () {
   
  document.getElementsByTagName("head")[0].appendChild(link);
  }, delayStack);
  };
   
  window.deferCss = window.deferCSS; // prevent my forgetfulness
   
  window.deferScript = function (url, delay) {
   
  var script = document.createElement('script');
  script.src = url;
  script.type = "text/javascript";
  delayStack += 10;
   
  window.setTimeout(function () {
   
  document.getElementsByTagName("head")[0].appendChild(script);
  }, delay == null ? delayStack : delay);
   
  };
   
  // Sample
  window.deferCSS('/NancyBlack/Modules/ContentSystem/ncb-content.min.css');
view raw deferResource.js hosted with ❤ by GitHub

สังเกตว่า Script จะต่างจาก CSS ตรงที่ว่า เราสามารถกำหนด Delay เพิ่มขึ้นได้ด้วย เพราะว่าลำดับการโหลด JavaScript มีความสำคัญนั่นเอง ถ้าเราจะโหลด Script บางตัว ที่ต้องใช้ Script อีกตัวก่อน การที่ Delay น้อยเกินไป อาจจะทำให้ได้ผลที่ผิดเพี้ยนไป

แต่ว่าการทำแบบนี้ จะทำให้ Style ของหน้า แสดงผลออกมาช้า การแก้ไขอีกอย่างที่ Lighthouse แนะนำคือให้เราทำการแยก Style เท่าที่จำเป็นสำหรับการแสดงผลหน้าเว็บช่วงต้นของงเราออกมา และใส่เอาไว้ในโค๊ดของหน้าเว็บ โดยใช้ Tag style เลย เราก็ทำตามเรียบร้อย ซึ่งถ้าแอบดูโค๊ดของเว็บ LEVEL51 ก็จะเห็นว่ามี Style บางส่วนสำหรับเว็บมือถือ ที่ผมเลือกมาใส่ไว้

นอกจากนี้ สำหรับโลโก้ที่เขียนว่า LEVEL51 ผมก็ได้ทำการใช้ https://www.base64-image.de/ ทำการแปลงไฟล์ ให้ออกมาเป็น data url เพื่อฝังรูปไปกับโค๊ด Html เลยทีเดียว เพื่อให้โลโก้ขึ้นมาก่อนเพื่อนเลย

ชะลอให้ Angular JS และ 3rd Party Script เริ่มทำงานช้าที่สุด

เนื่องจากเว็บ LEVEL51 มีการใช้ Angular JS เป็นหลัก ซึ่ง Angular JS เองก็ค่อนข้างมีระยะเวลาการประมวลผลที่นาน ทำให้ผมตัดสินใจ ใช้ Timeout อีกครั้ง ในการรอเวลาที่จะให้ AngularJS เริ่มทำงานออกไป

สำหรับกรณีของ LEVEL51 ซึ่งยังใช้ Angular 1.3 อยู่ ก็คือคำสั่ง angular.bootstrap แต่ว่ามันเป็นการลงดีเทลโค๊ดมากไปถ้าจะอธิบายด้วยว่ามันทำงานยังไง อันนี้ปล่อยให้ไปอ่านต่อเองนะ โดยผมเลือกเวลาไว้ที่ 2 วินาที เพราะนั่นคือเวลาที่ First Contentful Paint จบแล้ว

จากนั้น เราก็ทำการรวม Script ที่เป็น 3rd Party ทั้งหลายไว้ที่ function เดียว และก็ทำการ Delay มันไปอีกเช่นกัน โดยเราเลือกเวลาไว้ที่ 3 วินาที เพื่อให้มันทำงาน หลังจากเว็บเราโหลดเสร็จแล้วจริงๆ ซึ่งผลพลอยได้อีกอย่าง ก็คือ สถิติการนับ Page View ที่เราได้จาก Google Analytics ก็จะเป็นจากคนที่เข้าชม และอดทนรอเราได้ถึง 3-4 วินาทีด้วย แสดงว่าอยากดูจริง

ส่วนโค๊ด ให้ดูเป็นไอเดีย ประมาณนี้ - สำหรับโปรแกรมเมอร์ทั้งหลาย ขอออกตัวบอกก่อนว่า ng-module เป็น directive ที่ไม่มีอยู่จริง ใช้เพื่อให้โค๊ดนี้ สามารถไปรวมเอารายชื่อ module ทั้งหมดออกมาได้เท่านั้น

แต่ว่าการทำแบบนี้ จะเป็นการเพิ่มเวลา Time to Interactive แทน ก็คือ หน้าเว็บขึ้นละ แต่ว่ากว่าจะกดใช้งานได้จริงๆ ต้องรอ เพราะว่า Script ยังโหลดไม่เสร็จ ดังนั้นเราต้องพยายามรักษาสมดุลตรงนี้ด้วย ซึ่งผมก็ได้ทดสอบลองเพิ่มเวลา Timeout ดู ก็มีผลทำให้คะแนนลดลงไปด้วยเหมือนกัน

ผลตอบรับจากการ Optimize

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

ผลอย่างแรกเลยก็คือ Bandwidth ที่ใช้ ลดลงทันที โดย Bandwidth ที่เราต้องเสียเงิน (ไม่ได้ผ่าน CloudFlare) ก็คือส่วนต่างระหว่าง Cached กับ Data Served ลดลงจากประมาณ 2-3GB/วัน เหลือ 1GB/วัน หายไปครึ่งนึงเลยทีเดียว

ก่อนการปรับแต่ง พบว่า Bandwidth เราใช้ไปถึง 10GB/วัน สำหรับผู้ใช้ 1946 คน หรือคนละประมาณ 5MB

พอเริ่มมีการใช้ Lazy Image Load เหลือ 4.1MB/คน ลดลง 20% 

พอปรับแต่งทั้งหมด คือใช้ Lazy Image Load กับ WebP ด้วย เหลือ 2.7MB/คน เหลือครึ่งเดียวจากตอนแรก

ทีนี้ก็ถึงคราวของการพิสูจน์ข้อมูลทางสถิติทั้งหลายที่ได้กล่าวมาตอนต้นบ้างว่ามันเป็นจริงแค่ไหน ก็ปรากฏว่ามีแนวโน้มตามนั้นจริง โดย ปริมาณ PageView มากขึ้น 15% อาจจะแปลได้ว่า คนเข้ามาดูหน้าแรก แล้วกดไปดูหน้า Product ต่อมีมากขึ้น และปริมาณ Bounce Rate ลดลง 45% เลยทีเดียวเชียว แถมผลที่ดีขึ้นนี้ ดูจากเฉพาะคนที่รอเว็บโหลดเกิน 3 วินาทีมาแล้วด้วย เพราะว่าเราทำการ Delay เอาไว้ในตอนที่ Optimize แล้วนั่นเอง แอบบอกอีกด้วยว่า อันนี้จ่ายค่าโฆษณา Facebook ลดลงด้วยนะ~

ทีนี้ก็เหลือแค่ว่า ยอดขายเราจะดีขึ้น 130% แบบ Walmart ที่บอกว่า เว็บเร็วขึ้น 0.1 วินาที ยอดขายเพิ่ม 1% บ้างหรือเปล่่าเท่านั้นเอง ลุ้นมากเลยนัะเนี่ย~!

ส่งท้าย

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

ส่วนถ้าอยากได้ที่ปรึกษาด้านนี้ ขอออกตัวไว้ก่อนเลยว่า ปกติทางบริษัทฯ ไม่ได้รับงานด้านซอฟท์แวร์แล้วนะ เพราะว่าทำไม่ทัน :'( ถ้าเกิดต้องการมือโปรด้านนี้ เราอาจจะแนะนำเพื่อนๆ ให้รู้จักได้ครับ (หรือถ้ามีจังหวะ อาจจะรับเอง อิอิ) ทักมาทางเพจ http://m.me/level51pc ได้เลย ยินดีให้คำปรึกษาครับ แต่คิดว่าข้อมูลจากในโพสนี้ ก็น่าจะเพียงพอที่จะติดอาวุธ สำหรับไปใช้คุยกับ Web Developer ในบริษัท หรือ Freelance ให้มาทำให้เราได้แล้วละ

ส่วนถ้าต้องการเครื่องแรงๆ มาทำเว็บ มานั่งวิเคราะห์ข้อมูล ก็อย่าลืมนึกถึง LEVEL51 ด้วยนะคร๊าบ ~

BLOG

LEVEL51 คือใคร?

เราเป็นบริษัทโน๊ตบุ้คของคนไทย ใช้เครื่องจากโรงงาน CLEVO แบบยี่ห้อดังในต่างประเทศ ที่คุณสามารถเลือกสเปคเองได้เกือบทั้งเครื่อง ถ้าโน๊ตบุ้คและคอมพิวเตอร์ของคุณ คืออุปกรณ์สำคัญในการทำงาน นี่คือเครื่องที่ออกแบบมาสำหรับคุณ

1317
ลูกค้าที่รักเรา
0
เครื่องเกินแสนบาท
49
K
มูลค่าเครื่องโดยเฉลี่ย
0
K
สถิติเครื่องแพงสุด

ลูกค้าหน่วยงานราชการและมหาวิทยาลัย

ลูกค้ากลุ่ม Video Production, 3D Design, Software House

Landscape Design

ลูกค้ากลุ่มบริษัทอุตสาหกรรมและก่อสร้าง

 

 

 

พิเศษเฉพาะคุณ - รับคูปองส่วนลด 2,000 บาท สำหรับการสั่งซื้อเครื่องกับเรา