Skip to content

Commit 2ed2983

Browse files
committed
feat: render blocking task by default
1 parent f153154 commit 2ed2983

File tree

3 files changed

+95
-22
lines changed

3 files changed

+95
-22
lines changed

packages/docs/src/routes/docs/(qwik)/core/tasks/index.mdx

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ contributors:
2020
- aendel
2121
- jemsco
2222
- varixo
23-
updated_at: '2025-10-31T03:33:22Z'
23+
updated_at: '2025-11-08T03:33:22Z'
2424
created_at: '2023-03-31T02:40:50Z'
2525
---
2626

@@ -35,16 +35,16 @@ Tasks are meant for running asynchronous operations as part of component initial
3535
>
3636
> - Tasks are asynchronous.
3737
> - Tasks run on server and browser.
38-
> - Tasks run before initial rendering and block the initial render.
39-
> - Subsequent task re-executions (when tracked state changes) don't block rendering by default, but can be configured to block DOM updates with `deferUpdates: true`.
38+
> - Tasks run before rendering and can block rendering.
39+
> - Subsequent task re-executions (when tracked state changes) block rendering by default, but can be configured to not block DOM updates with `deferUpdates: false`.
4040
4141
`useTask$()` should be your default go-to API for running either synchronous or asynchronous work as part of component initialization or state change. It is only when you can't achieve what you need with `useTask$()` that you should consider using `useVisibleTask$()` or `useResource$()`.
4242

4343
The basic use case for `useTask$()` is to perform work on component initialization. `useTask$()` has these properties:
4444
- It can run on either the server or in the browser.
4545
- It runs before the initial rendering and blocks the initial render.
4646
- If multiple tasks are running then they are run sequentially in the order they were registered. An asynchronous task will block the next task from running until it completes.
47-
- By default, subsequent re-executions (when tracked state changes) do not block DOM updates unless `deferUpdates: true` is specified.
47+
- By default, subsequent re-executions (when tracked state changes) do block DOM updates unless `deferUpdates: false` is specified.
4848

