รีวิวการใช้ Qwik กับโปร⁠เจ⁠กต์ Senate ของ WeVis

โปรเจกต์ Senate ของ WeVis ที่เพิ่งเปิดตัวไปไม่นาน เป็นอีกโปรเจกต์หนึ่งที่อยู่ในความรับผิดชอบของเรา ในครั้งนี้เราก็ได้เลือกทดลองที่จะใช้ Qwik เป็น Frontend Framework ในการขึ้นงานชิ้นนี้ขึ้นมา ในบทความนี้เราก็จะมารีวิวกันว่า เออ ใช้แล้วเป็นยังไงบ้าง จุดแข็ง/จุดอ่อน มีอะไรที่ชอบ/ไม่ชอบไหม เผื่อท่านที่เข้ามาอ่านจะได้ตัดสินใจว่าจะลองใช้ในโปรเจกต์ตัวเองหรืออื่นๆ ในอนาคต

Qwik คืออะไร?

ถ้าอธิบายสั้นๆ Qwik (ควิก) ก็เป็น Frontend Framework อันหนึ่งเหมือนกับ React.js, Vue.js, Svelte, Solid.js บลาๆ โดยมีจุดเด่น 2 อย่าง (จากหลายอย่าง) ที่เตะตา Frontend อย่างเรา คือ

  1. ชะลอการรันและโหลด JavaScript ไปให้นานที่สุดเท่าที่จะทำได้ (Delay execution and download of JavaScript for as long as possible.)
  2. เข้ารหัสสถานะการรันของแอปพลิเคชันและเฟรมเวิร์คบนฝั่งเซิฟเวอร์ แล้วนำมารันต่อที่ไคลเอนต์ (Serialize the execution state of the application and the framework on the server and resume it on the client.)

เนื่องจากวันนี้เราไม่ได้มา Tutorial เราจะยกตัวอย่างง่ายๆ ด้วย Counter ก็พอ

import { component$, useSignal } from '@builder.io/qwik';

export const Counter = component$(() => {
  const count = useSignal(0);

  return <button onClick$={() => count.value++}>{count.value}</button>;
});

ถ้าคนเคยเขียน Vue.js, Solid.js มาก่อนก็จะมองว่าระบบ Reactivity มีความคล้ายๆ กันแล้วก็ครอบ Component ด้วยฟังก์ชัน component$(/* ... */)

หลักการคร่าวๆ คือตัว Optimizer ของ Qwik จะทำการแตกจุดที่มีสัญลักษณ์ $ (ดอลลาร์) ออกไปเป็น Chuck หนึ่งๆ แล้วค่อยโหลดเข้ามาเมื่อมีความจำเป็น เช่น ถ้าผู้ใช้ยังไม่คลิกที่ปุ่มเพื่อเพิ่ม count ก็จะยังไม่โหลด JavaScript อะไรมาเลย เมื่อผู้ใช้คลิกแล้วถึงจะมีการ initiate และ update Signal ให้ถูกต้อง ทำให้หน้าเว็บ (ตามทฤษฎีแล้ว) ไม่ต้องโหลด JavaScript อะไรเลย

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

ขายมาขนาดนี้แล้ว หากท่านใดต้องการซื้อ เรียนเชิญได้ที่ Getting Started Qwikly เลยจ้า

Qwik React

ฟีเจอร์อีกอย่างหนึ่งที่พูดตรงๆ ว่าตอนแรกที่เห็นแล้วรู้สึกเลยว่า “เชี่ย ฉลาดว่ะ” และทำให้รู้สึกกล้าที่จะลองใช้ Qwik มากกว่าเดิม คือ Qwik React เพราะสิ่งหนึ่งที่ได้เรียนรู้จากการใช้ Framework มาหลายตัวคือ ถ้า Ecosystem ไม่แข็งพอ ก็จะกลายเป็นว่าเราต้องมาเสียเวลาทำอะไรที่ Framework อื่นมี หรือที่แย่กว่าคือ มาแก้ Technical Debt ของ Library ที่คนเคยพอร์ตมาแล้วไม่ทำต่อ 😭️

โดย Qwik React จะทำให้เราสามารถทำการพอร์ต React Component มาใช้ใน Qwik ได้ด้วยการใช้คำสั่ง qwikify$(/* ... */) โดยตัวอย่างข้างล่างจะเป็นการพอร์ต Headless UI มาใช้

/* eslint-disable qwik/no-react-props */
/** @jsxImportSource react */
import { qwikify$ } from '@builder.io/qwik-react';
import { Popover } from '@headlessui/react'

