Tutorial

Dev Log - บันทึกการ Serve Webp และ Lazy-Loading บน Ghost CMS

By Arnon Puitrakul - 05 กรกฎาคม 2020

Dev Log - บันทึกการ Serve Webp และ Lazy-Loading บน Ghost CMS

หลังจากที่ Post ก่อนหน้านี้สักพักมาแล้ว เราเล่าเรื่องที่เราย้ายเว็บ arnondora.in.th มาใช้ Ghost CMS เว็บเราแน่นอนว่าเดือดร้อนจากปัญหาการ Optimise บางอย่างของ Ghost ที่ไม่มีใน Gatsby ซึ่งปัญหาที่ร้ายแรงสำหรับเว็บนี้มาก ๆ คือ รูปภาพ

เกิดอะไรขึ้นกับ รูปภาพ ?

จากที่หลาย ๆ คนที่อาจจะเคยอ่านบทความจากเว็บเรามาก่อน น่าจะพอเข้าใจว่า เราเขียนรีวิวเยอะพอตัวเลย ซึ่งในแต่ละ รีวิว รูปภาพมันเยอะมาก ๆ บางหน้าเยอะไปถึง 60 รูป หรือมากกว่านั้นเลย

ปัญหาทั้งหมด มันหายไปหมดเลย เพราะ Gatsby มันทำให้เราหมดเลย ตั้งแต่การ Optimise, Serve Next-Gen Format และ การ Resize รูปให้เราหมดเลย ทำให้ปัญหานี้มันเลยไม่ได้ถูก Issue ลงใน Lighthouse สักเท่าไหร่

แต่พอมาใช้ Ghost แน่นอนแหละ ว่ามันไม่มีระบบพวกนี้มาให้เราแน่นอน กับหลังจากที่ไปอ่านใน Board หลาย ๆ ที่มา ก็ประสบปัญหาเดียวกัน และยังไม่ได้มี Solution ออกมาแล้วมันใช้ได้เลยสักเท่าไหร่ งั้นเอาหล่ะ เพื่อ Performance ที่ดีขึ้น มาจัดสักหน่อยละกัน

ทำ Lazy-Loading

ถ้างั้น เรามาลองแก้ปัญหาแรกกันก่อนคือ ถ้ารูปมันใหญ่นัก เราก็ค่อย ๆ โหลดมันหลังจากที่โหลดหน้าทั้งหมดเสร็จแล้วละกัน พูดง่าย ๆ มันคือ การทำ Lazy-Loading นั่นเอง

แบบที่ง่ายที่สุด น่าจะเป็นการที่เราลักไก่มันโดยการที่เรารู้ว่า ถ้าเราใส่ src ใน Img Tag ปกติ เมื่อ Web Browser มัน Parse และ อ่านเจอ มันก็จะเข้าไปโหลดรูปมาให้เราทันที แต่เราไม่ต้องการแบบนั้น เราต้องการให้หน้ามันโหลดเสร็จก่อน งั้น เราก็หลอกมันซะเลยสิ

ด้วยการที่เราเอา src ไปซ่อนไว้ก่อน พอหน้าโหลดเสร็จ เราก็เอา src ออกมา เพื่อให้ Web Browser โหลดรูปนั้นขึ้นมา ทำให้เราจำเป็นต้องทำการซ่อน src ไว้ล่วงหน้าโดยการ Hard Code ไปเลย จะเปลี่ยนเป็น Code ด้านล่างนี้

<img class="feature_image" loading="lazy" data-src="{{feature_image}}" alt="{{title}}"/>

ง่าย ๆ ก็คือ ก็เปลี่ยนจาก src เป็น data-src เท่านี้ Web Browser ก็ไม่รู้จักแล้ว เหมือนละครไทยที่ นางเอกติดหนวด พระเอกก็จำไม่ได้แล้ว

<script>
        function init() {
        var imgDefer = document.getElementsByTagName('img');
        for (var i = 0; i < imgDefer.length; i++) {
            if (imgDefer[i].getAttribute('data-src')) {
            imgDefer[i].setAttribute('src',imgDefer[i].getAttribute('data-src'));
            }
        }
        }

        window.onload = init;
</script>

จากนั้นเขียน Javascript ง่าย ๆ ในการที่จะเอาค่าจากใน data-src มาแปะใน src เพื่อให้รูปภาพถูกโหลดเมื่อหน้าโหลดเสร็จ ก็ทำออกมาง่าย ๆ แบบนี้แหละ เราไปลอก Code สักที่มา

