Qwik – Vũ Khí Resumable Hạ Gục Hydration, Chinh Phục PageSpeed
🤗

Qwik – Vũ Khí Resumable Hạ Gục Hydration, Chinh Phục PageSpeed

Tags
Notion
SaaS
Tech
Qwik
Published
May 31, 2025
Author
AI summary
Tối ưu PageSpeed đã trở thành thách thức quen thuộc trong phát triển web. Chúng ta thường nỗ lực cải thiện từng điểm trên Lighthouse, nhưng hiếm khi đạt được bước nhảy vọt thực sự.
Một trong những hạn chế lớn nhất khi sử dụng Server-Side Rendering (SSR) cho các framework như Vue hay React chính là cách chúng thực hiện Hydration. Quá trình này đòi hỏi lượng JavaScript lớn phải tải về và chạy trên trình duyệt, làm chậm đáng kể thời gian tương tác. Đặc biệt với các website thương mại điện tử, việc tích hợp vô số thư viện bên thứ ba (tracking, thanh toán...) càng khiến bài toán tối ưu trở nên phức tạp hơn bao giờ hết.
Và rồi, Qwik ra đời với mục tiêu cải thiện đáng kể điểm số Lighthouse thông qua công nghệ Resumability.

Ngược dòng thời gian

Trước khi đi sâu vào Qwik, hãy cùng nhìn lại hành trình phát triển của các mô hình rendering đã và đang định hình thế giới frontend. Mỗi giai đoạn đều ra đời để giải quyết một vấn đề thực tế, và đều để lại dấu ấn sâu sắc.

Client-side Rendering (CSR)

Thời điểm ban đầu, chúng ta làm quen với khái niệm SPA. Ứng dụng web được tải thông qua một file HTML gần như trống, sau đó các JavaScript bundles sẽ chịu trách nhiệm tải dữ liệu, hiển thị nội dung và xử lý mọi tương tác ngay trên trình duyệt.
Ưu điểm lớn của CSR là mang lại trải nghiệm người dùng mượt mà sau lần tải đầu tiên: chuyển trang không cần tải lại, tương tác nhanh như ứng dụng native. Đây là mô hình rất phù hợp cho các dashboard, ứng dụng chat hay mạng xã hội – nơi người dùng tương tác liên tục.
Tuy nhiên, CSR cũng nhanh chóng bộc lộ nhiều hạn chế:
  • Time to Content lâu: Người dùng phải chờ tải và thực thi JavaScript mới thấy nội dung.
  • SEO kém: HTML ban đầu không chứa dữ liệu thực, gây khó khăn cho các bot tìm kiếm khi lập chỉ mục.
  • Hiệu năng thấp trên thiết bị yếu: Toàn bộ quá trình render được đẩy sang phía client.
Từ những vấn đề đó, SSR và SSG ra đời – như những nỗ lực "kéo" việc rendering quay trở lại server.

Server-side Rendering (SSR)

SSR giải quyết bài toán Time to First Byte (TTFB) và SEO bằng cách render toàn bộ HTML ngay trên server. Khi người dùng truy cập, họ nhận về một trang đã có nội dung đầy đủ – không còn là một khung rỗng như CSR.
Điểm mạnh:
  • Hiển thị sớm hơn: Cải thiện các chỉ số Web Vitals quan trọng như Largest Contentful Paint (LCP).
  • SEO tốt: Nội dung đã có sẵn trong HTML ngay từ đầu, dễ dàng cho bot tìm kiếm.
  • Hỗ trợ thiết bị yếu: Vì việc render đã được xử lý phía server.
Nhưng SSR cũng có cái giá phải trả:
  • Tăng tải server: Mỗi yêu cầu cần dựng HTML động, tiêu tốn tài nguyên.
  • Hydration vẫn là nút thắt: Sau khi HTML hiển thị, trình duyệt vẫn phải tải và chạy JavaScript để "kích hoạt" ứng dụng, khiến Time to Interactive (TTI) bị kéo dài.
Chính vấn đề hydration này đã mở ra hướng đi mới cho các mô hình rendering hiện đại.

Static Site Generation (SSG) – "Pre-render everything"

