stencil icon indicating copy to clipboard operation
stencil copied to clipboard

feat(runtime): add prefetchComponent() api

Open manucorporat opened this issue 6 years ago • 18 comments

manucorporat avatar May 21 '19 14:05 manucorporat

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).

simonhaenisch avatar Jun 20 '19 01:06 simonhaenisch

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:

Screen Shot 2019-07-29 at 2 56 55 PM

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 avatar Jul 29 '19 22:07 petermikitsh

@petermikitsh in theory you can define your own bundles too, and add toast to the same bundle as the "app-root" once

manucorporat avatar Jul 30 '19 13:07 manucorporat

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, ...
}

kraftwer1 avatar Sep 04 '19 14:09 kraftwer1

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.

mobregozo avatar Nov 05 '19 17:11 mobregozo

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?

beck24 avatar Nov 21 '19 08:11 beck24

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.

beck24 avatar Nov 23 '19 17:11 beck24

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 avatar Nov 24 '19 00:11 simonhaenisch

@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.

beck24 avatar Nov 24 '19 02:11 beck24

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.

manucorporat avatar Nov 24 '19 09:11 manucorporat

bump.

Was it added, or is it on the roadmap right now?

kirillgroshkov avatar Aug 30 '20 23:08 kirillgroshkov

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.

ianldgs avatar Dec 21 '20 13:12 ianldgs

Since this was submitted, we have two new output targets:

Both of these let you bypass the lazy loader:

  • dist-custom-elements-bundle compiles all components into one big file and is intended to be consumed by webpack/Rollup/etc.
  • dist-custom-elements compiles each component into separate files and can be imported directly by the browser

Will prefetching still provide value with these new output targets?

claviska avatar Dec 21 '20 14:12 claviska

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.

MarkChrisLevy avatar Dec 21 '20 16:12 MarkChrisLevy

So what is the reccomended approach for this right now? Doing it through service workers maybe?

adrm avatar Feb 19 '21 14:02 adrm

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

ianldgs avatar Feb 19 '21 14:02 ianldgs

@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);
	}
}

simonhaenisch avatar Feb 19 '21 22:02 simonhaenisch

@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
}

kraftwer1 avatar Aug 25 '21 11:08 kraftwer1

Has there been any more discussion around this topic? Or are there any recommended alternative solutions?

allensulzen avatar Aug 09 '23 16:08 allensulzen

👋 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!

rwaskiewicz avatar Nov 29 '23 20:11 rwaskiewicz