10
Streaming
2026 Edition
In the previous chapter, you moved loaders to the right route boundary. But even when loaders are placed correctly, one slow request can still delay the whole page.
In this chapter, you'll learn how streaming helps keep the UI visible while slower data is still loading, and how to use routeLoader$(), useResource$(), and <Resource /> to improve perceived performance.
In this chapter...
Here are the topics we will cover:
π What is streaming?
Streaming is a rendering technique that lets part of the UI appear before all the data is ready.
Instead of blocking the whole page until every request finishes, you can render the parts that are already ready and defer the slower content.

This improves perceived performance because the user can see the page structure earlier instead of staring at a blank screen.
In Qwik, there are two useful ways to approach this:
Itβs time to take a quiz!
Test your knowledge and see what youβve just learned.
What is streaming?
π£οΈ Streaming with routeLoader$() and <Resource />
In the previous chapter, you added a 3-second delay to fetchRevenue() to simulate a slow request.
With a standard routeLoader$(), Qwik waits for the loader to finish before rendering the route.
With a standard routeLoader$(), the route waits for the slow request before rendering.
Qwik also supports a deferred version of routeLoader$().
You can read more about this pattern in the official Qwik guide on streaming/deferred loaders.
By returning an asynchronous function from the loader, you can render the DOM up to a <Resource /> boundary first, then wait for the deferred value to resolve.
This helps illustrate an important idea: where you place a <Resource /> boundary affects what can appear first.
Let's apply that pattern to the revenue chart.
In src/routes/dashboard/index.tsx, keep useFetchLatestInvoices and useFetchCardData as they are, and turn only useFetchRevenue into a deferred loader.
Then render <RevenueChart /> through a <Resource /> boundary:
In this version, only the revenue request is deferred.
useFetchRevenuestill usesrouteLoader$(), but now it returns an asynchronous function.That deferred value is passed to
<Resource />.Everything before that boundary can render first.
In this example, that means the heading and the cards can appear before the revenue chart resolves.
With this deferred routeLoader$() pattern, the effect is visible on a full page reload, but not during SPA navigation in our setup.
The UI before the <Resource /> boundary can render before the revenue data is ready.
This first approach is useful for understanding deferred loaders and resource boundaries.
In the next section, you'll switch to useResource$() to build clearer loading states.
Itβs time to take a quiz!
Test your knowledge and see what youβve just learned.
What does the deferred routeLoader$() change in this dashboard example?
π¦ Streaming with useResource$() and <Resource />
In this section, you'll switch to useResource$() and <Resource />.
Unlike routeLoader$(), useResource$() must be declared inside a component.
The Qwik docs describe useResource$() as a lower-level API. It can return a value, and it does not block rendering while the resource is being resolved.
Let's use the same dashboard example again, but keep the same mental model from the previous chapter: one request for revenue, one request for latest invoices, and one request for the card data.
Replace the three routeLoader$() functions with three useResource$() resources in src/routes/dashboard/index.tsx:
This time, the dashboard has three separate resources and three separate <Resource /> boundaries:
cardDataResourcerenders the four summary cards.revenueResourcerenders the revenue chart.latestInvoicesResourcerenders the latest invoices panel.
Each <Resource /> can render three UI states:
onPendingwhile the data is loading.onRejectedif something fails.onResolvedwhen the data is ready.
The behavior is different depending on how the user reaches the page. During SPA navigation, the cards and latest invoices appear immediately, while the revenue chart shows Loading Revenue during the 3-second delay. On a full page reload, Qwik renders the cards first, then the revenue chart and latest invoices appear after the delayed revenue request resolves.
During SPA navigation, the cards and latest invoices appear instantly. The revenue chart shows Loading Revenue for 3 seconds, then renders when the revenue data is ready.
If you reload the page directly, the behavior is a little different. The cards render first, then after the 3-second revenue delay, the revenue chart and latest invoices render together.
That looks closer to the deferred routeLoader$() example: the page can render up to a resource boundary first, then continue when the delayed resource has resolved.
Now that the loading states are visible, let's improve the experience by replacing the plain loading text with skeletons.
Itβs time to take a quiz!
Test your knowledge and see what youβve just learned.
What is the purpose of the onPending callback?
𦴠Adding loading skeletons
The plain Loading... text works, but it does not give the user a clear sense of what is coming next. A skeleton is a lightweight placeholder shaped like the final UI.
Download the skeleton components and place the file in src/components/ui/skeletons.tsx:
The skeleton file uses a shimmer animation. Add the animation to src/global.css:
Now replace the loading text with the matching skeleton for each resource:
This is better because each loading state now reserves space for the part of the dashboard it belongs to. The page feels less jumpy while the data is loading, and one slow request does not force every section to use the same placeholder.
Skeletons should look like the final layout, not like a separate loading screen. The closer the placeholder is to the real UI, the less visual surprise users feel when the data arrives.
π― Deciding where to place your <Resource /> boundaries
You now have one <Resource /> boundary per request. That matches the three separate data needs from the previous chapter, and it gives each part of the page its own loading, error, and resolved UI.
Here is the complete dashboard route with the three resources and the three skeleton boundaries:
The important detail is that the slow fetchRevenue() request is no longer coupled to fetchCardData() or fetchLatestInvoices(). Each request has its own boundary, so each section can show the right placeholder and resolve on its own schedule.
Practice: Review the three boundaries
Your turn: compare the final dashboard route with the three routeLoader$() functions from the previous chapter.
The structure should feel familiar: three independent data requests, but now each request is handled with useResource$(), <Resource />, and a skeleton that matches the UI it renders.
Summary
You now have three useful patterns for this dashboard:
Use a deferred
routeLoader$()when you want the page to render up to a<Resource />boundary on a full reload.Use
useResource$()when a component needs its own pending, resolved, and rejected UI states.Place
<Resource />boundaries around UI that can load independently, and use skeletons shaped like the final layout.
Itβs time to take a quiz!
Test your knowledge and see what youβve just learned.
Where should you usually place a Resource boundary?
Source code
You can find the source code for chapter 10 2026 Edition on GitHub.
You've Completed Chapter 10
Nice! You've learned how to stream dashboard UI with Resource boundaries, useResource$(), and loading skeletons.
Next Up
11: Mutating Data
The next 2026 chapter will continue the dashboard app with data mutations.
Was this helpful?