Với SSG, ý tưởng là render toàn bộ HTML tại build time, sau đó phục vụ như các file tĩnh từ CDN. Điều này mang lại tốc độ tải cực nhanh mà không tốn tài nguyên server trong quá trình hoạt động.
Các framework như Gatsby, Hugo hay Next.js (với getStaticProps) đã tận dụng mô hình này hiệu quả cho blog, tài liệu hay các trang web tĩnh.
Tuy nhiên, SSG cũng gặp giới hạn:
  • Khó khăn khi nội dung thay đổi thường xuyên: Việc cập nhật nội dung cần build lại, không phù hợp với dữ liệu động.
  • Dữ liệu quá lớn: Thời gian build có thể kéo dài, thậm chí không khả thi với hàng ngàn trang động.
ISR (Incremental Static Regeneration) là một nỗ lực để cải thiện SSG, nhưng một hướng đi táo bạo hơn đang dần hình thành…

Island Architecture: "Chỉ Hydrate Phần Cần Thiết”

Astro tiên phong với một triết lý mới: toàn bộ trang vẫn là HTML tĩnh, nhưng chỉ một số components tương tác mới được hydrate khi cần thiết. Ý tưởng này giúp giảm đáng kể lượng JavaScript phải gửi về client.
Ưu điểm:
  • Giảm JS tải về: Giúp trang web nhanh hơn đáng kể, đặc biệt trên các thiết bị yếu.
  • Vẫn giữ được tương tác: Nhờ khả năng hydrate từng component riêng lẻ.
Astro không phải là người đầu tiên nghĩ ra mô hình này, nhưng là người đưa nó lên thành một kiến trúc chính thức. Sau này, Nuxt cũng giới thiệu NuxtIsland theo cùng hướng tiếp cận.
Tuy nhiên, hydration – dù được phân mảnh – vẫn hiện diện, và đó chính là giới hạn mà Qwik muốn vượt qua.

Qwik: "Resumability" Thay Thế Hoàn Toàn "Hydration”

Qwik không chỉ tối ưu hydration – mà loại bỏ hoàn toàn khái niệm hydration bằng cách giới thiệu Resumability.
Thay vì tải và khởi chạy toàn bộ JavaScript để "kích hoạt" ứng dụng sau khi hiển thị (hydrate), Qwik chỉ "resume" đúng phần mà người dùng đang tương tác. Điều này có nghĩa là:
  • Không cần chạy JS ban đầu: Thời gian tương tác (TTI) gần như tức thì.
  • JS được tải dần theo hành vi: Tải cực kỳ nhẹ, mang lại cảm giác mượt mà như ứng dụng native.
Điều thú vị là: Qwik vẫn sử dụng JSX/TSX quen thuộc như React, nhưng ẩn sâu bên trong là một kiến trúc compiler-driven cực kỳ mạnh mẽ. Kiến trúc này giúp chia nhỏ mọi phần, tuần tự hóa trạng thái (serialize state), tiền tải (preload) thông minh, và render hoàn toàn trên server mà không cần JavaScript ban đầu.

Những gì chúng tôi đã làm – và vì sao vẫn chưa đủ?

Trước tiên, chúng ta cần nhìn lại một chút các metrics của Lighthouse
notion image
Chúng ta dễ dàng nhận thấy LCP (Largest Contentful Paint), FID/INP (First Input Delay/Interaction to Next Paint), và CLS (Cumulative Layout Shift) là ba chỉ số Core Web Vitals chính. Trong khi đó, TTFB (Time to First Byte) là một yếu tố quan trọng ảnh hưởng đến các chỉ số này.
Ban đầu, tôi từng nghĩ rằng Google đưa ra các metrics này dường như để "đánh đố", rất khó để đạt được điểm cao. Nhưng sau khi suy nghĩ kỹ lại, tôi nhận ra chúng thực sự ảnh hưởng lớn đến trải nghiệm người dùng (UX). Thật khó chịu nếu chúng ta truy cập một trang web mà phải chờ đợi mãi không xong, giao diện bị vỡ, hoặc bố cục nhảy lung tung. Cùng với đó là hình ảnh chính (LCP) mãi không hiển thị hoặc mất nhiều thời gian để tải xong.
Từ việc phân tích các metrics quan trọng này, chúng tôi nhận thấy cần phải thực hiện các giải pháp sau: Improve Response Time, Code Splitting, Lazy-loading, Preload Priority, Lazy-loading 3rd party, và Island Component.
Bài toán của chúng tôi phức tạp hơn một chút vì chúng tôi xây dựng một nền tảng (platform). Nền tảng này có vài trăm components khác nhau và tích hợp rất nhiều 3rd party do đặc thù của ngành thương mại điện tử. Hơn nữa, toàn bộ nội dung hiển thị trên từng trang đều do người dùng tự quyết định kéo thả các components. Điều này có nghĩa là chúng tôi không thể biết trước chính xác những components nào sẽ xuất hiện trên một trang cụ thể. Thử thách này lớn hơn nhiều so với việc tối ưu một trang web hay landing page thông thường vốn đã biết trước nội dung.