ซึ่งหลังจากใช้ Lighthouse ทดสอบออกมา คะแนน Performance ก็ไม่ได้เพิ่มขึ้นสักเท่าไหร่ นั่นเป็นเพราะ รูปภาพที่จะทำแบบนั้นได้ จะต้องเป็นรูปที่เราเขียนอยู่ใน Theme นั่นคือ รูปภาพที่อยู่ในบทความต่าง ๆ มันก็จะ Lazy-Loading ด้วยวิธีนี้ไม่ได้ ทำไงดีละทีนี้

เลยคิดวิธีพิเรนออก ในเมื่อเรา Custom กับ Image Tag ไม่ได้เลย งั้นเราก็เอา src ไปซ่อนก่อนที่มันจะ Interpret สิ เอออออออ ฉลาดดดดดดดดดดด IQ20000

<script>
        document.addEventListener("DOMContentLoaded", function() {
            var imgDefer = document.getElementsByTagName('img');
            for (var i = 0; i < imgDefer.length; i++) {
                if (imgDefer[i].getAttribute('src')) {
                    imgDefer[i].setAttribute('data-src',imgDefer[i].getAttribute('src'));
                    imgDefer[i].setAttribute('src','data:image/png;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=');
                }
            }
        });
</script>

เราเลยบอกว่า งั้นหลังจากที่มัน Parse เป็น DOM แล้ว เราก็เอา Content ที่อยู่ใน src ทั้งหมด ไปแปะไว้ที่ data-src ส่วน src เราก็แทนที่ด้วย Base64 Image ก็คือ รูปเปล่า ๆ นั่นเอง เพื่อให้มันมีอะไรให้โหลดตอนหน้ายังโหลดไม่เสร็จ จากนั้น เราก็ใช้ Script อันก่อนหน้านี้ในการเอา src ออกมาเมื่อหน้าโหลดเสร็จ ใช่ฮ่ะ อ่านคร่าว ๆ แล้ว ดูเหมือนจะรอด แต่ ไม่ !!!

ไม่ต้องไปไกลถึง Lighthouse ทดสอบอะไรเลย แค่เราลองวัดเวลาโดยใช้ Performance Inspector ของ Google Chrome วัดออกมา พบว่า Script Eval Time บวกขึ้นมาเกือบ 2 เท่าของก่อนที่จะแก้ซะอีก

นั่นเป็นเพราะว่า มันต้องมานั่งสลับ src ให้ไปอยู่ที่ data-src แล้วก็ทำกลับมาอีก แถมในขณะที่มันกำลังสลับ src ให้เป็น data-src มันอยู่ในช่วงพึ่งได้ DOM ออกมา ยังไม่ได้ Render หน้าออกมาเลย นั่นแปลว่า First Paint หรือเวลาในการที่หน้ามีการ Render อะไรออกมา มันก็จะช้ากว่า ที่มันควรจะเป็นแน่ ๆ อื้ม...... เอาไงดีละหว่า

Native Lazy-Loading มาช่วยแย้ววววว

ข้อเสียของการทำ Lazy-Loading แบบเก่าคือ หน้ามันจะเลื่อน หมายความว่า เมื่อ Web Browser มันโหลดหน้าขึ้นมา ไม่ก็เห็น Img แหละ แต่มันก็ไม่รู้จะอ่านอะไรออกมา เพราะเราไม่ได้บอกอะไรมันที่มันเข้าใจเลย ทำให้มันก็ทำออกมาเป็น Tag เปล่า ๆ แล้วมันจะเป็นเหมือนกับรูปเสีย เพราะมันหา src ไม่เจอนั่นเอง

แล้วพอหน้าโหลดเสร็จ เราสลับ data-src เป็น src รูปจะถูกโหลด และ พอโหลดเสร็จ มันก็จะเอารูปมาวางไว้ แต่เพราะมันไม่ได้เว้นที่ไว้นี่แหละ มันเลยต้องมาแทรก ทำให้เราเจอปรากฏการณ์ที่หน้ามันจะเลื่อนด้วย กับ รู้สึกว่าหน้ามันกระพริบนิด ๆ ทำให้ Experience มันไม่ได้ดีเลย

พอเราไปหา ๆ ใน Google มา เราเจอสิ่งที่เรียกว่า Native Lazy-Loading หืมมมม มันเหมือนพระเอกขี่ม้าขาวมาหาเลย เพราะเราสามารถที่จะ Implement เจ้านี่ได้โดยง่ายมาก ๆ เพียงแค่เราเติม loading="lazy" ลงไปก็ใช้ได้เลย เราจะไม่เจออาการรูปเสีย แล้วค่อยโหลดกลับขึ้นมา และ เราไม่ต้องนั่งเขียน Javascript เองด้วย โหหหหห โคตรสบายละทีนี้

