Skip to content

9

Optimizing Data Fetching
2026 Edition

In the previous chapter, you learned how to load dashboard data with routeLoader$(). Now it's time to optimize where that data should be loaded.

In this chapter, you'll learn the difference between static and dynamic rendering, why a dashboard is a better fit for dynamic rendering, and how route boundaries affect when loaders run.

🧱 Static and dynamic rendering

What is static rendering?

With static rendering, data fetching and rendering happen at build time, when you deploy the application.

When a user visits the page later, the cached result can be served again.

There are a couple of benefits of static rendering:

  • Faster websites: prerendered pages can be cached and distributed globally.
  • Reduced server load: the server does not need to generate the page again for every request.
  • SEO: the content is already available when the page loads, which makes it easier to index.

Static rendering is useful for pages with no changing data or data that is shared across users, such as a blog post, a documentation page, or a product page.

It might not be a good fit for a dashboard with data that changes regularly.

It’s time to take a quiz!

Test your knowledge and see what you’ve just learned.

Why might static rendering not be a good fit for a dashboard app?

You must choose response !

What is dynamic rendering?

With dynamic rendering, content is rendered on the server at request time, when the user visits the page.

There are a couple of benefits of dynamic rendering:

  • Fresh data: the page can reflect frequently updated information.
  • User-specific content: it is easier to serve personalized pages such as dashboards or account areas.
  • Request-time information: the server can work with information that is only available when the request happens.

That's a better fit for the dashboard in this course. Revenue, latest invoices, and summary cards can all change over time, so the page needs fresh data to stay useful.

In this dashboard, you're using dynamic server rendering. Qwik City also supports static site generation, but static rendering needs to be configured explicitly. Here, dynamic rendering is a better fit because the dashboard data should not be frozen at build time.

💡

Qwik has a nice advantage here: even when pages are prerendered, they still benefit from resumability instead of paying the usual full hydration cost on page load.

Dynamic rendering is the right choice here, but you still need to decide where your loaders should run.

A loader in layout.tsx can make sense when several child routes need the same data. But if the data is only needed by one page, placing that loader too high in the route tree can trigger unnecessary requests.

🧭 Route boundaries in Qwik

In Qwik, a layout.tsx file defines shared UI for its child routes.

If you place a routeLoader$() inside that shared layout, it can be triggered for child routes that share the same route boundary.

This can be useful when several child routes need the same data.

But in this dashboard, the overview data is only used by the dashboard home page. That means placing those loaders in the shared layout makes their scope broader than necessary.

Triggering data requests

To make this easier to see, open src/routes/dashboard/layout.tsx and add a console.log() inside one of the loaders.

src/routes/dashboard/layout.tsx

Then open the browser, watch your terminal, and move across the dashboard links. Because the loader lives in the shared layout, it can run for child routes that use that layout, even when those pages do not need the overview data.

💡

In development, you may notice this request as soon as you hover a dashboard link. That happens because Qwik City <Link> components prefetch their target route by default, and that prefetch also includes any matching routeLoader$(). In development, this usually happens on mouseover or focus. In production, it usually happens when the link enters the viewport instead. You can read more about this behavior in the Qwik docs on Link prefetching.

When the loader lives in layout.tsx, it can be triggered for dashboard child routes that share the same route boundary.

This would make sense if all dashboard pages needed the same data.

But in this application, the overview data is only needed on the dashboard home page. It would be better to trigger these requests only when the user navigates to /dashboard/.

📍 Moving the loader to index.tsx

To scope the requests more precisely, move the dashboard loaders from layout.tsx to index.tsx.

Start by removing the routeLoader$() functions and their related imports from src/routes/dashboard/layout.tsx.

Your shared layout should now only keep the dashboard structure and shared UI.

src/routes/dashboard/layout.tsx

Then move those loaders into src/routes/dashboard/index.tsx, so the dashboard home page fetches its own data.

src/routes/dashboard/index.tsx

Now, the requests only run when the user navigates to the dashboard home page.

When the loaders live in index.tsx, the requests run only for the dashboard home page.

⚡ Avoiding unnecessary requests

This is a better fit for the dashboard overview. The revenue chart, latest invoices, and summary cards are only used on the home page, so the loaders should live there too.

That does not mean layout.tsx is a bad place for loaders. If several child routes need the same shared data, placing the loader in the layout can still be the right choice.

The goal is simply to match the loader location to the real scope of the data.

It’s time to take a quiz!

Test your knowledge and see what you’ve just learned.

What happens when dashboard loaders are placed in layout.tsx?

You must choose response !

🐢 Simulating a slow data fetch

Moving loaders to the right route boundary is a good first step, but what happens if one request is slower than the others?

Let's simulate a slow data fetch to see what happens.

In src/lib/loaders.ts, add a delay of 3 seconds to the fetchRevenue() function.

src/lib/loaders.ts

Now open http://localhost:5173/dashboard/ in a new tab and notice how the page takes longer to load. In your terminal, you should also see the following messages:

After adding a 3-second delay to fetchRevenue(), the whole page waits for the slow request before rendering.

Here, you've added an artificial 3-second delay to simulate a slow data fetch. The result is that the whole page now waits for that request before it can render.

Which brings us to a common challenge developers have to solve:

With dynamic rendering, your application is only as fast as your slowest data fetch.

The solution to this problem is streaming.

In the next chapter, you'll learn how to use streaming to improve the user experience while slower data is still loading.

It’s time to take a quiz!

Test your knowledge and see what you’ve just learned.

What is the effect of simulating a slow data fetch?

You must choose response !

Source code

You can find the source code for chapter 9 2026 Edition on GitHub.

You've Completed Chapter 9

Nice! You've learned how to load dashboard data at the right route boundary in Qwik.

Next Up

10: Streaming

Learn how to keep the UI responsive while slower data is still loading.