Tối ưu response time

Dễ dàng nhận thấy, response time ảnh hưởng rất lớn tới FCP (First Contentful Paint) và LCP. Nếu thời gian phản hồi của server quá lớn, rất khó để các metrics này đạt được điểm cao. Ngay cả người dùng cũng sẽ mất kiên nhẫn nếu một trang web tải mãi không xong.
Có thể tóm gọn lại, chúng tôi đã:
  • Full Page Cache: Tỷ lệ hit cache của các yêu cầu trên hệ thống của chúng tôi khá lớn. Do đó, việc triển khai full page cache đã giúp giảm đáng kể thời gian phản hồi, đồng thời giảm tải tài nguyên cho server vì không phải render lại nhiều.
  • Monitor: Chúng tôi theo dõi response time của các yêu cầu trên các công cụ như New Relic. Mục đích của việc này là để nhanh chóng phát hiện ra các yêu cầu có vấn đề, hoặc những bất thường xảy ra mỗi khi chúng tôi phát hành một phiên bản mới.
  • Improve API: Từ các chỉ số thu được trên các công cụ giám sát, các API có response time lớn đều được xử lý tối ưu để giảm thời gian phản hồi chung cho toàn bộ yêu cầu.

Preload priority resource

<!-- Preload critical images --> <link rel="preload" as="image" href="/hero-image.webp"> <link rel="preload" as="font" href="/font.woff2" crossorigin> <!-- Preload critical CSS --> <link rel="preload" as="style" href="/critical.css">
 
Khi truy cập bất kỳ trang thương mại điện tử nào, chúng ta thường thấy ngay các thành phần như slider, hình ảnh sản phẩm, hoặc bộ sưu tập. Việc "báo" cho trình duyệt biết cần tải gì trước là cực kỳ quan trọng. Khi các tài nguyên thiết yếu như hình ảnh được tải sớm, người dùng sẽ cảm thấy ít phải chờ đợi hơn rất nhiều, cải thiện đáng kể trải nghiệm ban đầu.
Tuy nhiên, chúng ta cũng cần xác định chính xác những gì thực sự cần tải sớm. Không phải mọi thứ đều nên được preload; nếu không, chính những thứ quan trọng lại không được ưu tiên đúng mức.
Vậy, với đặc thù của một nền tảng mà nội dung trang không thể biết trước (vì người dùng tự quyết định các component), làm thế nào chúng tôi xử lý vấn đề này?
  • Xác định Component đầu tiên: Dựa trên dữ liệu, chúng tôi xác định rõ component nào sẽ nằm ở vị trí đầu tiên trong nội dung của trang.
  • Xác định tài nguyên cần Preload: Sau đó, chúng tôi xác định những component nào có tài nguyên cần preload (ví dụ: hình ảnh chính, video thumbnail...). Các tài nguyên này được đưa vào props của component, và khi component đó được render, quá trình preload sẽ được kích hoạt.

Inline critical CSS