แต่ ๆๆๆๆๆๆๆ Native Lazy-Loading นั้นยังไม่ได้ถูกรองรับใน Web Browser บางตัว อย่าง The Next IE หรือ Safari นั่นเอง แต่พอเรามานั่งดู Statistics ใน Google Analytics แล้วเราพบว่า ส่วนใหญ่มาก ๆ เกิน 90% จบที่ Google Chrome หมดเลย ทำให้เราเออ ปล่อยที่เหลือไปได้ เพราะมันไม่ได้เป็นส่วนที่ทำให้หน้ามันไม่โหลดเลย โหลดได้แต่ช้าหน่อยก็โอเคแหละ และใน Google Chrome เองก็จะรองรับตั้งแต่ Version 76 เป็นต้นไปด้วยนะ

คิดว่ามหากาพย์แห่งการพจญภัยจะจบลงแบบ Happy Ending เท่านี้ก็คิดผิดซะแล้ว สุดท้ายแล้วไม่ว่าจะเป็น Lazy-Loading แบบไหน เราก็จำเป็นที่ต้อง Modify ตัว Img Tag ที่อยู่ใน Content อยู่ดี แน่นอนว่า อย่างที่บอกไปคือ มัน ทำ ไม่ ได้ เอาแล้ว ไงอะ

Let the hacking began !

ในเมื่ออ่อนโยนแล้ว มันก็ทำไม่ได้ งั้นเราก็ต้องจับ ผี ตัวนี้มาขืนใจกันบ้างแล้วละ ต้องจับมาล้วงลับ เล่นตับแก กันหน่อย

เราลองคุ้ยอยู่นานแสนนาน จนเข้าไปคุ้ยไส้ในของ Ghost เลยว่า Editor มันยัด HTML ตอนที่เราเติมรูปเข้าไปยังไง และหลายจุดอีกมากมาย จนสุดท้าย เข้าไปดูใน Database ของ Ghost เราพบว่า เอ๋ !!!!!!!!! เธอเห็นเหมือนที่ฉันเห็นมั้ย B1 ฉันก็เห็นเหมือนกันแหละ B2 ใช่ฮ่ะ มันคือ Field ใน Database ชื่อว่า html สนุกแล้วละทีนี้

พอเราลองเอาออกมาแคะดู เราพบว่า ฮั่นแน่ นี่เล่นเก็บ Generated HTML พร้อม Serve เลยนี่หว่า นิสัยเสียจริง ๆ เลยนะนายเนี่ย เราเลยลองแก้มันไปหน่อย พอเข้าหน้านั้น ก็พบว่า มันถูกแก้จริง ๆ ด้วย

พอเราเข้ามาในหน้า Editor มันก็มีที่เราแก้อยู่จริง ๆ นั่นแปลว่า สิ่งที่ Editor ของ Ghost ทำ มันเข้าไปอ่าน Field html ออกมาจาก Database แล้ว Render ในหน้า Editor นั่นเองงงงงง เหมือนเริ่มเห็น Solution แล้วว่า ถ้าเราแก้ HTML ทั้งหมดที่อยู่ใน Database โดยการเติม loading="lazy" ก็จะทำให้รูปภาพทั้งหมดใช้ Native Lazy-Loading ได้แล้ว

นอกจากนั้น ตัวรูปภาพเอง เราอยากให้มันได้ Serve Next-Gen Format อย่าง Webp ด้วย แต่ไม่ใช่ทุก Web Browser ที่รองรับ Webp ทำให้เราต้องทำ Fallback ด้วย

<picture>
    <source srcset="/content/images/2020/06/how_to_choose_harddisk_1.webp" type="image/webp">
    <source srcset="/content/images/2020/06/how_to_choose_harddisk_1.jpg" type="image/jpeg">
    <img src="/content/images/2020/06/how_to_choose_harddisk_1.jpg">
</picture>

ซึ่งการทำ Fallback เรานั่งอ่านมาในเว็บจากต่างประเทศ เขาบอกว่า ให้เราใช้ Picture Tag แล้วในนั้น เราก็อัด Source ไปหลาย ๆ ตัวตั้งแต่ Webp ที่เราต้องการ แล้วก็ Format ปกติลงไป และ Fallback ด้วย Img Tag สำหรับ Web Browser ที่ไม่ได้รองรับไปด้วย ทำให้ HTML ที่จะต้องเปลี่ยนมันเลยหน้าตาประมาณนี้แหละ