export const QPopover = qwikify$(() => (
  <Popover className="relative">
    <Popover.Button>Solutions</Popover.Button>

    <Popover.Panel className="absolute z-10">
      <div className="grid grid-cols-2">
        <a href="/analytics">Analytics</a>
        <a href="/engagement">Engagement</a>
        <a href="/security">Security</a>
        <a href="/integrations">Integrations</a>
      </div>

      <img src="/solutions.jpg" alt="" />
    </Popover.Panel>
  </Popover>
));

ถ้ามองเผินๆ นี่คือความสะดวกชั้นดีของคนที่มาใช้ Qwik เลย เพราะถ้าเขียนอะไรใน Qwik ไม่ได้ก็ qwikify$ โค้ด React.js เอาได้เลย

ใช่ ตอนแรกก็คิดแบบนั้นแหละ แต่จริงๆ แล้วเป็น Mindset ที่ผิด

เพราะการที่เรา qwikify$ โค้ด React.js มา ไม่ได้หมายความว่า Qwik จะทำการ convert เป็น Qwik Component จริงๆ ให้ เพียงแต่ทำ Island (คล้ายๆ Astro React Integration) ให้ React อาศัยอยู่ในขอบเขตของ Component นั้นได้ ซึ่งถ้าเราเอา Qwikify Component มา .map ก็จะทำให้เกิดการ init React.js ตามจำนวนที่มัน Loop ไป ซึ่งอาจจะส่งผลต่อ Performance โดยตรงได้

ดังนั้น อะไรพอร์ตเองได้พอร์ตเองจะดีที่สุด แล้วอะไรที่ไม่ได้จริงๆ ค่อยใช้ qwikify$ เอา (ตามไกด์ไลน์ของ Qwik เองก็เขียนไว้ว่าควรใช้ qwikify$ เพื่อค่อยๆ Migrate จาก React.js มาเป็น Qwik แทน)

และก็ปัญหาอันนึงของ qwikify$ ที่เจอคือ มันรับได้แค่ Children เดียว กลายเป็นว่า Logic ที่เหมือนจะ Reuse ได้ก็จะทำไม่ได้ เช่น Popover ที่ปุ่มกับการ์ดอาจจะต่างกันได้ทั้งคู่ (แต่อนิเมชันอาจจะเหมือนกัน) แต่พอรับได้แค่ Children เดียวก็ต้องใช้การ Duplicate Component เอา หรือใช้วิธีการ Pass ข้อมูลบางอย่างผ่าน Props เข้ามาแทน

ใช้แล้วเป็นยังไงบ้าง?

พูดได้ว่า ไม่ได้รู้สึกต่างจากการเขียน Solid.js อะไรมากที่ระบบ Reactivity เป็น Signal และเขียนด้วย JSX ฟีเจอร์พื้นฐานก็ไม่ได้ต่างจาก Framework อะไรอื่น (คือก็ทำ State ได้ แก้ State ได้ เอา State มา If, For ได้ ทำ Effect ได้ ฯลฯ) อาจจะเพราะด้วยเนื้องานของ WeVis ส่วนใหญ่ที่ทำเป็น Single-page Interactive Scrolly-telling เลยไม่ได้มีความจำเป็นที่จะต้องใช้ฟีเจอร์ลึกๆ เฉพาะๆ มากนัก

ข้อดีที่เห็นได้ชัดเลยคือเรื่อง Performance ที่ถึงแม้ว่างานนี้จะไม่ได้ optimize จัดขนาดนั้น (และคิดว่ามีบางจุดที่อาจจะทำให้ดีกว่านี้ได้) เว็บเราก็ยังได้คะแนนในช่วง 80+/100 ในหมวด Performance ของ PageSpeed อยู่ ซึ่งถ้าเปรียบเทียบกับโปรเจกต์ที่ผ่านๆ มา ถือว่าเป็นคะแนนที่สูงมาก เพราะการทำ Single-page ต่อให้ทำแบบ SSG ออกมา ก็มักจะมีปัญหาในเรื่องของความหนักของ Bundle (เนื่องจากต้องยัดทุกอย่างไว้ในหน้าเดียว แถมยังมา Hydrate ตอนโหลดใหม่อีก) ที่จะต้องทำการ Lazy Load แบบ Manual ออกมาเอง แต่เนื่องจาก Qwik แก้ปัญหาเหล่านี้ให้โดยอัตโนมัติ ทำให้ไม่มีปัญหาเรื่องนี้เลย

ส่วนข้อเสียที่เจอ (เอาจริงๆ มองว่าเป็นข้อควรระวังดีกว่า) คือ...