<style> /* Critical CSS được inline trực tiếp vào HTML */ .hero { background: url('/hero.webp'); } .nav { position: fixed; top: 0; } </style>
Một bài toán khác chúng tôi phải đối mặt do có rất nhiều components là kích thước file CSS quá lớn. Điều này chắc chắn sẽ ảnh hưởng tiêu cực đến điểm CLS (Cumulative Layout Shift), bởi lẽ khi trang render, CSS có thể chưa được tải xong, dẫn đến việc các phần tử bị nhảy layout.
Thêm vào đó, chúng tôi không thể tối ưu hóa từ build time một cách triệt để, vì không thể biết chính xác người dùng sẽ sử dụng những components nào để giảm dung lượng file CSS của các component không cần thiết.
Do việc tối ưu không thể hoàn toàn xử lý từ build time, chúng tôi đã kết hợp cả chiến lược build time và run time:
  • Build-time: Chúng tôi đã phát triển một plugin cho Vite để tự động xác định mỗi component cần sử dụng CSS nào. Từ đó, chúng tôi có thể xây dựng một bản đồ (map) chi tiết giữa component và CSS tương ứng mà nó cần. Đồng thời, các CSS dùng chung cho layout tổng thể cũng được đóng gói riêng.
  • Run-time: Dựa vào dữ liệu và trạng thái của trang, hệ thống sẽ xác định được layout và các components đang được sử dụng. Từ thông tin này, chúng tôi có thể tự động xây dựng critical CSS (CSS tối thiểu cần thiết để hiển thị phần quan trọng nhất của trang) cho từng trang cụ thể ngay tại thời điểm chạy.

Code Splitting

// Route-based splitting const ProductPage = lazy(() => import('./ProductPage.vue')); // Condition-based splitting const PaymentModal = lazy(() => import('./PaymentModal.vue').then(module => ({ default: module.PaymentModal })) );
Code Splitting có lẽ không còn là kỹ thuật xa lạ – đây là một trong những từ khóa phổ biến nhất khi nói về tối ưu hiệu năng web. Về bản chất, chúng ta cần chia nhỏ các phần mã nguồn – từ từng component riêng lẻ cho đến các module JavaScript lớn – và chỉ tải chúng khi thật sự cần thiết. Mục tiêu cốt lõi của kỹ thuật này là giảm dung lượng tải ban đầu để tăng tốc độ tải trang và cải thiện trải nghiệm người dùng một cách đáng kể.
Thông thường, các công cụ đóng gói module hiện đại như Webpack hay Vite đã hỗ trợ sẵn các hình thức code splitting hiệu quả thông qua:
  • Route-based splitting (Chia theo Route/Trang): Kỹ thuật này chia nhỏ mã nguồn theo từng trang hoặc route riêng biệt. Khi người dùng truy cập một trang cụ thể, chỉ mã nguồn cần thiết cho trang đó mới được tải về, giúp giảm đáng kể lượng JavaScript ban đầu.
  • Condition-based splitting (Chia theo Điều kiện): Sử dụng dynamic import (import()) để chỉ tải các module hoặc component khi một điều kiện nhất định xảy ra (ví dụ: khi người dùng nhấp vào nút mở modal, chuyển tab, hoặc cuộn đến một phần tử cụ thể). Điều này đảm bảo rằng các đoạn mã không cần thiết cho tương tác ban đầu sẽ không làm chậm quá trình tải trang.

Rewrite library

