Misko Hevery, the creator of AngularJS, recently announced the beta availability of Qwik, his new web framework. Qwik claims to build applications that feel fast regardless of application size. In most cases, Qwik first downloads only 1 KB of JavaScript. Event handlers and application code are lazy-loaded and prefetched as needed.
In a talk addressing How to Remove 99% of JavaScript From the Main Thread, Hevery introduced the rationale behind Qwik as follows:
Qwik has a very very simple goal which is that it wants to make sure that no matter the complexity of your website, you should be able to get 100/100 on a Google page speed score. […] All of this thing comes down to basically making the fastest possible Time to Interactive. […]
What you see is that for the most part, as an industry, we are doing a fabulous job of optimizing our images, a fabulous job optimizing our CSS, but not so much on JavaScript. Because it is a systemic problem for everybody on the web, I am going to argue that the problem is the tooling, not the developers.
Tooling [that optimizes the delivery of JavaScript for speed] is the kind of problem Qwik focuses on.
Misko attributed the negative impact of JavaScript on the time-to-interactive metric to hydration. Hydration occurs in connection with server-side rendering. The client requests a page; the server makes the relevant queries, populates the page, and sends it back to the client. While server-side-rendered pages are usually displayed faster to users than client-side-rendered pages (e.g., faster First Contentful Paint), the pages are not immediately interactive. The server sent a static version of the page. The client then needs to download and execute the JavaScript responsible for providing the interactivity of the page.
In many frameworks, this process of reconciliation of the initially shipped HTML with the application’s JavaScript is called hydration. During hydration, the web application framework associates event handlers with DOM elements and initializes the state of the application. After hydration, user actions will be picked up by the event handlers: the application is now interactive.
Qwik eschews hydration by running the application on the server (server-side rendering as usual); serializing all relevant pieces of state; and sending to the client both page content and serialized state as HTML. The relevant state includes event listeners, internal data structures, and application state. With the serialized state, the client can just resume execution where the server left off.
Loading the JavaScript that handles an interaction is by default deferred, possibly until the interaction is actually initiated by the user. This means that the event handler for a button may be loaded (at the latest) when the user clicks on that button. This just-in-time JavaScript fetching is complemented with prefetching strategies that leverage the browser’s native ability to schedule the loading of documents without sacrificing the interactivity of the page.
The Qwik documentation details:
- Qwik only prefetches the code which is needed for the current page. Qwik avoids downloading code associated with components that are static. In the worst case, Qwik prefetches the same amount of code as the existing frameworks’ best case. In most cases, Qwik prefetches a small fraction of code compared to the existing frameworks.
- Prefetching of code can happen on other threads than the main thread. Many browsers can even pre-parse the AST of the code off the main thread.
- If the user interaction happens before the prefetch is completed, the browser will automatically prioritize the interaction chunk before the remaining prefetch chunks.
- Qwik can break up the application into many small chunks, and these chunks can be downloaded in the order of probability that the user will interact with them.
The Qwik website provides tutorials, examples, and a playground for developers to learn and experiment with Qwik. A simple counter application, consisting of a button and a message displaying how many times the button was clicked, is implemented as follows:
import { component$, useStore } from '@builder.io/qwik';
export const App = component$(() => {
const store = useStore({ count: 0 });
return (
<div>
<p>Count: {store.count}</p>
<p>
<button onClick$={() => store.count++}>Click</button>
</p>
</div>
);
});
Developers create resumable components with Qwik’s component$
API. Stateful components reveal their dependencies to pieces of state with the useStore
API. Developers create resumable event handlers by appending the $
character to the name of the handler (e.g., onclick$
in the example above). With these developer-provided hints, Qwik bundles application files in a way that enables and optimizes the lazy loading of JavaScript. The server-rendered HTML for the previous counter application is as follows:
<!DOCTYPE html>
<html
q:container="paused"
q:version="0.11.1"
q:render="ssr"
q:base="/repl/21kry8ac4hl/build/"
>
<html>
<head q:head>
<title q:head>Tutorial</title>
</head>
<body>
<!--qv q:id=0 q:key=AkbU84a8zes:-->
<div>
<p>
Count:
<!--t=1-->0<!---->
</p>
<p>
<button
on:click="app_component_div_p_button_onclick_8dwua0cjar4.js#App_component_div_p_button_onClick_8dWUa0cJAr4[0]"
q:id="2"
>
Click
</button>
</p>
</div>
<!--/qv-->
</body>
</html>
<script type="qwik/json">
{"ctx":{"#2":{"r":"0!"}},"objs":[{"count":"1"},0],"subs":[["2 #0 0 #1 data count"]]}
</script>
<script id="qwikloader">
((e,t)=>{const n="__q_context__",o=window,r=new Set,i=t=>e.querySelectorAll(t),s=(e,t,n=t.type)=>{i("[on"+e+"\\:"+n+"]").forEach((o=>l(o,e,t,n)))},a=(e,t)=>new CustomEvent(e,{detail:t}),c=(t,n)=>(t=t.closest("[q\\:container]"),new URL(n,new URL(t.getAttribute("q:base"),e.baseURI))),l=async(t,o,r,i=r.type)=>{const s="on"+o+":"+i;t.hasAttribute("preventdefault:"+i)&&r.preventDefault();const a=t._qc_,l=null==a?void 0:a.li.filter((e=>e[0]===s));if(l&&l.length>0){for(const e of l)await e[1].getFn([t,r],(()=>t.isConnected))(r,t);return}const d=t.getAttribute(s);if(d)for(const o of d.split("\n")){const i=c(t,o),s=b(i),a=performance.now(),l=u(await import(i.href.split("#")[0]),s),d=e[n];if(t.isConnected)try{e[n]=[t,r,i],f("qsymbol",{symbol:s,element:t,reqTime:a}),await l(r,t)}finally{e[n]=d}}},f=(t,n)=>{e.dispatchEvent(a(t,n))},u=(e,t)=>{if(t in e)return e[t];for(const n of Object.values(e))if("object"==typeof n&&n&&t in n)return n[t]},b=e=>e.hash.replace(/^#?([^?[|]*).*$/,"$1")||"default",d=e=>e.replace(/([A-Z])/g,(e=>"-"+e.toLowerCase())),p=async e=>{let t=d(e.type),n=e.target;for(s("-document",e,t);n&&n.getAttribute;)await l(n,"",e,t),n=e.bubbles&&!0!==e.cancelBubble?n.parentElement:null},v=e=>{s("-window",e,d(e.type))},w=()=>{var n;const s=e.readyState;if(!t&&("interactive"==s||"complete"==s)&&(t=1,f("qinit"),(null!=(n=o.requestIdleCallback)?n:o.setTimeout).bind(o)((()=>f("qidle"))),r.has("qvisible"))){const e=i("[on\\:qvisible]"),t=new IntersectionObserver((e=>{for(const n of e)n.isIntersecting&&(t.unobserve(n.target),l(n.target,"",a("qvisible",n)))}));e.forEach((e=>t.observe(e)))}},q=(e,t,n,o=!1)=>e.addEventListener(t,n,{capture:o}),y=t=>{for(const n of t)r.has(n)||(q(e,n,p,!0),q(o,n,v),r.add(n))};if(!e.qR){const t=o.qwikevents;Array.isArray(t)&&y(t),o.qwikevents={push:(...e)=>y(e)},q(e,"readystatechange",w),w()}})(document);
</script>
<script>
window.qwikevents.push("click")
</script>
</html>
Note how the HTML is enhanced with:
q:
attributes (e.g.,q:base
,q:id
,q:key
);- HTML comments (e.g.,
<!--qv q:id=0 q:key=AkbU84a8zes:-->
) with framework-specific information; - serialized state (cf.
<script type="qwik/json"> {"ctx": ..., "objs":[{"count":"1"},0], "subs":[["2 #0 0 #1 data count"]]} </script>
); - and Qwik scripts (e.g.,
<script id="qwikloader"> ... </script>
,window.qwikevents.push("click")
that resume the application on the client side.
Qwik’s playground allows developers to understand how the application code is split and bundled. In the case of the counter application code, the client bundle is as follows:
The previous screenshot shows that the counter application has been split into three scripts. When the user clicks on the button, two scripts are dynamically downloaded and executed (Qwik runtime and code for the click event handler):
To understand what exactly happens and how code splitting works, developers can refer to Qwik’s documentation. Qwik’s website provides plenty of information (tutorials, examples, presentations) and an interactive code playground). The community has also made available a non-trivial e-commerce example. E-commerce vendors generally assess that increasing page speed leads to increased revenue.
Qwik’s team includes Miško Hevery, creator of AngularJS; Manu Almeida, creator of the Go-based Gin web framework; and Adam Bradley, creator of the Stencil web component compiler; and the Ionic UI toolkit.
Qwik is now available in beta. Qwik is an open-source project under the MIT license. Contributions are welcome and should follow Qwik’s code of conduct.