Lifecycle

Qwik ทำงานแบบ Server Side Rendering เป็นหลัก คือจะทำการ Render HTML บนเซิฟเวอร์ก่อน 1 ครั้ง แล้วจึงส่ง HTML มาที่ฝั่งไคลเอนต์ (เบราเซอร์) โดยจะมีความแตกต่างคือ Qwik จะทำการ Serialize ข้อมูล State ต่างๆ มากับ HTML ด้วย ซึ่งสิ่งนี้เป็นข้อดีของ Qwik ที่ทำให้ Qwik สามารถรันเว็บได้โดยไม่ต้อง Hydrate เหมือน Framework อื่น แต่ด้วย Lifecycle เหล่านี้ ทำให้เกิด Hook 2 ตัวคือ useTask$ ที่รันได้ทั้งเซิฟเวอร์และไคลเอนต์และ useVisibleTask$ ที่รันฝั่งไคลเอนต์ ที่ต้องทำความเข้าใจและใช้ให้ถูก ยังไม่นับการผูก Event ด้วย Hook useOn useOnDocument useOnWindow อีก ซึ่งมองว่าเป็น Pitfall สำหรับคนที่มาเขียน Qwik ใหม่ๆ ได้ (ดู Best Practices - Qwik)

ปัญหาหนึ่งคือ บางครั้งเราต้องผูก Scroll-triggered Animation ไว้กับ Intersection Observer ผ่านการใช้ useVisibleTask$ ซึ่งทำให้เกิดปัญหา 2 อย่าง

  1. ต้องใช้ useVisibleTask$ ซึ่งทำให้เกิดการ Hydrate (ต่อให้ตั้ง strategy เป็น "intersection-observer" ก็ต้อง Hydrate เพื่อติด Intersection Observer อยู่ดี)
  2. บางทีเราก็อยากให้มัน trigger ตอน Intersection Observer มี threshold เป็น 1 กลายเป็นว่าต้องติด Intersection Observer ใน useVisibleTask$(/* ... */) ที่ใช้ Intersection Observer 🤦‍♂️️

อีกข้อควรระวังอย่างหนึ่งที่เกิดจากการ Serialize คือ สิ่งที่เรา import มาใช้ใน Template จะโดน serialize ไปกับไฟล์ HTML ด้วย ซึ่งอาจจะทำให้ไฟล์ HTML ลำพังใหญ่กว่าไป fetch แยกเอาฝั่ง Frontend ได้

CSS จิ้ม Child Combinator ไม่โดน

เวลา Qwik แปะ Children หรือครอบ React Island (จาก qwikify$ เมื่อกี้) จะมี Element <q-slot>...</q-slot> หรือ <qwik-react>...</qwik-react> โผล่มา ซึ่งอาจจะทำให้ CSS จิ้ม Child Combinator (>) ไม่โดนได้

สิ่งที่ได้/บทสรุป

โดยภาพรวมแล้ว เราก็รู้สึกว่า Qwik ก็เป็น Framework ตัวหนึ่งเหมือนกันที่มาเขย่าวงการด้วยแนวคิดใหม่ๆ ที่ให้ความสำคัญในเรื่อง Performance เป็นหลัก รวมทั้งความสามารถในการทำ Island ที่ทำให้เราสามารถใช้ React.js ควบคู่กับ Qwik ได้ ที่น่าจะเป็นจุดดึงดูดให้ Dev หลายๆ คนสนใจมาลองใช้กัน

อย่างไรก็ตาม ถ้าเปรียบเทียบกันเชิง Ecosystem กับ Community แล้ว Qwik ก็ยังค่อนข้างมีความสดใหม่อยู่ ยังไม่นับถึงการมาของ Server Component ใน Framework แถวๆ นี้ที่ค่อนข้างจะมีความน่าสนใจไม่แพ้กัน ทำให้เราคงต้องมารอดูกันว่าในอนาคต Qwik จะยังมีความ Relevance ขนาดไหน แต่ก็ไม่แน่ว่า Qwik อาจจะเป็นตัวแปรหนึ่งที่ทำให้ Framework อื่นๆ เริ่มหันมาใช้แนวทางแบบนี้กันมากขึ้นก็ได้

สำหรับใครที่ซื้อแล้ว สามารถทดลองเล่นและศึกษาข้อมูลเพิ่มเติมได้ที่ https://qwik.builder.io/ ได้เลย

ขอบคุณรูปภาพพื้นหลัง Header จากพี่ทราย ดีไซน์เนอร์ผู้น่ารักประจำงาน Senate ของเรา

🪑FacebookTweet