Sử dụng lại các thư viện mã nguồn mở là lựa chọn hợp lý trong phần lớn các trường hợp. Tuy nhiên, đến một ngưỡng nào đó về hiệu năng, kích thước bundle, hoặc tính đặc thù của dự án, chúng tôi nhận ra rằng việc "gò" hệ thống vào khuôn khổ của các thư viện bên ngoài đôi khi lại trở thành nút thắt, kìm hãm mọi nỗ lực tối ưu.
Vấn đề không nằm ở chất lượng của các thư viện — phần lớn đều được viết rất tốt và đang phục vụ hàng triệu người dùng trên toàn thế giới. Vấn đề thực sự nằm ở chỗ: chúng được thiết kế để phục vụ mọi nhu cầu, trong khi chúng tôi chỉ cần một phần rất nhỏ và cực kỳ cụ thể.
Ví dụ điển hình là vue-i18n. Đây là một thư viện mạnh mẽ, hỗ trợ rất nhiều tính năng như chuyển đổi ngôn ngữ động, lazy load, số nhiều, định dạng ngày giờ... Nhưng trên thực tế, hệ thống của chúng tôi chỉ cần dịch văn bản tĩnh, được xác định sẵn theo locale của người dùng ở thời điểm tải trang ban đầu. Vậy thì không có lý do gì để chúng tôi phải bundle cả một runtime lớn, bộ phân tích phức tạp, và cơ chế phản ứng (reactive mechanism) mà chúng tôi không hề dùng đến. Việc viết lại một giải pháp i18n siêu gọn nhẹ, chỉ bao gồm việc ánh xạ key ở compile-time và chèn trực tiếp JSON vào template, đã giúp chúng tôi giảm hàng chục KB JavaScript và cải thiện rõ rệt TTFB. Đó chính là lý do vue-i18n-lite ra đời.
Validation cũng là một ví dụ tương tự. Những thư viện như vee-validate hay vuelidate cung cấp một hệ sinh thái đầy đủ, với ngôn ngữ định nghĩa schema (schema DSL), cơ chế injection ngữ cảnh, và quy tắc phản ứng. Nhưng điều đó cũng đi kèm với một runtime lớn và sự trừu tượng hóa nặng nề. Trong khi đó, thứ chúng tôi cần chỉ đơn giản là: vài hàm nhận giá trị và trả về boolean hoặc string, dễ kiểm thử, dễ gỡ lỗi, và có thể chạy đồng bộ hoặc bất đồng bộ tùy ý. Từ nhu cầu đó, chúng tôi đã phát triển vue-tiny-validate — một thư viện chỉ nặng khoảng ~1.4KB (gzip), tương thích cả Vue 2 và Vue 3, giữ nguyên triết lý đơn giản nhưng vẫn đủ linh hoạt cho mọi biểu mẫu thực tế.
Viết lại một thư viện là một quyết định không hề nhẹ nhàng. Nhưng trong bối cảnh tối ưu hóa hiệu suất là mục tiêu hàng đầu, đây trở thành lựa chọn bắt buộc để đảm bảo rằng mỗi dòng JavaScript được tải về thực sự là thứ chúng tôi cần, không hơn.

Island Component

// Island component // ~/components/islands/ProductReviews.vue <template> <div> <div v-for="review in reviews" :key="review.id"> {{ review.content }} </div> </div> </template> <script setup> // Component sẽ được render như island const reviews = ref([]); onMounted(async () => { reviews.value = await $fetch('/api/reviews'); }); </script>
Sau khi tối ưu từng thư viện và từng byte JavaScript, chúng tôi nhận ra rằng phần lớn thời gian phản hồi vẫn đến từ việc phải render các component động — dù đôi khi chúng chỉ chiếm một phần rất nhỏ trong tổng thể trang.
Chúng tôi đã tiếp cận theo hướng Island Architecture, tương tự như Nuxt. Trong kiến trúc này, mỗi component tương tác được xem như một "island" độc lập, chỉ được hydrate khi thực sự cần thiết. Trong khi đó, nhiều nội dung khác trên trang vẫn được render tĩnh.
Tuy nhiên, khác với Nuxt, nơi HTML cho mỗi island thường được render ở runtime rồi mới chèn vào DOM, chúng tôi đã cải tiến bằng cách render sẵn toàn bộ HTML của các island ngay từ build-time. Kết quả này được tuần tự hóa (serialize) thành JSON và nhúng trực tiếp vào response. Khi trình duyệt nhận được, nó chỉ cần đọc JSON và gán HTML vào đúng vị trí, không cần render lại từ server hay client ở runtime.
Cách làm này mang lại nhiều lợi ích đáng kể:
  • Giảm đáng kể response time: Server không mất thời gian render runtime cho từng yêu cầu.
  • Tiết kiệm chi phí server: Chuyển hầu hết công việc tính toán sang giai đoạn build time.
  • Giảm thiểu độ trễ hydrate: HTML tĩnh đã có sẵn, client chỉ cần gắn logic tương tác.