แต่การที่เราเปลี่ยน Img Tag เป็นแบบด้านบนตรง ๆ เลยมันก็ไม่ได้อีก เพราะถึงเปลี่ยนไปมันก็จะหา Source ไม่เจออยู่ดี เพราะไฟล์ที่เป็น Webp มันไม่มีอยู่จริงในเครื่อง ยังไงก็โหลดขึ้นมาไม่ได้ ทำให้เราต้องมานั่งแปลงรูปอีก

เล่นแร่แปรสกุลรูป

สำหรับการแปลงรูป นั่นทำได้ผ่านโปรแกรมมากมายที่เราสามารถโหลดมาใช้งานได้ตรง ๆ เลย หนึ่งในโปรแกรมที่ Gatsby ใช้สำหรับการแปลงเป็น Webp นั่นคือ cwebp แน่นอนว่า มันสามารถ Install ผ่าน Homebrew ได้เลย ง่ายจอบอ แล้วเวลาเราแปลง เราก็เรียกไฟล์ต้นฉบับ แล้ว Save ออกเป็นไฟล์สกุล webp ใน Path เดียวกันได้เลย เป็นอันจบ ชิว ๆ

ปกติ เวลาเราเขียน Content แล้วเอารูปแปะ เราจะต้องเอารูปต้นฉบับทั้งหมดไปย่อขนาด และ Compress ซะก่อนที่จะ Upload ลงเว็บไปได้ ไม่งั้นพอเวลาทุกคนโหลดมา ก็จะเจอกับรูปต้นฉบับจากกล้องไฟล์ละ 5-10 MB ไปเลย ซึ่งจะบ้าเราะะะะะะะะะ

ซึ่งถ้าคนที่อ่าน Webp ได้มันก็จบแหละ เพราะมัน Compress มาแล้วขนาดไม่ได้อลังการงานสร้างมากขนาดนั้น แต่ คนที่ใช้ Web Browser อื่นละ อย่างน้อยเขาไม่ควรจะเสีย Traffic ไปกับรูปที่มีขนาดใหญ่เท่า 🐘 เลย ทำให้เราต้อง Compress ไฟล์ต้นฉบับไปด้วย

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

ทำให้เราต้องใช้ Lossy Compression แทนที่ ยอมเสียคุณภาพของภาพไปซะหน่อย (ลองดูเทียบกัน แทบไม่ต่างเลย) แต่แลกมากับขนาดที่ลดลงเยอะมาก ๆ แบบ มาก ๆ เลย เลยไปนั่งดูว่า โปรแกรมอะไรบ้างที่ใช้ แน่นอนว่า Source ที่น่าเชื่อถือสำหรับเราอย่าง web.dev ของ Google ใน Post เรื่องการ Optimise Image แนะนำว่าให้ใช้ imagemin-cli ในการสั่ง Compress ได้เลย

คิดว่าจะราบลื่นเหรอ เหอะ คิดผิดซะแล้ว อันความคุ้ย Documentation อยู่นานมาก ก็ยังงง ๆ กับ Plugin ที่ไม่ได้เขียนมาเผื่อ CLI เท่าที่ควร เลยเออ ยอมแพ้ แล้วไปซบตัวโปรแกรมที่โหลดมาเป็น Binary พร้อมรันใน CLI ตรง ๆ เลย

โดยที่ไฟล์ PNG เราเลือกใช้ pngquant ปรับ Quality ลง 50% ไปเลย ส่วน GIF เราก็เลือกใช้ gifsicle และสุดท้าย JPG เราก็เลือกใช้ Mozjpg

ปัญหาของการใช้ Lossy Compression คือ มันจะลดคุณภาพลงไปเรื่อย ๆ ทำให้ถ้าเรา Compress ซ้ำ ๆ ลงไปเรื่อย ๆ คุณภาพของภาพก็อาจจะแย่ลงเรื่อย ๆ ก็เป็นได้ ทำให้เราต้องแก้ปัญหานี้ด้วยการที่เราจะเก็บไฟล์ Original เอาไว้ นั่นทำให้เราจะต้องเก็บแยกเป็น 3 ไฟล์แยกคือ Webp ที่เราแปลงไปก่อนแล้ว, รูปต้นฉบับ และ รูปที่ผ่าน Lossy Compression

