stencil
stencil copied to clipboard
feat(runtime): add prefetchComponent() api
We have a use case for this in our Stencil + Ionic app:
Our app renders the login page with a hidden side menu and no headers. After signing in, once the user's data has been retrieved from the API, the app's state changes to logged in; this causes the side menu and headers to appear, and a route-redirect to kick in which redirects the user to their landing page.
<ion-app>
<ion-split-pane when={Boolean(this.user)} contentId="main">
<SideMenu contentId="main" user={this.user} {...otherProps} />
<ion-nav id="main" />
</ion-split-pane>
</ion-app>
However, because the landing page is lazy-loaded, the login page is still rendered in the ion-nav until the landing page has been fetched (which might take a few seconds depending on network speed). That causes the login page to stay in the ion-nav with the sidebar and headers already visible.
When serving the app locally this is not really notable because of how fast the landing page is fetched.
If the API suggested here was available, I could simply add
componentDidLoad() {
prefetchComponent('page-landing');
}
into the login page to start fetching that page in the background.
Another use case would be to pre-fetch ion-select-popover or ion-modal, because the first time they are loaded they also need up to a few seconds to actually show up after the click.
Just a thought, this would probably also be solvable by using a service worker to pre-fetch everything (and skip waiting if no service worker has been registered yet).
I have a use-case for this functionality.
In my app, we use a “toast” Stencil component (which are mini-modals we show in the upper right portion of the viewport, for displaying messages, errors, and warnings that occur during our app’s usage). For additional context, they look like this:
Like all other Stencil components, our toast component is lazily loaded.
If a user visits our app, but then the network connection is dropped, and an error is thrown, we attempt asynchronously fetching our toast component then. But since there’s no network connection, fetching the component fails, and error messages are not displayed -- which would leave our users confused.
We’d like to ensure any “mission critical” components are always loaded and available on page load-- especially when those components are for helping the user when we break off the application's happy path and into the error/exception throwing path.
@petermikitsh in theory you can define your own bundles too, and add toast to the same bundle as the "app-root" once
Will this be possible?
<!-- index.html -->
<script src="preload.js"></script> <!-- blocking request -->
<script src="app.js"></script>
// preload.js
prefetchComponent("my-loading-spinner"); // also loads child components (recursively)
// app.js
render() {
<my-loading-spinner />; // immediate render: no flickers, no glitches, ...
}
out of curiosity, is this still going to happen or should I look for another option? I tried to solve this situation as it was recommended before by using the bundles property from the stencil config file, but it ended up making the first chuck of javascript quite big, and I think it will be super helpful to have the prefetchComponent API available.
I would also like to see this API come to fruition. I'm building a SPA and have found that occasionally navigation suffers a noticeable lag between when the user clicks to navigate and when the page transition actually occurs if network latency is sub-optimal. Pre-loading components would alleviate this issue.
@mobregozo did you find any good way to do this in the meantime?
I made my own crude prefetching solution in the meantime if it helps anyone else: https://github.com/beck24/stencil-component-prefetch
Hopefully we get this functionality baked in at some point.
I made my own crude prefetching solution
@beck24 just had a look and good idea, but seems pretty verbose... also, you should probably use componentDidLoad in your example instead of componentDidRender, because otherwise you'll do that invisible DOM adding every time the used component re-renders 🤓
At the moment, the solution for us is to have a service worker, and if it's the first time that the app/page is loaded (i. e. there's no existing active service worker), then we skip waiting; that service worker then pre-fetches the whole app, and it's pretty smooth for us (we also show an "update available" toast when there's a new service worker). But I understand that a service worker is not suitable sometimes, and even with the service worker we would like to be more explicit about some of the component fetching at runtime... so this API would still be helpful to us.
@simonhaenisch thanks for the input - I updated the documentation to use componentDidLoad
Any chance you can share the service worker implementation for downloading everything? I haven't done much with service workers yet.
Hello everyone! We have been thinking again about this feature, and we will be adding it to stencil very soon, thanks a lot for all the comments we will take all of them into consideration.
bump.
Was it added, or is it on the roadmap right now?
What about something like:
const empty = document.createElement('div');
empty.style = 'display:none;';
empty.append(document.createElement('my-component-1'));
empty.append(document.createElement('my-component-2'));
document.body.append(empty);
Would still have to clean up, though.
With this approach, the styles also get added to the page - the lack of them also causes a FOUC.
Since this was submitted, we have two new output targets:
dist-custom-elements-bundledist-custom-elements(not documented yet, available in prerelease version 2.4.0-1)
Both of these let you bypass the lazy loader:
dist-custom-elements-bundlecompiles all components into one big file and is intended to be consumed by webpack/Rollup/etc.dist-custom-elementscompiles each component into separate files and can be imported directly by the browser
Will prefetching still provide value with these new output targets?
Will prefetching still provide value with these new output targets?
@claviska Absolutely! Remember, that there is also app output target and prefetching would be really useful to avoid loading lag (especially in slow mobile connection), e.g. when you click a button, which opens a modal and ion-modal was not loaded before you have to wait (without any visible indicator that something is loading) not only for ion-modal to load but for loading component that is to be presented within modal. So when having prefetch api I could prefetch those components, that are crucial in order to avoid visible lags.
So what is the reccomended approach for this right now? Doing it through service workers maybe?
So what is the recommended approach for this right now? Doing it through service workers maybe?
I believe something similar can be achieved with dist-custom-elements, although the framework output targets don't play well with it yet: https://github.com/ionic-team/stencil-ds-output-targets/issues/136
@adrm yeah we've solved it with a service worker. On first load it downloads the whole app in the background. Then we instruct the service worker to immediately take over if there's no active installation yet, so that the service worker cache kicks in immediately. That way all files are served from cache and there's no download lag or at least it's not notable.
I doubt this prefetchComponent API is going to make it into the source anytime soon because Manu is currently not actively maintaining Stencil, and even when he was still active, there was no progress on this for a while.
Just for some inspiration, here's some of the service worker code I wrote:
/**
* If no registration is active yet, this is the first install of a service worker; therefore we can just immediately let the waiting registration become active and take over.
*/
self.addEventListener('install', () => {
if (!self.registration.active) {
self.skipWaiting();
}
});
/**
* Since we only skip waiting if there wasn't an active registration already, a client can send a `skipWaiting` message, which we will use to activate the waiting registration.
*/
self.addEventListener('message', ({ data }) => {
if (data === 'skipWaiting') {
self.skipWaiting();
}
});
/**
* Every time a new service worker is taking over, let's claim all the clients. This will trigger a `controllerchange` event, which all the clients should listen to and reload (to avoid issues with outdated cached files).
*/
self.addEventListener('activate', (e) => {
e.waitUntil(clients.claim());
});
We show a toast to the user when an update is available, something like this in our app-root component:
/**
* Handle service worker updates. The `swUpdate` event is emitted by the code that Stencil injects into `index.html`.
*/
@Listen('swUpdate', { target: 'window' })
async onServiceWorkerUpdate() {
const registration = await navigator.serviceWorker.getRegistration();
if (!registration?.waiting) {
return; // the first sw installation also triggers the `swUpdate` event but we don't want to show a toast in that case
}
const toast = await showToast('A new version is available.', { buttons: [{ text: 'Reload', role: 'reload' }] });
const { role } = await toast.onWillDismiss();
if (role === 'reload') {
registration.waiting.postMessage('skipWaiting');
}
}
async componentWillLoad() {
/**
* The `controllerchange` event occurs when a new service worker becomes active. When this happens, we want to trigger a page-reload, unless this is the first service worker to take over (i. e. there's no active worker registered yet).
*/
if ('serviceWorker' in navigator) {
// don't await this because it shouldn't block the app load
navigator.serviceWorker
.getRegistration()
.then((registration) => {
if (registration?.active) {
navigator.serviceWorker.addEventListener('controllerchange', () => window.location.reload());
}
})
.catch(console.error);
}
}
@splitinfinities do you know the status of this PR?
I think preloading individual components during runtime is an important feature by Stencil for many of us devs.
The comments above mention workarounds such as web workers or dist-custom-elements-bundle. However, none of those exactly solve the core need: to easily let consumers of our Design Systems preload individual components like this:
<!-- index.html -->
<script src="preload.js"></script> <!-- blocking request -->
<script src="app.js"></script>
// preload.js
await prefetchComponent("my-loading-spinner") // also loads child components (recursively)
// app.js
render() {
<my-loading-spinner /> // no FOUC thanks to preload.js
}
Has there been any more discussion around this topic? Or are there any recommended alternative solutions?
👋 Thanks for all of your work on Stencil over the years! Due to inactivity, we’re going to close this PR out. If anyone is interested in the functionality described here, please feel free to open a new issue in the repo. Thanks!