Để triển khai cơ chế này, chúng tôi đã tự viết một plugin riêng cho Vite. Plugin này có nhiệm vụ phân tích cây component, xác định chính xác component nào cần hydrate, đồng thời render sẵn HTML tương ứng cho từng island trong giai đoạn build. Từ đó, output JSON được tối ưu hóa và nhúng thẳng vào HTML.
Nhờ vậy, kiến trúc island của chúng tôi không chỉ giữ được ưu điểm về trải nghiệm tương tác của Vue, mà còn đạt hiệu suất tương đương các static site generator hiện đại, thậm chí tốt hơn trong một số trường hợp đặc thù của dự án.
Mỗi cải tiến đều mang lại những kết quả đáng khích lệ: tốc độ tải trang nhanh hơn, người dùng hài lòng hơn. Nhưng sâu thẳm, chúng tôi vẫn cảm nhận được một "trần nhà vô hình" – giới hạn của việc tối ưu dựa trên các kiến trúc rendering hiện tại. Nơi mà gánh nặng của JavaScript và quá trình "hydration" vẫn là một thách thức dai dẳng, đặc biệt với một nền tảng thương mại điện tử có hàng trăm components động và vô số script từ bên thứ ba. Chúng tôi đã tự hỏi: Liệu có cách nào để không chỉ tối ưu từng phần, mà là thay đổi căn bản cuộc chơi? Liệu có một "vũ khí tối thượng" nào thực sự giúp chúng ta chạm đến cái "đỉnh" Lighthouse mà không phải đánh đổi quá nhiều?
Và rồi, giữa những trăn trở đó, một cái tên, một triết lý mới đã thực sự thu hút sự chú ý của chúng tôi, hứa hẹn một cuộc cách mạng thực sự…

Vượt Qua Giới Hạn Của Hydration: Khám Phá Sức Mạnh Thay Đổi Cuộc Chơi Của Qwik

Chúng ta đã dành nhiều công sức để "vá" và "tối ưu" những gì đang có. Nhưng nếu vấn đề cốt lõi nằm ở chính nền móng – ở cách mà các framework hiện đại "thổi hồn" sự tương tác vào trang web thông qua hydration – thì sao? Hydration, dù đã được cải tiến với Island Architecture, vẫn yêu cầu client phải tải về, phân tích và thực thi một lượng JavaScript nhất định để "đánh thức" các component sau khi HTML từ server hiển thị. Đây chính là lúc Time To Interactive (TTI) thường bị kéo dài, và là nơi những nỗ lực tối ưu của chúng ta dường như chỉ giải quyết được phần ngọn.
Vậy, sẽ ra sao nếu chúng ta có thể loại bỏ hoàn toàn bước hydration đó? Sẽ ra sao nếu ứng dụng có thể "tiếp tục" (resume) trạng thái và sự tương tác của nó ngay trên trình duyệt mà không cần phải thực thi lại logic của component từ đầu?
Đây không còn là giả thuyết. Đây chính là lời hứa mà Qwik mang đến, với khái niệm "Resumability" làm trọng tâm. Nó không chỉ là một cải tiến nhỏ giọt, mà là một sự thay đổi mang tính kiến trúc, một cú "twist" thực sự trong cách chúng ta nghĩ về hiệu năng web.

Sự khác biệt cốt lõi

Tính năng
Hydration (SSR truyền thống với React, Vue, Angular...)
Resumability (Qwik)
JavaScript ban đầu
Cần tải và thực thi code của các component đã render để "hydrate" chúng, gắn event listeners, và khôi phục state.
Gần như bằng không. Chỉ tải JavaScript khi người dùng tương tác với một phần cụ thể của trang.
Thời gian tương tác (TTI)
Bị trì hoãn cho đến khi quá trình hydration hoàn tất. Điều này có thể mất vài trăm mili giây đến vài giây, tùy thuộc vào độ phức tạp của ứng dụng.
Gần như tức thì. Vì không có hydration, trang có thể tương tác ngay khi HTML được render xong và một đoạn JavaScript siêu nhỏ (Qwikloader) được tải.
Công việc của Client
Phải làm lại rất nhiều việc mà server đã làm (hiểu cấu trúc component, khôi phục state).
Chỉ cần "tiếp tục" từ trạng thái đã được server chuẩn bị sẵn.
Serialization
Thường chỉ serialize state cơ bản. Logic và event listeners phải được client tái tạo.
Serialize toàn diện: state, event listeners, và các "closures" cần thiết để thực thi khi có tương tác.

Serialization: Bí Mật Đằng Sau Resumability