พอเวลาเราจะ Compress เราจะเรียกไฟล์ต้นฉบับมาทำ ทำให้คุณภาพ ของเราจะเหมือนเดิมตลอด แต่ รูปภาพในเว็บเราคือ เยอะมาก ๆ เลยนะ ถ้าต้องมานั่งทำแบบนี้แต่ละทีก็ดูจะไม่ตลกเท่าไหร่ เหมือนตอนที่ใช้ Gatsby เลย

ทำให้เราจำเป็นที่จะต้องเอา Database มาเก็บว่า Image ไหนเราจัดการไปแล้วบ้าง พออ่านมาแล้วเจอ จะได้ไม่ต้องทำซ้ำ โดยการสร้าง Table ชื่อ assets เพื่อเก็บไฟล์ที่ผ่านการจัดการเรียบร้อยแล้ว

รูปที่ไม่ได้ใช้แล้ว

Featureนึงที่ Ghost ไม่มีแต่ Wordpress มีคือ Asset Manager หรือบางคนเรียก File Browser อะไร ก็ว่ากันไป แต่มันเป็น Feature ที่ทำให้เราสามารถ Browse รูปภาพที่อยู่ในเครื่อง Server ของเราได้

เหตุการณ์ที่เราเจอคือ เมื่อเรา Upload รูปลงไปแล้ว แล้วพบว่า เอ๋ ผิดรูป หรือมีปัญหาอะไรกับรูป แล้วเรา Upload ของใหม่เข้าไป ปรากฏว่า รูปใหม่ มันก็ขึ้นในหน้าเว็บแหละ  แต่รูปเก่ามันก็คาอยู่ในเครื่อง รูปเดียวไม่เท่าไหร่ แต่ถ้ามันมากกว่านั้น พื้นที่ในการเก็บข้อมูลของเรามันจะหมดไปกับอะไรแบบนี้เยอะมาก เปลืองแหละว่างั้น

ทำให้เราเลยสร้างอีก Table ใน Database คือ asset_post สำหรับเก็บว่า ในแต่ละบทความมีการใช้รูปอะไรบ้าง เพื่อที่เราจะสามารถโปรแกรมให้มันลบไฟล์ที่ไม่ได้ถูกใช้แล้วออกไปเลยก็ได้

แปลง HTML

หลังจากที่เราได้รู้แล้วว่า เราสามารถแก้ HTML ได้โดยตรงจาก Field ชื่อ html ที่อยู่ใน Database ได้เลย และไฟล์รูปของเราก็พร้อมที่จะเอาไป Embed ในหน้าเว็บแล้ว ขั้นตอนต่อไป เราจะเติมพวก Native-Lazy Loading และ Webp ลงไปกัน

เราเลือกใช้ Python ในการเขียน เพราะเราถนัด และ ต้องการ Rapid Prototype เพื่อจะ Proof of Concept ก่อน

SELECT id,title,html FROM POST ORDER BY created_at DESC

เริ่มจากการดึงตัว HTML จาก Database กันก่อน ใน Python เองก็มี Adapter สำหรับการเชื่อมต่อ MySQL อยู่แล้ว ลองเข้าไปอ่านกันได้ ติดตั้งเราทำผ่าน pip ไปเลยง่ายจบ แล้วเราก็ Query Post ออกมาจาก Table posts ตรง ๆ ได้เลย

แต่สังเกตุที่ Query ของเรา เราไม่ใช้ Select All Fields นะ เราเลือกมาแค่ที่เราต้องการจะใช้ ตรงนี้เราอยากจะบอกว่า เวลาเราเขียน Query อย่ามักง่ายเอามันมาให้หมดแล้วไม่ใช้ เพราะการดึงออกมา มันกินทรัพยากร และ เวลาในการดึงอยู่ ยิ่งเราดึงน้อย ใช้เงื่อนไขให้น้อยที่สุด แต่ยังได้สิ่งที่เราต้องการก็ยิ่งดี มีระบบเคยล่มเพราะการ Select All Fields มาแล้วนะบอกเลย

cursor.execute(query)

dataset = []

for (id, title, html) in cursor :
    dataset.append({'id' : id, 'title' : title, 'html': html})

แต่ ๆ เวลา Adapter มันคืนผลกลับมา มันให้มาเป็น Cursor นี่ก็ลักไก่ด้วยการแปลงมันให้เป็น List ของ Dictionary เพื่อความง่ายในการเข้าถึง เพราะสุดท้ายเราก็ต้องทำทุก Post อยู่แล้ว

position = html.find()

