Tutorial

ทำ Image Lazy Loading บน Ghost CMS ด้วย Nginx กัน

By Arnon Puitrakul - 11 มีนาคม 2021 - 1 min read min(s)

ทำ Image Lazy Loading บน Ghost CMS ด้วย Nginx กัน

จากเมื่อหลายเดือนก่อนหน้า เรามีความพยายามในการทำ Image Lazy Loading ซึ่งตอนนั้นทำด้วยการเข้าไปอ่าน HTML บน Database ของ Ghost แล้วแปลงมันตรงนั้นเลย ซึ่งมันก็พาปัญหาตามมามากมาย เช่น แปลงซ้ำบ้าง หรือพอเราเข้ามาแก้ใน Editor ของ Ghost แล้ว Save มันก็จะหายไป ก็ต้องทำใหม่ หรือจะเป็น ถ้าเรา Publish บทความลงไปใหม่ มันก็ต้องรอให้ถึงเวลาที่จะรัน Script ถึงจะได้ Lazy Loading ออกมา

จนเมื่อวันก่อนหน้าอยู่ ๆ เว็บก็เข้าไม่ได้แบบ งง ๆ ระหว่างนั้นก็เข้าไปนั่งอ่านเรื่อง Nginx ว่ามันมีปัญหาอะไรยังไงบ้าง จนเราไปเจอของเล่นใหม่คือ HTTP Sub Module อยู่ ๆ ไอเดียก็บรรเจิดความบ้าขึ้นมา จนกลายมาเป็นบทความนี้แหละ

Image Lazy Loading คืออะไร ?

ปกติแล้ว ถ้าเราใส่รูปภาพลงไปในหน้าเว็บของเรา Browser มันก็จะอ่าน HTML แล้วโหลดรูปภาพมาทั้งหมด ก็จะค่อย ๆ โหลดเข้ามา ปัญหาคือ บทความรีวิวที่มีรูปภาพเยอะมาก ๆ อาจจะถึง 100 รูปในบางหน้าเลย และแต่ละรูปเราก็ไม่สามารถ Compress แบบ Lossy ให้เล็กมากนัก มันก็จะทำให้ประสบการณ์ไม่ดีได้ ก็ต้อง Compress ให้ได้เล็กที่สุด แต่ก็ยังคงความชัดของภาพอยู่

ทำให้สุดท้าย บางหน้าที่รูปเยอะมาก ๆ อาจจะต้องโหลดถึง 8-10 MB ต่อหน้า ซึ่งถือว่าเยอะมาก ๆ ในขณะที่เว็บอื่น เรากำลังพูดถึงหน่วย KB เท่านั้น ทำให้เว็บเรากลายเป็นว่าใช้ Data เยอะมาก ๆ เกินไปจริง ๆ โดยเฉพาะผู้ที่เข้ามาผ่าน Cellular Network และมี Network Cap ก็จะแย่ไป

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

วิธีการแก้ปัญหานี้ สามารถจัดการได้โดยการทำสิ่งที่เรียกว่า Lazy-Loading ตามความหมายเลย ขี้เกียจโหลด จะโหลดก็ต่อเมื่อเราใช้งานเท่านั้น ในที่นี้จะหมายถึงว่า เมื่อผู้ใช้เลื่อนมาถึงตรงที่รูปจะต้องขึ้น มันค่อยโหลด ทำให้ถ้าคนที่เข้ามาแล้วอ่านถึงไหนรูปจะโหลดถึงแค่นั้น

ในการ Implement จริง ๆ เมื่อก่อนเลย เราสามารถใช้ได้ 2 ท่าหลัก ๆ คือการใช้ Intersection Observer API หรืออย่างเว็บเก่าเราใช้ React ก็สามารถใช้พวก Event พวก Scroll ในการเช็คแล้วจัดการตามนั้นก็ได้ แต่จะเห็นว่า มันแอบยากไปหน่อย เราต้องมานั่งเขียน Script เพิ่มเติมเยอะมาก ๆ ไม่น่าจะโอเคเท่าไหร่ จนกระทั่งเมื่อ 2 ปีก่อนก็มีการออก Browser-Level สำหรับ Image Lazy-Loading ที่ทำให้เรา Implement มันได้โคตรง่าย