Qwik đạt được khả năng resumability thông qua một kỹ thuật gọi là serialization (tuần tự hóa). Khác với các framework truyền thống như React hay Vue – nơi client phải tải toàn bộ JavaScript để "đánh thức" ứng dụng – Qwik lưu trữ trạng thái của ứng dụng (bao gồm state, event listeners, và closures) trên server. Khi server render trang, nó gửi HTML kèm theo một "bản đồ" dưới dạng JSON, chứa thông tin về cách các component hoạt động.
Trên client, một đoạn mã nhỏ gọi là Qwikloader sẽ đọc bản đồ này và "tiếp tục" (resume) ứng dụng từ trạng thái đã được chuẩn bị sẵn. Điều này mang lại các lợi ích sau:
  • Không cần tải lại toàn bộ logic: Chỉ tải JavaScript khi người dùng tương tác với một phần cụ thể của trang.
  • Tối thiểu hóa JavaScript ban đầu: Ứng dụng trở nên tương tác gần như ngay lập tức.
  • Hiệu suất vượt trội: Giảm thời gian chờ đợi so với quá trình hydration thông thường.
<!-- HTML được render từ server với serialized state --> <div q:id="0" q:host> <button q:id="1" onClick$="./chunk-abc123.js#handler_0" q:state='{"count": 0}' > Count: 0 </button> </div> <!-- Qwik loader (~1KB) --> <script> // Đọc serialized state và setup event listeners window.qwikLoader = { async handleClick(event) { const handler = await import('./chunk-abc123.js'); return handler.default(event); } } </script>
Qwik serialize không chỉ state mà còn cả event handlersclosures. Khi user click button, Qwik mới load JavaScript tương ứng.

So sánh thực tế: React vs Qwik

React (với Next.js SSR):
// pages/counter.tsx import { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0); return ( <div> <h1>Count: {count}</h1> <button onClick={() => setCount(c => c + 1)}> Increment </button> </div> ); } // Toàn bộ React runtime (~42KB) + component code phải load trước khi trang interactive
Qwik:
import { component$, useSignal } from '@builder.io/qwik'; export default component$(() => { const count = useSignal(0); return ( <div> <h1>Count: {count.value}</h1> <button onClick$={() => count.value++}> Increment </button> </div> ); });

Demo Thực Tế: Trang Checkout eCommerce với Qwik

Để minh chứng cho những gì Qwik đã làm, tôi đã quyết định làm demo một trang checkout thường thây của các platform eCommerce vẫn hay làm.
Tại sao tôi lại lựa chọn eCommerce và lại là checkout page?
  • Nó có rất nhiều các payment methods khác nhau. Ở đó tha hồ SDK để chúng ta “thách thức” Qwik xem điểm nó có cao như lời đồn không. Từ Stripe, Paypal, Google Pay
  • Nó cũng rất nhiều các tracking sdk từ Facebook, Tiktok, Pinterest… Mà nói về thế giới SDK của analytics thì có quá nhiều luôn. Xem Qwik nó giải quyết ra sao luôn.
notion image
Kết quả Lighthouse cho demo checkout page - 98/100 trên mobile
Với một page checkout với kha khá các sdk từ các 3rd party thì theo bạn đạt 98 điểm trên mobile có ổn không? Nếu là mình, thật sự rớt nước mắt vì có áp dụng bao nhiêu kỹ thuật ở trên đi nữa cũng rất khó để đạt đến điểm cao như thế này.
Bạn có thể tự kiểm tra tại: https://qwik-checkout.fly.dev/ Và chạy Lighthouse test để xác minh kết quả.

Hạn chế và Trade-offs của Qwik