จากนั้นในแต่ละบทความเราจะต้องมา Process ต่ออีก เรารู้ว่าเป้าหมายของเราคือการเปลี่ยนตรง Img Tag ทั้งหมด นั่นแปลว่า เรารู้ว่า สิ่งทีเราต้องมองหาคือ <img .....> เราจะยุ่งกับแค่นั้น ความดีงามของ Python คือนางมี Method ชื่อว่า find() สำหรับการหา Keyword บางอย่างใน String ซึ่งมันจะคืนค่ากลับมาเป็นตำแหน่งของ String ที่ตรงกับ Keyword ที่เราป้อนเข้าไป

start_position = html.find('<img')
end_position = html.find('>', start_position)

งั้นเอางี้ละกัน เราลองมองหาจุดเริ่มต้นกันก่อน นั่นคือ start_position ตามหลักแล้ว มันไม่ควรจะมีอะไรที่เป็นตัวเครื่องหมายมากกว่า อยู่ระหว่าง HTML Tag แน่นอน ไม่งั้นเวลาอ่านออกมา ยังไง ๆ มันก็พังอยู่แล้ว เราก็เลยบอกว่า งั้น จุดจบหรือ end_position เราให้มันหาเครื่องหมายมากกว่า จากตำแหน่งที่ต่อจากจุดเริ่มต้น ทำให้เราจะได้ ตำแหน่งของตัวปิดของ Img Tag นั้นออกมา

current_tag = html[start_position : end_position+1]

จากนั้นก็ง่ายเลย คือ เราก็ Substring ออกมา เราก็จะได้ทั้ง Tag ออกมาหมดเลย ส่วนถามว่าทำไมต้องบวก 1 ที่ end_position ลอง Copy Img ของสักที่มาแล้วลอง Substring ใน Python ดู

<img src="/content/images/2020/07/review_nomad_base_station_01.jpg" alt="รีวิว Nomad Base Station">

จากนั้น เราต้องมาแยกส่วนประกอบใน Img Tag เช่น src และ alt เริ่มที่ src กันก่อน ถือว่าง่ายที่สุดแล้ว ถ้าเราลองดูตัวอย่างจากด้านบน เราก็แค่เรียก split() จาก Space ไป แล้วเราก็ substring แต่ละช่องออกมา เช็ค 3 ตัวแรก ว่าเป็น src หรือเปล่า ถ้าใช่ เราก็ substring ด้านหน้า  และ เครื่องหมายคำพูดด้านหลังออกไป ก็จะได้ src ออกมาแล้ว

ส่วน alt ถ้าเราใช้วิธีเดียวกัน น่าจะยากไปหน่อย ลองไปดูที่ alt มันจะมี Space อยู่ถ้าเราใช้วิธีเดียวกัน มันจะยากมาก งั้นเราลองวิธีเดิม ๆ เหมือนที่เราแยก Img Tag ออกมาเลย นั่นคือ การ find หา alt=" แล้วเราก็หาเครื่องหมายคำพูดอันต่อไป ก็จะได้ alt ออกมาแล้ว

ต้องยอมรับว่า บาง Content เราใช้ External Image Server เช่นพวก CDN ต่าง ๆ ทำให้พวกนี้จะใช้ Webp ไม่ได้ ต้องใช้ Native Lazy-Loading อย่างเดียว ต้องแยกเคสออกไป

if os.path.isfile() :
	//Webp Fallback
else :
	//Native Lazy-Loading

วิธีการเช็คง่าย ๆ คือ เราก็เช็คในเครื่องเลยว่า มันมีไฟล์นี้อยู่จริงมั้ย ผ่าน isfile() มาเช็คได้เลย ถ้ามีไฟล์รูปอยู่จริง เราก็ Generate Webp Fallback ได้เลย หรือถ้าไม่มี เราก็ใช้แค่ Native Lazy-Loading ก็ได้เลย

modified_tag = '<img loading="lazy" ' + current_tag[4:]

เริ่มจาก Native Lazy-Loading ง่ายที่สุดแล้ว จาก Img Tag ปกติ เราก็แค่เติม loading="lazy" เข้าไปเท่านั้น วิธีที่เราคิดได้แบบเร็ว ๆ คือ เราเอา <img ออกจาก Tag ที่เรา Extract มา แล้วเราก็เติม หัว Tag พร้อมกับ Native Lazy-Loading พร้อมกับส่วนที่เหลือลงไปก็เรียบร้อย เย้

<picture>
    <source srcset="/content/images/2020/06/how_to_choose_harddisk_1.webp" type="image/webp">
    <source srcset="/content/images/2020/06/how_to_choose_harddisk_1.jpg" type="image/jpeg">
    <img src="/content/images/2020/06/how_to_choose_harddisk_1.jpg">
