TL;DR
- 🔥 We'll find out whether the trending framework Qwik is faster than Next.js
- 🤖 We'll look at a demo that generates with OpenAI's DALL-E and its performance benchmark.
- 🚀 We'll discuss how Next.js leverages React server component to achieve fast SSR.
This article is also available on
Feel free to read it on your favorite platform✨
Page speed matters. Faster website results in better UX, better SEO, and more profit. The latest research done by Rekuten 24 shows that optimizing Core Web Vitals leads to:
- 53.37% in revenue per visitor.
- 33.13% in conversion rate.
- 15.20% in average order value.
- 9.99% in average time spent.
- A 35.12% reduction in exit rate.
The modern frameworks and frontend libraries address speed and help developers ship better user experience to the users. We'll be discussing two of the modern frameworks: Qwik and Next.js. We'll see an experiment benchmark and look into how each framework achieves optimal performance.
Let's go.
How The Experiment Is Set Up
- qwik v0.16
- Next.js v13
- React v18
Let's Peek The Result
I built two identical applications with Qwik and Next.js and measured the performances. The demo look like this:
You can find the demo on GitHub. Feel free to take a look at the repo and try it out✨
The application lets users enter prompts to DALL·E, an AI image generator, and displays the generated AI images in the page. It also displays the latest Twitter feed about DALL·E.
The key features are:
- Server Side rendering the application.
- Client side fetching the image results with DALL·E's REST API.
- Server side fetching the Twitter feed with Twitter's REST API.
Here's the core Web Vitals by Lighthouse for Qwik:
Comparing to Next.js's Web Vitals:
We can observe the following:
- Qwik's speed index is 0.5 s faster than Next.js.
- Qwik's time to interactive is 0.3 s faster than Next.js.
- Next.js has a 0 ms total blocking time, comparing to Qwik's 160 ms.
- Next.js has a slightly higher overall performance score by 5 points.
- Next.js's largest contentful paint is 0.8 s faster than Qwik.
Let's take a closer look at the how the application in each framework is set up.
Qwik Server Side Rendering
The component structure:
export const onGet: RequestHandler<TwitterResponse> = async () => {
const data = await fetchTweets();
return data;
}
export default component$(() => {
const tweets = useEndpoint<TwitterResponse>();
return (
<Layout>
<DallePromptAndResult />
<Resource
value={tweets}
onResolved={(result) => (
<SSRTwitterCarousel data={result} />
)}
/>
<StaticPromptRecommendation />
</Layout>
)
})
When the Qwik server receives a page request, it starts the rendering process on the server. The "useEndPoint" function invokes the "onGet" function on the server and fetches the twitter feed. The "Resource" component will pause the rendering until the twitter data is resolved or rejected. Once the rendering is completed, the server responds to the client with rendered HTML.
We can observe the server side rendering behavior in the performance profile:
The lead time to respond to the client is blocked by the server side data fetching and component rendering.
Next.js SSR and Streaming
The component structure:
export default async function Page() {
const tweets = await fetchTweets();
return (
<Layout>
<DallePromptAndResult />
<Suspense fallback={<Skeleton />}>
<SSRTwitterCarousel data={tweets} />
</Suspense>
<StaticPromptRecommendation />
</Layout>
)
}
The "Page" component is a server component. It looks just like a normal component but it supports async/await.
When the Next.js server receives a page request, it starts the rendering process and streaming the rendering result to the client so that the client can progressively render and display the UI to the users. While the server is fetching twitter feeds, React renders the suspense placeholder to indicate the pending state. Once the data is resolved or rejected, React reveals the suspense boundary and displays the final UI.
You can see the client starts rendering while the server is streaming. It reduces the lead time for the users to start seeing the page.
Resumability v.s. React Server Component
Qwik embraces the "Get HTML and render" mental model.
Resumability is Qwik's innovation. It allows an application to be rendered as much as possible on the server and resumes the rest of the rendering on the client.
The framework looks like this:
When receiving a request, the Qwik server starts the rendering process and generate
- serialized application state,
- serialized event handlers,
- component HTMLs,
- and component code chunks.
The server then responds to the client with the page HTML and the serialized state.
On the client, the browser processes the critical rendering path and displays the UI. Qwik deserializes the state after the page HTML is loaded. The state contains all the local states of each component. The lazy loaded components are able to be dynamically imported independently without knowing the parent's state because they can refer to the deserialized state for their local states when they are rendering on the client.
Qwik also serializes event handlers.
In the server-generated HTML, the event handlers are referenced as dynamic JavaScript chunks. When users interact with an interactive element, Qwik uses the reference to dynamically download the chunk from the server and fire the event with the event handler inside the chunk.
On the other hand, Next.js has a different approach to server side rendering.
Next.js and React server component promotes the "Render while fetching" mental model.
Next.js 13 introduces React server component as an experimental feature. Server component is a special type of component that can only be rendered on the server. Combining with Suspense and Streaming, the framework is able to progressively render interactive UI while avoiding long data requests from blocking the page rendering.
The framework looks like this:
When Next.js server receives a request, it delegates React to handle rendering. On the server, React renders the component tree into a UI description in JSON instead of native HTML. The UI description describes the entire component tree. For the client components, React describes them as components for the client to handle with serialized props. For the server components, React renders them to native HTMLs and places them in the UI description.
On the client, React receives and reconciles the UI description in the response stream to progressively render the UI. When React sees a suspense boundary, it renders the suspense placeholder until the pending data is resolved and reveals the suspense boundary.
Because React receives a UI description instead of native HTML on the client, it needs to construct the component tree, render the UI, and attach event handlers on the client. It's known as hydration.
Final Thoughts
Qwik's concept of serializing states and event handlers is very innovative. The client is able to render the page with just HTML and minimal JavaScript. It reduces the amount of JavaScript the client needs to download for the initial load and leverages dynamic import to download event handlers and components on the fly. We can clearly observe the benefit from the benchmark and performance profile.
However, the combination of React server component, suspense, and streaming has a profound impact on user experience. Users are able to see and interact with the content of the page without waiting for server side data fetching to complete.
The result of the experiment is not conclusive to me. Both frameworks performed well in the benchmarks. The differences in First Content Paint, Largest Content Paint, and Cumulative Layout Shift are likely a result of different Twitter images.
References
- API Reference: Create image - OpenAI
- Case Study - web.dev
- Critical rendering path - MDN
- DALL·E 2 - OpenAI
- Data Fetching Fundamentals - Next.js
- First class support for promises - Andrew Clark
- How Rakuten 24's investment in Core Web Vitals increased revenue per visitor by 53.37% and conversion rate by 33.13% - web.dev
- Hydration is Pure Overhead - Builder.io
- Lighthouse performance scoring - Chrome Developers
- Lazy loading - MDN
- Next.js - Vercel
- Node.js library - OpenAI
- Populating the page: how browsers work - MDN
- Progressively - Builder.io
- qwik - Builder.io
- qwik-vs-next - Daw-Chih Liou
- RFC: React Server Components - React Community
- Resumable vs. Hydration - Builder.io
- Reactivity - Builder.io
- Server and Client Components - Next.js
- The "why" of web performance - MDN
- Think Qwik - Builder.io
- Time to Interfactive - Chrome Developers
- Total Blocking Time - Chrome Developers
- Twitter API v2 - Twitter Developer Aplatform
- Web Vitals - web.dev
- Speed Index - Chrome Developers
- Largest Contentful Paint - Chrome Developers
Here you have it! Thanks for reading through🙌 If you find this article useful, please share it to help more people in their engineering journey.
🐦 Feel free to connect with me on twitter!
⏭ Ready for the next article? 👉 Tired of Slow Code Reviews? Read this
Happy coding!