Dù Qwik mang lại nhiều cải tiến đột phá về hiệu năng — đặc biệt là ở Time to Interactive (TTI) và lượng JavaScript tải ban đầu — nhưng như bất kỳ công nghệ mới nào, Qwik cũng đi kèm những hạn chế và đánh đổi đáng lưu ý:
  • Yêu cầu làm quen với cú pháp và triết lý mới: Qwik đòi hỏi người dùng làm quen với khái niệm resumability, một mô hình hoàn toàn khác so với hydration truyền thống. Việc viết code theo cách "hoãn thực thi" (ví dụ: onClick$(), useStore(), useSignal()) có thể gây bỡ ngỡ, đặc biệt với những nhà phát triển đến từ các framework quen thuộc như React hoặc Vue.
  • Debug phức tạp hơn: Do logic được phân mảnh và trì hoãn đến runtime, việc debug trong môi trường phát triển hoặc kiểm tra hành vi khi lỗi xảy ra trên client có thể phức tạp hơn. Trong các framework truyền thống, toàn bộ logic đã được tải sẵn, giúp quá trình này dễ dàng hơn.
  • Hệ sinh thái tích hợp còn hạn chế: Qwik chưa có hệ sinh thái plugin và component phong phú như React hay Vue. Các công cụ bên ngoài như thư viện form, i18n, hoặc headless UI đôi khi phải được viết lại, hoặc tích hợp thủ công, đòi hỏi nhiều công sức hơn.
  • Tối ưu cho Edge/Serverless, nhưng không phù hợp mọi nơi: Qwik phát huy tối đa sức mạnh khi chạy trong môi trường Edge (như Vercel, Cloudflare Workers...). Trong môi trường server truyền thống hoặc các ứng dụng SPA không cần SEO, lợi thế của Qwik có thể giảm đi đáng kể.
  • Tuổi đời còn trẻ: Qwik mới được công bố chính thức từ năm 2022 và bản 1.0 chỉ vừa ổn định trong năm 2023. Điều này đồng nghĩa với việc:
    • Một số tính năng vẫn đang trong quá trình hoàn thiện.
    • Cộng đồng và tài nguyên học tập còn hạn chế.
    • Có ít case study sản phẩm lớn được triển khai thực tế, khiến việc dự đoán các vấn đề phát sinh ở quy mô lớn trở nên khó khăn hơn.

Khi Nào Nên Chọn Qwik?

✅ Phù hợp khi:

  • Website cần SEO tốt và tốc độ tải nhanh
  • Ứng dụng eCommerce, landing page, blog
  • Team sẵn sàng học công nghệ mới
  • Dự án mới hoặc có thể refactor

❌ Chưa phù hợp khi:

  • Cần tích hợp nhiều thư viện React/Vue có sẵn
  • Team thiếu thời gian training
  • Ứng dụng internal không cần tối ưu quá mức
  • Dự án đã stable với framework hiện tại

Kết luận

Sau nhiều năm vật lộn với bài toán tối ưu hiệu năng web — từ SSR, SSG cho đến các kiến trúc như Island Architecture — chúng tôi nhận ra vẫn luôn tồn tại một "trần kính" vô hình: đó chính là hydration. Dù đã cố gắng chia nhỏ bundle, lazy load components, hay thậm chí render từng phần UI theo yêu cầu, thì cuối cùng vẫn phải chấp nhận thực tế là: mọi thứ sẽ chỉ thực sự “hoạt động” sau khi trình duyệt tải và thực thi xong một lượng JavaScript không nhỏ.
Với các ứng dụng phức tạp như eCommerce, nơi mỗi mili giây đều ảnh hưởng đến tỷ lệ chuyển đổi, giới hạn đó trở thành nút thắt nghiêm trọng.
Qwik xuất hiện như một lối đi khác hẳn — không phải cải tiến từ mô hình cũ, mà là thay đổi tận gốc. Thay vì hydrate, Qwik resume. Thay vì tải mọi thứ từ đầu, Qwik khôi phục lại trạng thái từ server, và chỉ thực thi đúng phần cần thiết khi người dùng tương tác. Điều đó giúp Qwik đạt hiệu năng gần như không tưởng: gần như zero JS startup, nhưng vẫn duy trì được tính tương tác động như bất kỳ framework hiện đại nào.
Là một người đam mê công nghệ mới và không ngại "đặt cược" vào các giải pháp chưa phổ biến, tôi từng xây dựng sản phẩm thực tế trên Vue 3 khi nó còn chưa phát hành chính thức. Việc chọn Qwik cũng xuất phát từ tinh thần đó: không phải vì nó đã hoàn hảo, mà vì tôi tin rằng đây là hướng đi đúng — một bước nhảy đáng để thử cho bất kỳ ai thật sự nghiêm túc với hiệu năng.
Qwik không dành cho tất cả. Nhưng nếu bạn đang cảm thấy những công cụ cũ đã không còn đủ sức “bật” qua các chỉ số khó tính của Lighthouse, hoặc đơn giản là bạn cũng như tôi — muốn thử bước một bước trước phần còn lại — thì hãy thử Qwik.
Hãy thử áp dụng nó, trải nghiệm sự khác biệt, và cùng cộng đồng khám phá tiềm năng mà framework này mang lại.