</picture>

อีกประเภทคือเป็น File รูปที่เรา Serve เอง ซึ่งเราก็แปลงไปก่อนแล้ว ถ้าเรากลับไปดูที่ Picture Tag เราจะเห็นว่า ส่วนที่เราต้องเติมจริง ๆ น่าจะเป็น ที่อยู่ของ Webp และ ต้นฉบับ และ ประเภทของ ไฟล์ต้นฉบับ ซึ่งพวกที่อยู่เรามีหมดแล้ว เหลือแต่ ประเภทของไฟล์

import mimetypes

mimetypes.guess_type()

ประเภทของไฟล์เราสามารถดึงออกมาจากไฟล์ได้เช่นกัน จาก Standard Library อย่าง mimetypes และเรียกคำสั่ง guesstype ออกมาแล้วแปะลงไปได้เลย เมื่อเราได้ส่วนประกอบครบหมดแล้ว เราก็จับมันยัดลงไปใน Picture Tag ได้เลย ง่ายมาก ๆ

นอกจากนั้น ใน Picture Tag เรายังสามารถ Optimise ได้อีก ด้วยการกำหนด Width แล้ว Serve รูปภาพที่ตรงกับ Width ของหน้าจอได้เลย แต่ที่เรายังไม่ได้ทำ เพราะเรายังหาไม่เจอว่า เราจะ Resize รูปได้ยังไง​ฮ่า ๆ ถ้าทำได้ไว้จะมาทำอีกที น่าจะใช้พวก imagemagick ได้

ยังไม่จบบบบ

คิดว่าจะจบแล้วสินะ ยัง ฮ่า ๆๆๆๆๆ ถ้าเราใช้ Logic แค่นี้ไป ปัญหามันจะเกิดทันที เพราะถ้าเรารันซำ้อีกครั้ง Img Tag ที่เราแปลงไปแล้วมันจะถูกแปลงซำ้อีก ทำให้ HTML ที่ต้องโหลดใหญ่ขึ้นเรื่อย ๆ ยุ่งขึ้นเรื่อย ๆ ดังนั้นก่อนที่เราจะแปลง HTML เราต้องเช็ตก่อนว่า มันถูกแปลงไปแล้วหรือยัง

ประกอบกับใน Picture Tag เราจะต้อง Fallback โดยการใส่ Img Tag ในนั้น วิธีที่เราใช้หามันไม่ได้สนใจเลยว่ามันจะอยู่ใต้ Picture Tag มั้ย อีก

วิธีที่เราเช็คง่าย ๆ คือ เราก็เติม status="converted" ลงไปใน Img Tag ที่อยู่ใต้ Picture Tag แล้วเช็คจากตรงนั้นเอา และ ภาพที่เราดึงมาจาก Server อื่น เราก็เช็คจาก Native Lazy-Loading แค่นั้นเลย

Automated it!

หลังจากที่เราเขียน Python Script เสร็จแล้ว ถ้าเราต้องมารันเองตลอดทุกครั้งที่เราลงบทความใหม่ เราว่ามันก็ไม่ใช่เรื่องเท่าไหร่ เราก็เล่นง่าย ๆ เลยคือใช้ Crontab รันทุกเวลาที่เราตั้งเอาไว้แค่นั้น

ทำให้บทความที่เราลงไปก่อนที่ Script จะแปลง ก็ยังเข้าใช้งานได้ แต่ Feature พวก Next-Gen Image และ Native Lazy-Loading ก็จะใช้ไม่ได้ แต่ก็ยังเข้าใช้งานได้ปกติเลย ดังนั้นวิธีนี้ไม่มี Downtime เลยสักนิดเดียว ถือว่าผ่านนนนนน

สรุป

เรียกได้ว่าเป็นมหากาพย์แห่งการอยากใช้ Webp และ Native Lazy-Loading เลย ด้วยการต้องมานั่งแปลง Webp เอง นั่งแปลง HTML เอง เรียกได้ว่าเล่นแร่แปรธาตุเลยก็ว่าได้ ด้วยวิธีนี้ มันทำให้ความเป็นไปได้แทบไม่มีที่สิ้นสุดเลยนะ ถ้าเราเข้าไป Modify HTML ได้ มันคือ เราจะเติมอะไรก็ได้ลงไปเลยนะ ทั้งหมดที่เรานั่งเขียนก็ใช้เวลาไม่นานมาก ประมาณ 3 คืนเท่านั้นเอง แต่วิธีทีเราเอามาเล่าในวันนี้คือ เป็นเพียงแค่ Final Solution เท่านั้น กว่ามันจะออกมาได้คือ มันผ่านการลองผิดลองถูกอยู่หลายวิธีเหมือนกัน

