Implement a Visibility‑Triggered Lazy Hydrated Component in Qwik
Learn how to make a Qwik component hydrate only when it becomes visible, using useClient$ and useVisibleTask$ for true lazy loading.
16 Jul 2025, 00:06 UTC

Desired Outcome
Create an interactive Qwik component that stays as static HTML until the user scrolls it into view, at which point its JavaScript is fetched and hydrated only for that instance. This reduces initial payload and avoids unnecessary hydration of off‑screen UI.
Prerequisites
- Node.js ≥18 and npm or yarn installed.
- A Qwik project scaffolded with
npm create qwik@latest(or equivalent). - Basic familiarity with Qwik concepts:
useClient$for client‑side code anduseVisibleTask$for viewport‑based tasks.
Procedure
-
Create a stateless component
Define a component that renders static markup and exposes an interactive part (e.g., a button that toggles a counter).
-
Wrap the interactive logic in useClient$
This tells Qwik to split the callback into a separate chunk that will be downloaded only when the callback runs.
-
Trigger the client‑side init with useVisibleTask$
The observer runs the provided callback when the component’s root element enters the viewport.
-
Use the component in a page
Place
<LazyCounter />anywhere in your route (e.g.,src/routes/index.tsx). -
Build and preview
Run the dev server (
npm run dev) or production build (npm run build && npm run preview) to test.
import { component$, useVisibleTask$, useClient$ } from '@builder.io/qwik';
export const LazyCounter = component$(() => {
// Static part – always rendered on the server
return (
This counter is lazy‑hydrated.
{/* Interactive part will be injected by useVisibleTask$ */}
);
});
const initCounter = useClient$(() => {
let count = 0;
const host = document.getElementById('counter-host');
if (!host) return;
host.innerHTML = `
+1
0
`;
const btn = host.querySelector('#inc');
const val = host.querySelector('#val');
btn.addEventListener('click', () => {
count++;
val.textContent = String(count);
});
});
export const LazyCounter = component$(() => {
useVisibleTask$(() => {
initCounter();
});
return (
This counter is lazy‑hydrated.
);
});
import { LazyCounter } from '@/components/LazyCounter';
export default component$(() => {
return (
<>
Qwik Lazy Hydration Demo
{/* Repeat to see multiple lazy chunks */}
);
});
Expected Checks
- Open Chrome DevTools → Network, disable cache, and reload the page.
- Verify that no JavaScript chunk containing
initCounteris requested initially (look for a file named something likechunk-*.jsthat includes the counter logic). - Scroll the page until a
<LazyCounter />enters the viewport. - Observe a network request for the corresponding chunk and, in the Console, a log from
useClient$(if you added one) confirming the callback ran. - Interact with the button; the counter should update, proving hydration succeeded.
Recovery Options / Limitations
- Older browsers without IntersectionObserver: The visible task may never fire. Include a polyfill (e.g.,
intersection-observernpm package) or provide a fallback that hydrates on load for those environments. - Observer overhead: Using
useVisibleTask$on many small items can create many observers. For lists, consider a single observer on a parent container and delegate visibility checks, or useuseVisibleTask$only on items that are likely to be off‑screen initially. - JavaScript disabled: The component remains as static HTML (the server‑rendered markup). When JavaScript is re‑enabled, scrolling back into view will trigger the visible task and fetch the chunk.
To verify the fallback, temporarily block the chunk request in DevTools (Network → right‑click → Block request URL) and reload; the component should stay non‑interactive but still display its static text. Unblocking and scrolling again restores functionality.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.