<img loading="lazy" src="...." alt="...."/>

ง่าย ๆ เพียงแค่เราเติม loading="lazy" ลงไปใน Img Tag เท่านั้น มันก็จะ Lazy Load ตัวรูปภาพของเราบน Web Browser ที่รองรับแล้ว

ท่าเดิมที่ทำแล้วพัง....

ใน Ghost CMS ที่เราใช้เป็นระบบหลักในเว็บนี้ ยังไม่ได้ Support การทำ Lazy-Loading หรืออะไรทั้งสิ้น ทำให้เราต้องเป็น Hacker ออกมาหาวิธีในการ Hack เพื่อจะเอา Feature มันออกมาให้ได้ ตอนนั้นเราก็เล่นเข้าไปอ่าน Database ของ Ghost เลย เราเจอว่าใน Table Post มันมี Field ชื่อ html อยู่ เลยลองแก้ดู ปรากฏว่าได้เฉย เราเลยเขียน Python Script ง่าย ๆ ในการเข้าไปอ่าน HTML ที่อยู่ใน Database ทั้งหมดมาแล้วเอามา Parse เอา Img Tag ออกมาแล้ว Replace ซะ

จากนั้นเราก็เขียน crontab ตั้งเวลาไว้ว่า พอถึงเวลา ก็ให้มันรัน Script ที่เขียนทิ้งไว้ มันก็จะเข้าไปแก้ Tag ของเราตามที่เขียนไว้ ทำให้บทความที่พึ่งเขียนและลงทันทีก็จะไม่ได้เป็น Lazy Loading ต้องรอพ้นคืนนึงไปก่อน มันก็จะใช้งานได้ สุดท้ายมันก็เจอปัญหาเยอะ ตั้งแต่ ถ้าเราเข้ามาแก้บทความใน Ghost Admin ใหม่ พวก Tag ที่ Modified ไว้มันก็จะหายไป ก็ต้องมารันใหม่ถึงจะกลับมา ก็ไม่ค่อยโอเคเท่าไหร่ ไหนจะเรื่อง Consistency ของ Ghost ที่เล่นกับ HTML ได้เจ็บแสบมาก ๆ ทำให้เราหา Pattern ในการ Replace Tag ได้ค่อนข้างยาก สุดท้ายก็เลยพับเก็บไป ไม่ได้ใช้อีก จนมาถึงวันนี้แหละที่เจอท่าใหม่

Nginx Sub Module to the RESCUE

หลังจากเข้าไปอ่านใน Document ของ Nginx เพื่อมาแก้ปัญหาว่าทำไมอยู่ ๆ เว็บก็เข้าจากข้างนอกไม่ได้ แปลก ๆ จนไปเจอ HTTP Sub Module ขึ้นมานี่แหละ แบบว่าพีคจับใจมาก ๆ เพราะในตัวอย่างเขาเอามา Replace จาก IP Address ให้เป็น Host Name แล้วทำไมเราจะเติมอะไรบางอย่างลงไปไม่ได้ละ ได้เล๊ยยย !!

<img src="https://arnondora.in.th/content/images/2021/01/network_upgrade_fibre_optics_05.jpg" class="kg-image" alt="">

เรารู้ว่า หน้าตาของ Img Tag ในเว็บของเราเป็นแบบด้านบน และปลายทางของเราคือ เราต้องการที่จะเติม loading="lazy" เป็นอีก Attribute เข้าไป สิ่งที่เราทำได้คือการ Replace อะไรบางอย่าง อื้ม.... ยังไงดีนะ อะได้งั้นเราทำแบบนี้ละกัน

<img src="https://arnondora.in.th -> <img loading="lazy" src="https://arnondora.in.th