จากการลงมานั่งเขียนอะไรแบบนี้ ทำให้เรารู้สึกสนุกเหมือนตอนที่เขียนโปรแกรมใหม่ ๆ อีกครั้งเลย มันสนุกที่ได้นั่งคิด ได้นั่งลอง สนุกมากจริง ๆ อยากให้เราทำ Project อะไรเพิ่มในเว็บก็ Comment มากันได้ Project ต่อไป ที่คิดจะทำ น่าจะเป็นเรื่องของการ Optimise เว็บให้ดีขึ้นกว่านี้  (เราบอกเลยว่า ไม่ได้ 100 แน่นอน เพราะ Ads ที่เราแปะ เราควบคุมไม่ได้เลย) พร้อมกับการเพิ่ม Feature อย่างการแปลง Video เป็น Next-Gen Format อย่าง WebM และ ที่สำคัญเรื่อง SEO สำคัญมาก ๆ จะเขียนตัววิเคราะห์คุณภาพ Content ออกมา คล้าย ๆ กับ Yoast SEO ใน Wordpress เลย

ส่วนถ้าถามว่า เราจะ Open Source Script ก้อนนี้มั้ย ณ วันที่เราเขียนบทความจะยังไม่ได้ปล่อยออกมานะ เพราะทั้งหมดที่เขียนไปมันเป็น MVP (Minimum viable product) มาก ไว้เรา Optimise และเพิ่ม Feature หลาย ๆ อย่างแล้วจะ Open Source นะ

Read Next...

จัดการเรื่องแต่ละมื้อ แต่ละเดย์ด้วย Obsidian

จัดการเรื่องแต่ละมื้อ แต่ละเดย์ด้วย Obsidian

Obsidian เป็นโปรแกรมสำหรับการจด Note ที่เรียกว่า สารพัดประโยชน์มาก ๆ เราสามารถเอามาทำอะไรได้เยอะมาก ๆ หนึ่งในสิ่งที่เราเอามาทำคือ นำมาใช้เป็นระบบสำหรับการจัดการ Todo List ในแต่ละวันของเรา ทำอะไรบ้าง วันนี้เราจะมาเล่าให้อ่านกันว่า เราจัดการะบบอย่างไร...

Loop แท้ไม่มีอยู่จริง มีแต่ความจริงซึ่งคนโง่ยอมรับไม่ได้

Loop แท้ไม่มีอยู่จริง มีแต่ความจริงซึ่งคนโง่ยอมรับไม่ได้

อะ อะจ๊ะเอ๋ตัวเอง เป็นยังไงบ้างละ เมื่อหลายเดือนก่อน เราไปเล่าเรื่องกันขำ ๆ ว่า ๆ จริง ๆ แล้วพวก Loop ที่เราใช้เขียนโปรแกรมกันอยู่ มันไม่มีอยู่จริง สิ่งที่เราใช้งานกันมันพยายาม Abstract บางอย่างออกไป วันนี้เราจะมาถอดการทำงานของ Loop จริง ๆ กันว่า มันทำงานอย่างไรกันแน่ ผ่านภาษา Assembly...

Monitor การทำงาน MySQL ด้วย Prometheus และ Grafana

Monitor การทำงาน MySQL ด้วย Prometheus และ Grafana

นอกจากการทำให้ Application รันได้แล้ว อีกเรื่องที่สำคัญไม่แพ้กันคือการวางระบบ Monitoring ที่ดี วันนี้เราจะมาแนะนำวิธีการ Monitor การทำงานของ MySQL ผ่านการสร้าง Dashboard บน Grafana กัน...

เสริมความ"แข็งแกร่ง" ให้ SSH ด้วย fail2ban

เสริมความ"แข็งแกร่ง" ให้ SSH ด้วย fail2ban

จากตอนที่แล้ว เราเล่าในเรื่องของการ Harden Security ของ SSH Service ของเราด้วยการปรับการตั้งค่าบางอย่างเพื่อลด Attack Surface ที่อาจจะเกิดขึ้นได้ หากใครยังไม่ได้อ่านก็ย้อนกลับไปอ่านกันก่อนเด้อ วันนี้เรามาเล่าวิธีการที่มัน Advance มากขึ้น อย่างการใช้ fail2ban...