Getting Started
This document takes you through creating a To-do application as popularized by https://todomvc.com/. This guide assumes that you have prior knowledge of other frameworks and JSX.
Prerequisite
node.js
v14 or higher (withnpm
)- your favorite IDE
Creating an app
The first step is to create an application. Qwik comes with a CLI that allows you to create a basic working skeleton of an application. We will use the CLI to create a Todo sample app, and we will use that application to do a walk-through of Qwik so that you can familiarize yourself with it.
- Ask Qwik CLI to create a project:
$ npm create qwik@latest
- Create an application (
qwik-todo
)
💫 Let's create a Qwik project 💫
? Project name › qwik-todo
- Select
Todo
starter project:
? Select a starter › - Use arrow-keys. Return to submit.
Starter - Blank Qwik starter app.
Starter Builder
Starter Partytown
❯ Todo
- Qwik is a framework that starts on the server and then moves to the browser. Select serving technology:
? Select a server › - Use arrow-keys. Return to submit.
❯ Express - Express.js server.
Cloudflare
Setup later
The resulting output should look like this:
💫 Let's create a Qwik project 💫
✔ Project name … qwik-todo
✔ Select a starter › Todo
✔ Select a server › Express
⭐️ Success! Project saved in qwik-todo directory
📟 Next steps:
cd qwik-todo
npm install
npm start
At this point, you will have qwik-todo
directory, which contains the starter app.
Running in client mode (development)
The easiest way to get running application is to follow the steps from the npm create qwik@latest
:
- Change into the directory created by the
npm create qwik@latest
.
cd qwik-todo
- Install NPM modules:
npm install
- Invoke the server
npm start
- You should see a server running with your To-do application
vite v2.8.6 dev server running at:
> Local: http://localhost:3000/
> Network: use `--host` to expose
ready in 157ms.
- Visit http://localhost:3000/ to explore the To-do app.
The application is running in development mode using Vite. This is a special mode that supports Hot-Module-Reloading (HMR.)
While HMR is great for development, Qwik runs like a traditional framework, where all of the work is done in the browser. If you look into the network tab of the dev-tools, you will see that all of the code is eagerly downloaded into the browser and executed. To understand how Qwik is different, we need to run in production mode to see the magic happen.
Running in production mode
Qwik is SSR/SSG framework that can 1) start execution in node.js
2) serialize the application state into HTML and 3) resume the application from HTML in the browser. This section is a tour of those capabilities.
- Build the application:
$ npm run build
- Results in output similar to this:
> qwik-todo@0.0.0 build /Users/misko/qwik-todo
> npm run typecheck && npm run build.client && npm run build.server
> qwik-todo@0.0.0 typecheck /Users/misko/qwik-todo
> tsc --noEmit
> qwik-todo@0.0.0 build.client /Users/misko/qwik-todo
> vite build --outDir server/public
vite v2.8.6 building for production...
✓ 34 modules transformed.
server/public/index.html 0.37 KiB
server/public/assets/main.81a5c326.js 0.20 KiB / gzip: 0.15 KiB
server/public/assets/index.9d3fa03a.js 0.25 KiB / gzip: 0.18 KiB
server/public/q-b5deaed3.js 0.67 KiB / gzip: 0.42 KiB
server/public/q-cc9047c3.js 5.08 KiB / gzip: 1.47 KiB
server/public/q-3a2b0629.css 6.65 KiB / gzip: 2.07 KiB
server/public/q-59136a66.js 39.29 KiB / gzip: 13.07 KiB
> qwik-todo@0.0.0 build.server /Users/misko/qwik-todo
> vite build --outDir server/build --ssr src/entry.express.tsx
vite v2.8.6 building SSR bundle for production...
✓ 30 modules transformed.
server/build/entry.express.js 13.23 KiB
There are three parts to the build:
- TypeScript compilation which is performed with
tsc
- Bundling for the client
- Bundling for the server
Because Qwik applications start their execution on the server and then resume on the client, it is necessary to bundle the application twice. There are a few reasons for this:
- Browsers want ES modules, whereas
node.js
runs better withcommonJS
. - Browsers need to take advantage of lazy loading, and therefore browsers need many small files. Servers are long-running, so lazy loading does not have benefits.
- Server code may execute different functions which allow the server to make direct connections to databases etc..
For these reasons, the bundling step is performed twice.
After a successful build, the application can be served as it would be served in production:
$ npm run serve
Output:
> qwik-todo@0.0.0 serve /Users/misko/qwik-todo
> node server/build/entry.express.js
http://localhost:8080/
We can now see the application running by visiting http://localhost:8080/.
Tour of the production
At this point, it is important to take a tour of the To-do application to understand the main differences with the current generation of frameworks. Understanding the differences will give you a better insight into many of the technical decisions of the framework.
- First, visit http://localhost:8080/ to familiarize yourself with the To-do application and convince yourself that the application is working as expected.
NOTE: for the next steps, it is recommended that you open the application in an incognito window, as many browser extensions inject code into sites which may make it look like Javascript is being downloaded.
-
Open the networking tab in DevTools in your browser and notice that the application did not download any JavaScript to startup. And yet the application is fully interactive. We call this property resumability, and it is the main feature of Qwik that allows even the most complex applications to start up instantaneously.
-
The question to answer is how is it possible for the application to be interactive with no Javascript. The answer is that Qwik applications come with a small Qwikloader. The Qwikloader is responsible for setting up global event listeners for the application and then downloading the application code on user interaction. The Qwikloader is very small, less than 1kB, and therefore is inlined directly into
index.html
to save round-trip cost. -
The Qwikloader is responsible for setting up global event listeners to make the application interactive. Open the DevTools Performance tab and profile the application. What you should see is that the Qwikloader should execute in about 10 ms. (Note: this can be further reduced by explicitly limiting the events to listen to.)
To resume a Qwik application, it takes less than 1kB of Javascript, which then executes on the client in about 10 ms. The thing to understand is that this code is in no way specific to the To-do application. The cost described above is fixed no matter the size or complexity of the application. Current generation frameworks must hydrate the application on the client to make it interactive. This requires downloading the framework and the application. The hydration cost is proportional to the size and complexity of the application. So it may start out small, but as the application grows, so will the hydration cost. With Qwik, the startup cost 1) is significantly smaller and 2) is fixed no matter the complexity of the application.
Understanding User Interactions
So far, we have shown how to run the Qwik application and how little code is needed to make the application interactive. Now let's look into what happens upon interaction.
- Startup the application:
$ npm run dev.ssr
This results in:
vite v2.8.6 dev server running at:
> Local: http://localhost:3000/
> Network: use `--host` to expose
ready in 471ms.
Open http://localhost:3000 and open the browser DevTools.
NOTE: Opening up the network tab will add client
and env.mjs
files. These are from Vite and are used for HMR. They will not be present in the production build and are ignored for this discussion.
With the network tab open, complete a to-do item by clicking on its checkmark button.
Notice how interacting with the application caused the network tab to download the necessary code to process the user input.
Now refresh the page to clear the network tab and enter text into the To-do app input box.
Notice how entering the text caused the network tab to download the necessary code that is different from the previous interaction. Depending on how the user interacts with the application, different code gets downloaded.
Now create a new item by hitting Enter
key in the input box. Notice that even more code now downloads.
There are a few things to notice:
- The code downloaded depends on how the user interacts with the application. Click on completion will download completion code; entering new text will download code associated with new to-do item creation.
- The initial files then download other code, including the code needed to re-render the component. (See prefetching on the discussion of how to avoid waterfall requests.)
- Only the code needed to re-render the specific component is downloaded.
- As you further interact with the application, more code gets downloaded on an as-needed basis. Notice that Qwik delays the loading of code for as long as possible.
QRLs
You may want to know how Qwik knows which code to download based on the user interaction. You can explore this by examining the DOM in the DevTools. Let's zoom in on the checkmark HTML. You should see something like this:
<input q:obj="9"
type="checkbox"
onClickQrl="/src/components/item/h_item_item_onrender_input_onclick.js#Item_onRender_input_onclick[0]"
class="toggle">
There are a few things to notice:
q:obj
is used when deserializing objects associated with your application. The attribute is a pointer to serialized JSON that contains the relevant data.onClickQrl
: is a serialized version of the event handler. It describes:click
: Qwikloader should listen on theclick
event...../h_item_item_onrender_input_onclick.js
: points to the URL that needs to be downloaded due to the click.Item_onRender_input_onclick
: points to the symbol whichh_item_item_onrender_input_onclick.js
exports which represents the handler for the click listener. (See Optimizer to understand how Qwik breaks up your application into lazy-loadable chunks.)[0]
: Index intoq:obj
used to restore the lexically scoped variables for the event handler.
NOTE: q:obj
and [0]
are implementation details of Qwik and can change at any time. They are described here for illustrative purposes only to explain how Qwik discovers which code it should download and which state it should restore to process the event handlers. Please don't rely on them in your application.
Because Qwik SSR serializes the event handlers into HTML, the Qwik application does not need to perform hydration on application start up. All of the necessary information to process the events is present in HTML. (This is what we is meant by Qwik applications being resumable, they can resume execution where the server left off without downloading any code for hydration.)
Understanding SSR/SSG
Server
Server-side rendering (SSR) and static-site generation (SSG) are ways of precomputing the HTML for faster site loading. Existing frameworks require that for a site to become interactive, it must undergo hydration. Qwik is unique in that it can serialize the event handlers, application state, and framework state information into HTML. The serialized information can then be used to skip hydration.
Let's dive into how Qwik generates HTML either in SSR or SSG.
- The entry point for SSR/SSG is
src/entry.server.tsx
. It exportsrender
function which invokesrenderToString()
function.
export function render(opts: RenderToStringOptions) {
return renderToString(
<html>
<head>
<meta charSet="utf-8" />
<title>Qwik Demo: ToDo</title>
</head>
<body q:base="/">
<Main />
<QwikLoader debug={opts.debug} />
</body>
</html>,
opts
);
}
- Notice that the rendering starts with the
<html>
tag. This is quite different from most frameworks which usually load the application into the existingindex.html
. The reason for this is that Qwik needs to inject<QwikLoader>
(and other components not shown here.) - The application is included as
<Main/>
tag that transitively includes other components which need are rendered. - The result of
renderToString
is aPromise<string>
. Qwik rendering pipeline is asynchronous, and it understands how to wait on components until they finish rendering for the purposes of SSR/SSG and before they are serialized.
Resuming in Browser
While the server has a clear entry point for your application, no such entry point exists for the Client/Browser. At first, this may seem surprising, but it is a natural consequence of resumability. There are as many entry points as there are serialized event handlers in the HTML. Any one of the event handlers may be invoked first, and hence different code will be downloaded that in turn will be responsible for bootstrapping the application and framework.
Understanding Serialization and Resumability
For the application to be resumable, the framework needs to know everything about the application without downloading any application code first. In practice, this means that HTML must contain:
- Event Handlers: Information on the location of all event handlers. This includes event name, code to download, symbol to retrieve, and application state to restore for the event handler.
- Application state: State which your application needs to function.
- Subscriptions: Qwik is reactive, so it needs to know which components hold subscriptions to which application state. This is necessary so that when the event handler modifies the application state, Qwik can re-render only the affected component.
- Component Hierarchy: Information where individual components start/end in the HTML as well as any projected children.
- Component Rendering: Information showing where rendering functions for each component can be downloaded.
The above information is split into two parts:
- Structural and event handler information is encoded in HTML directly in form of attributes or element names. (
<q:slot>
or<div q:slot="...">
,<button onClickQrl="./chunk.js#symbol">
) - Application and framework state as well as QRLs to component render functions is encoded in JSON format in
<script type="qwik/json">...</script>
Together the information allows Qwik to understand and reason about the application without downloading and executing any Javascript. This information removes the need for the framework to have hydration and thus makes Qwik resumable.
Writing a component
Let's look at how components are written in Qwik. Open src/main.tsx
to see the top level component.
export const Main = component$(() => {
const todos = useStore<Todos>({
filter: 'all',
items: [
{ completed: false, title: 'Read Qwik docs' },
{ completed: false, title: 'Build HelloWorld' },
{ completed: false, title: 'Profit' },
],
});
return (
<section class="todoapp">
<Header todos={todos} />
<Body todos={todos} />
<Footer todos={todos} />
</section>
);
});
Components are declared using component$()
function. A component optionally creates a state and returns an on-render function (return $(() => ...)
.
The unique thing to notice is the presence of the $
on component$()
as well as on-render function return $(() => ...)
. The presence of $
has a specific meaning:
- Developer: The developer needs to understand that
$
means that a lazy loading boundary exists in this location. In our case the component on-mount (or create) function which is responsible for creating thetodos
(useStore()
) will only execute on the server, as the<Main/>
component gets created on server and no new instances get created on the client. - Optimizer: The Optimizer uses the
$
to refactor the code and pull out the closures into top level importable symbols and place them in related chunks. The Optimizer leaves a pointer, known as QRL, to the refactored code in that location. - Runtime: The runtime uses the QRL to lazy load the symbol on an as-needed basis. Many symbols such as the on-mount and on-render will never download to the client because the
<Main>
component is static. No change totodos
would require a re-render.
Interactive component
Open src/components/header/header.tsx
to see an interactive component.
export const Header = component$(
(props: { todos: Todos }) => {
const state = useStore({ text: '' });
return (
<>
<h1>todos</h1>
<input
class="new-todo"
placeholder="What needs to be done?"
autoFocus
value={state.text}
onKeyup$={() => {
const event = useEvent<KeyboardEvent>();
const inputValue = (event.target as HTMLInputElement).value;
state.text = inputValue;
if (event.key === 'Enter' && inputValue) {
props.todos.items.push({
completed: false,
title: state.text,
});
state.text = '';
}
}}
/>
</>
);
},
{
tagName: 'header',
}
);
useStore()
export const Header = component$(
(props: { todos: Todos }) => {
const state = useStore({ text: '' });
return ( ... );
The useStore()
is used to create state of the application which is serializable. In our example above the store is created and initialized to its initial value. The creation happens on server during the initial rendering. The store is then serialized into HTML. In this component example, there is no need to re-run the creation code in the browser; for this reason on-render code will never be downloaded for this component.
Props
Notice that the component can declare props
, in this case {todos: Todos}
. Props allow the component to get data from parent components.
Event Listeners
Finally let's look at an event listener:
<input
...
value={state.text}
onKeyup$={() => {
const event = useEvent<KeyboardEvent>();
const inputValue = (event.target as HTMLInputElement).value;
state.text = inputValue;
if (event.key === 'Enter' && inputValue) {
props.todos.items.push({
completed: false,
title: state.text,
});
state.text = '';
}
}}
/>
The onKeyup$
tells the Optimizer to automatically extract the event listener into a lazy loadable chunk. This allows the Qwik runtime to delay the loading of the keyup
handler until the user interacts with the page. The keyup
handler modifies the store.text
and props.todos
. Both of these objects are stores and hence proxies. Any modification of the proxies automatically invalidates the subscribed on-render functions as described above. Qwik knows about the subscriptions because the subscriptions were serialized as part of the server render.
Configuring Optimizer
Open vite.config.ts
to see how the optimizer is configured. In this case the Optimizer is wrapped into qwikVite()
plugin for Vite.
** to be written **
Prefetching
** Not yet implemented **
Will allow Qwik to start prefetching symbols based on the likelihood that they will be needed in the browser. This removes any concerns over latency on interaction over slow networks.