งั้นเรา Replace ต้นของ Img Tag เลยละกัน เราจะได้มั่นใจด้วยว่ามันไม่ไป Replace มั่วซั่ว ตามด้วย Lazy Loading แล้วตามด้วย Src ที่ต้องเป็น Host Name ของเราเท่านั้น

location / {
        include /config/nginx/proxy.conf;
        proxy_pass http://x.x.x.x:x;

        #disable compression 
        proxy_set_header Accept-Encoding "";
		
		#rewrite the html
        sub_filter_once off;
        sub_filter_types text/html;
        sub_filter '<img src="https://arnondora.in.th' '<img loading="lazy" src="https://arnondora.in.th';
		
}

ก็บรรเลงเลย เริ่มจาก 2 บรรทัดแรกเป็นเรื่องของการทำ Reverse Proxy ปกติข้ามไป ข้ามไปที่ตรง Rewrite HTML เลย เริ่มจากอันแรกคือ sub_filter_once มันคือ จะให้มันหาเจอแค่อันเดียว Replace แล้วจบแล้ว Replace All ทั้ง Document เลย ในที่นี้ เราต้องการ Lazy Load ทุกรูปใน HTML Document เราก็ต้องปิดไป

sub_filter_types คือเราจะระบุเลยว่า จะให้มัน Filter บน Document ประเภทไหน ในที่นี้เราสนใจเฉพาะ HTML Document เท่านั้น เพราะ Img Tag อยู่ใน HTML ก็ใส่เป็น text/html ไป

สุดท้ายเป็น sub_filter ที่เราจะบอกว่า เราจะ Replace String แบบไหน เป็น String แบบไหนอีกที ก็คือตามที่เราคุยกันก่อนหน้านี้ว่า เราจะหา Img Tag แล้วตามด้วย Src ที่มาจากเว็บเรา แทนที่ด้วยเหมือนเดิม แต่คั่นด้วย Lazy-Loading ไปก็เป็นอันจบ

แต่ถ้าเราทำแค่นั้น ปัญหาคือ มันจะหา Tag ไม่เจอ เพราะ HTML มันถูก Compress ไปแล้ว ทำให้เราต้องเข้าไปปิด Compression ก่อนด้วยการแก้ Header Accept-Encoding เข้าไปให้ว่าง มันจะได้ไม่ชอบการ Compress ก็จะได้ตัวเปล่า ๆ ออกมา ทำให้เราสามารถ Replace String ที่เราต้องการได้ ย้ำว่า การเข้าไปปิด Compression ตรงนี้ไม่ได้ส่งผลกระทบในการโหลดของ Client แต่อย่างใด เพราะเป็นการปิด Compression ระหว่าง Nginx และ Web Server ที่อยู่ใน Localhost เท่านั้น

ได้ผลมั้ย

บอกเลยว่า ได้ผล !!! ดูจากวีดีโอด้านบนได้ เราจะเห็นได้ว่า มันใช้ได้ เมื่อหน้าโหลดมา มันจะยังไม่โหลดรูปทั้งหมด แต่พอเรา เลื่อนลงไปเรื่อย ๆ รูปมันก็จะค่อย ๆ โหลดขึ้นมา

พอเข้าไปดูใน HTML ของหน้านั้นเราก็จะเห็นว่า มันมี Lazy Loading ใส่มาให้เราพร้อมใช้งานแล้ว ก็ถือว่าเป็นอันเรียบร้อย

สรุป

สุดท้ายเราก็จะได้ Image Lazy-Loading มาใช้งานกันเรียบร้อย โดยที่เราไม่ต้องเข้าไปยุ่งกับ Ghost ให้ปวดหัวอีก ถือว่าเป็นวิธีที่เราว่าโอเคมาก ๆ ซึ่งจริง ๆ แล้ว เราสามารถนำไป Apply กับกรณีอื่น ๆ ได้เช่นกัน เช่น Wordpress ก็ทำได้ ถ้าเราไม่อยากที่จะไปยุ่งกับตัว Wordpress เอง ก็ลองเอาไปเล่นดูได้