4949
Tasks can also be used to perform work when a component state changes. In this case, the task will rerun every time the tracked state changes. See: [`track()`](#track).
5050

@@ -111,19 +111,12 @@ interface TaskOptions {
111111
The `deferUpdates` option controls whether subsequent task re-executions (when tracked state changes) should block DOM updates.
112112

113113
**Default behavior (`deferUpdates: false` or not specified):**
114-
- **Initial render**: The task blocks rendering (runs before the component renders for the first time)
115-
- **Subsequent runs**: The task runs asynchronously without blocking DOM updates
116-
117-
**With `deferUpdates: true`:**
118114
- **Initial render**: The task blocks rendering (same as default)
119115
- **Subsequent runs**: The task blocks DOM updates until it completes - the journal flush is deferred until the task finishes
120116

121-
**When to use `deferUpdates: true`:**
122-
- When you need to ensure that asynchronous state updates complete before the UI reflects changes
123-
- When you want to prevent visual flickering by ensuring all related state updates happen atomically
124-
125-
**Performance Considerations:**
126-
- Use carefully as blocking DOM updates on subsequent runs can impact perceived performance
117+
**With `deferUpdates: true`:**
118+
- **Initial render**: The task blocks rendering (runs before the component renders for the first time)
119+
- **Subsequent runs**: The task runs asynchronously without blocking DOM updates
127120

128121
Additionally, this task can be reactive and will re-execute when **tracked** [state](/docs/(qwik)/core/state/index.mdx) changes.
129122

@@ -142,7 +135,7 @@ Additionally, this task can be reactive and will re-execute when **tracked** [st
142135

143136
> If `useTask$()` does not track any state, it will run **exactly once**, either in the server **or** in the browser (**not both**), depending where the component is initially rendered. Effectively behaving like an "on-mount" hook.
144137
145-
`useTask$()` will block the initial rendering of the component until after its async callback resolves. Tasks execute sequentially even if they are asynchronous (only one task executes at a time within a component). Subsequent re-executions (when tracking state changes) run asynchronously by default and don't block rendering unless `deferUpdates: true` is set.
138+
`useTask$()` will block the rendering of the component until after its async callback resolves. Tasks execute sequentially even if they are asynchronous (only one task executes at a time within a component). Subsequent re-executions (when tracking state changes) run asynchronously by default and don't block rendering unless `deferUpdates: true` is set.
146139

147140
Take a look at the simplest use case of the task to run some asynchronous work on component initialization:
148141

@@ -173,18 +166,18 @@ const delay = (time: number) => new Promise((res) => setTimeout(res, time));
173166

174167
> In this example
175168
>
176-
> - The `useTask$()` computes the fibonacci number one entry per 100 ms. So 40 entries take 4 seconds to compute.
169+
> - The `useTask$()` computes the fibonacci number one entry per 100 ms. So 40 entries take 4 seconds to render.
177170
> - The `useTask$()` executes on the server as part of the SSR (the result may be cached in CDN.)
178-
> - Because the `useTask$()` blocks initial rendering, the rendered HTML page takes 4 seconds to generate.
179-
> - Because this task has no `track()` it will never rerun, making it effectively initialization code.
171+
> - Because the `useTask$()` blocks rendering, the rendered HTML page takes 4 seconds to render.
172+
> - Because this task has no `track()` it will never rerun, making it effectively an initialization code.
180173
> - Because this component only renders on the server, the `useTask$()` will never be downloaded or run on the browser.
181174
182175
> Notice that `useTask$()` runs on the server **BEFORE** the actual rendering. Therefore if you need to do DOM manipulation, use [`useVisibleTask$()`](#usevisibletask) instead, which runs on the browser after rendering.
183176
184177
Use `useTask$()` when you need to:
185-
- Run async tasks before initial rendering
178+
- Run async tasks before rendering
186179
- Run code only once before the component is first rendered
187-
- Programmatically run side-effect code when state changes (with or without blocking DOM updates)
180+
- Programmatically run side-effect code when state changes
188181

189182
> Note, if you're thinking about loading data using `fetch()` inside of `useTask$`, consider using [`useResource$()`](/docs/core/state/#useresource) instead. This API is more efficient in terms of leveraging SSR streaming and parallel data fetching.
190183
@@ -266,7 +259,7 @@ const delay = (time: number) => new Promise((res) => setTimeout(res, time));
266259
>
267260
> The `useTask$()`
268261
>
269-
> - By default, `useTask$()` blocks the initial rendering until it completes. Subsequent executions (when tracked state changes) don't block rendering by default. In this example, we don't await `delay()` to avoid blocking even the initial render - the delay runs independently while the task completes immediately.
262+
> - The `useTask$()` blocks rendering until it completes. If you don't want to block rendering, make sure that the task is resolved, and run the delay work on a separate unconnected promise. In this example, we don't await `delay()` because it would block rendering.
270263
271264
> Sometimes it is required to only run code either in the server or in the client. This can be achieved by using the `isServer` and `isBrowser` booleans exported from `@qwik.dev/core` as shown above.
272265

packages/qwik/src/core/tests/use-task.spec.tsx

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,84 @@ describe.each([
730730

731731
vi.useRealTimers();
732732
});
733+
734+
it('should execute task and not block render until finish', async () => {
735+
vi.useFakeTimers();
736+
(global as any).counter = 0;
737+
const Counter = component$(() => {
738+
const count = useSignal(0);
739+
const text = useSignal('val1');
740+
741+
useTask$(
742+
async ({ track }) => {
743+
const c = track(count);
744+
// skip initial render
745+
if ((global as any).counter > 0) {
746+
text.value = 'val' + (c + 1);
747+
await delay(100);
748+
}
749+
(global as any).counter++;
750+
},
751+
{
752+
deferUpdates: false,
753+
}
754+
);
755+
return (
756+
<button
757+
onClick$={() => {
758+
count.value++;
759+
}}
760+
>
761+
{text.value}
762+
</button>
763+
);
764+
});
765+
766+
// Start rendering
767+
const renderPromise = render(<Counter />, { debug });
768+
// Advance timers to complete rendering
769+
await vi.advanceTimersToNextTimerAsync();
770+
const { document } = await renderPromise;
771+
772+
// Initial render
773+
await expect(document.body.firstChild).toMatchDOM(<button>val1</button>);
774+
775+
// FIRST CLICK
776+
777+
// Trigger task by clicking
778+
let triggerPromise = trigger(document.body, 'button', 'click');
779+
780+
// Advance timers but not enough to complete the delay
781+
await vi.advanceTimersByTimeAsync(99);
782+
// Should have the new value
783+
await expect(document.body.firstChild).toMatchDOM(<button>val2</button>);
784+
// Advance timers to complete the delay
785+
await vi.advanceTimersByTimeAsync(1);
786+
// Wait for the trigger to complete
787+
await triggerPromise;
788+
789+
// Should have the new value
790+
await expect(document.body.firstChild).toMatchDOM(<button>val2</button>);
791+
792+
// SECOND CLICK
793+
794+
// Trigger task by clicking
795+
triggerPromise = trigger(document.body, 'button', 'click');
796+
797+
// Advance timers but not enough to complete the delay
798+
await vi.advanceTimersByTimeAsync(99);
799+
// Should have the new value
800+
await expect(document.body.firstChild).toMatchDOM(<button>val3</button>);
801+
// Advance timers to complete the delay
802+
await vi.advanceTimersByTimeAsync(1);
803+
// Wait for the trigger to complete
804+
await triggerPromise;
805+
806+
// Should have the new value
807+
await expect(document.body.firstChild).toMatchDOM(<button>val3</button>);
808+
809+
vi.useRealTimers();
810+
});
733811
});
734812

735813
describe('regression', () => {

packages/qwik/src/core/use/use-task.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,9 @@ export const useTaskQrl = (qrl: QRL<TaskFn>, opts?: TaskOptions): void => {
149149
assertQrl(qrl);
150150
set(1);
151151

152-
const taskFlags = opts?.deferUpdates ? TaskFlags.RENDER_BLOCKING : 0;
152+
const taskFlags =
153+
// enabled by default
154+
opts?.deferUpdates === false ? 0 : TaskFlags.RENDER_BLOCKING;
153155

154156
const task = new Task(
155157
TaskFlags.DIRTY | TaskFlags.TASK | taskFlags,

0 commit comments

Comments
 (0)