Best Practices
Inline operations in templates
Qwik optimizer can better optimize the reactivity of the application if the operations are inlined in the template.
// Don't do this!
export default component$(() => {
const signal = useSignal(0);
const isBiggerThanZero = signal.value > 0 ? 'Bigger than zero' : 'Smaller than zero';
return (
<div>
<button onClick$={() => signal.value++}>+</button>
<button onClick$={() => signal.value--}>-</button>
<div>{isBiggerThanZero} - Current value: { signal.value }</div>
</div>
);
});
The above implementation will cause the whole template to be re-rendered when the signal changes. This is because the isBiggerThanZero
is not inlined in the template.
export default component$(() => {
const signal = useSignal(0);
return (
<div>
<button onClick$={() => signal.value++}>+</button>
<button onClick$={() => signal.value--}>-</button>
<div>
{signal.value > 0 ? 'Bigger than zero' : 'Smaller than zero'} - Current
value: {signal.value}
</div>
</div>
);
});
useTask$
or useComputed$
Moving signal reads to This is similar to the tip above.
Every time Qwik "reads" a signal/store value the function that the "read" is happening at, will re-run again on every change to that signal.
Thatโs why itโs better to "read" values (and track them)
inside of useTask$
or useComputed$
functions instead of inside component functions,
whenever possible.
Because otherwise, your component function will re-run on each change.
// Don't do this!
export default component$(() => {
const count = useSignal(1);
const doubleCount = count.value*2;
return (
<div>{doubleCount}</div>
);
});
The above implementation will cause the whole template to be re-rendered when the signal changes.
Below, only the useComputed$
function will re-run on any count.value
change:
export default component$(() => {
const count = useSignal(1);
const dobuleCount = useComputed$(() => count.value*2);
return (
<div>{doubleCount.value}</div>
);
});
useVisibleTask$()
as a last resort
Use Although convenient, useVisibleTask$()
runs code eagerly and blocks the main thread, preventing user interaction until the task is finished. You can think of it as an escape hatch.
When in doubt, instead of "useVisibleTask$()" use:
useTask$()
-> perform code execution in SSR mode.useOn()
-> listen to events on the root element ofthe current component
.useOnWindow()
-> listen to events on thewindow
object.useOnDocument()
-> listen to events on thedocument
object.
Sometimes though, it is the only way to achieve the result.
In that case, you can add // eslint-disable-next-line qwik/no-use-visible-task
to the line before "useVisibleTask$" to remove the warning.
useOn()
, useOnWindow()
, or useOnDocument()
Register DOM events with Qwik allows to register event listeners in a declarative way, using the useOn()
or using JSX.
When using useVisibleTask$()
to programmatically register events, we are downloading and executing JavaScript eagerly, even if the event is not triggered.
// Don't do this!
useVisibleTask$(({ cleanup }) => {
const listener = (event) => {
const mouseEvent = event as MouseEvent;
console.log(mouseEvent.x, mouseEvent.y);
};
document.addEventListener('mousemove', listener);
cleanup(() => {
document.removeEventListener('mousemove', listener);
});
});
The above implementation causes more JavaScript to load eagerly, rather than responding precisely to user events. Increased upfront JavaScript loading results in slower app performance. See below for more details.
Instead, use the useOnDocument()
hook to register events on the document
object, this way Qwik will not execute any JS until the event is triggered.
useOnDocument(
'mousemove',
$((event) => {
const mouseEvent = event as MouseEvent;
console.log(mouseEvent.x, mouseEvent.y);
// No manual clean up required!
})
);
window
object
Avoid accessing the location from the Don't access window.location
directly, use useLocation()
hook instead.
// Don't do this!
useVisibleTask$(()=> {
if (window.location.href).includes('foo') {
//... do the thing
}
})
// or
useTask$(() => {
if (isBrowser) {
if (window.location.href).includes('foo') {
//... do the thing
}
}
})
Many actions related to location information can be executed during the initial server-side render, resulting in pure HTML without any JavaScript overhead.
Forcing this logic to run on the client side introduces increased upfront JavaScript and leads to eager loading.
// Do this!
const location = useLocation();
if (location.url.href.includes('foo')) {
// Do the thing
}
Exception
When using SSG for purely static files, it's inevitable to rely on the server without current location information during the build time.
However, exercise caution! If the required information (such as query parameters) isn't needed until a user event occurs, incorporate the check within your event handling code.
This approach helps to prevent eager loading of JavaScript and improves performance.
See: useLocation() Docs
Delaying Core Execution
At load time we want to execute as little as possible to free main thread for other priority. With Qwik you can even delay the framework execution (called "core"), but there are some rules to follow.
Some things to keep in mind before checking the rules :
- Delaying core execution usually comes at the price of devX, this is an advance performance trick.
- Like other chunks, core will be preloaded so the app doesn't have to load it at execution time.
useVisibleTask$
useVisibleTask$
will always execute core before its callback is called.
// Requires core when component is visible in the viewport
useVisibleTask$(() => {
console.log('Hello core');
});
// Requires core on requestIdleCallback
useVisibleTask$(
() => console.log('Hello core'),
{ strategy: 'document-idle' }
);
In some cases you can replace useVisibleTask$
with either useOn
or useTask$
useOn
Replace useVisibleTask$
with useOn('qvisible')
/ useOn('qidle')
if
- You only need to trigger a callback once
- The code must run on the client
useOn
, useOnDocument
& useOnWindow
execute core if they use a variable from the component scope :
import { libId } from 'library';
const globalId = 'global-id';
const Component = component$(() => {
const ref = useSignal();
const id = useId();
// Executes core at load time
useOnDocument('qidle', $(() => console.log(ref)));
// Executes core at load time
useOnDocument('qidle', $(() => console.log(id)));
// Does not execute core at load time
useOnDocument('qidle', $(() => console.log(globalId)));
// Does not execute core at load time
useOnDocument('qidle', $(() => console.log(libId)));
// Does not execute core at load time
useOnDocument('qidle', $(() => console.log('id')));
return (
<p ref={ref}></p>
<p id={id}></p>
<p id={globalId}></p>
<p id={libId}></p>
<p id="id"></p>
)
})
useTask$
Replace useVisibleTask$
with useTask$
if
- You need to listen on state changes
- The code can execute on the server
const Component = component$(() => {
const search = useSignal();
// Does not execute until `search` changes
useTask$(({ track }) => {
track(search);
console.log(search.value);
});
return <input bind:value={search} type="search" />;
});
useOn + useTask$
A classic usecase of useVisibleTask$
is to start listening on browser specific event:
const isMobile = useSignal(false);
useVisibleTask$(({ cleanup }) => {
const query = window.matchMedia('(max-width: 400px)');
const handler = (event) => {
isMobile.value = event.matches;
};
query.addEventListener('change', handler);
cleanup(() => query.removeEventListener('change', handler));
});
In this case we actually need core when the handler is triggered. Here is how to delay core execution :
const isMobile = useSignal(false);
// On idle, start listening on the event
useOnDocument(
'qidle',
sync$(() => {
const query = window.matchMedia('(max-width: 400px)');
const handler = (event) => {
// Forward the event to the document
const copy = new event.constructor('media-query:(max-width: 400px)', event);
document.dispatchEvent(copy);
};
// Store mediaQuery & handler to cleanup later
document['cleanup:media-query:(max-width: 400px)'] = () => {
query.removeEventListener('change', handler)
};
query.addEventListener('change', handler);
})
);
// useOnDocument execute core when it actually needs it
useOnDocument(
'media-query:(max-width: 400px)',
$((event) => {
isMobile.value = event.matches;
})
);
// useTask$ is used to cleanup event listeners
useTask$(({ cleanup }) => {
cleanup(() => {
if (!document['cleanup:media-query:(max-width: 400px)']) return;
document['cleanup:media-query:(max-width: 400px)']();
delete document['cleanup:media-query:(max-width: 400px)'];
});
});
As we can see, this is a LOT of work, and it's not a great dev experience ! But if you're building a library or just trying to go as lean as possible, this is possible.