Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

option to render layout before page is loaded #7213

Closed
JacobZwang opened this issue Oct 10, 2022 · 27 comments · Fixed by #8901
Closed

option to render layout before page is loaded #7213

JacobZwang opened this issue Oct 10, 2022 · 27 comments · Fixed by #8901
Labels
feature / enhancement New feature or request
Milestone

Comments

@JacobZwang
Copy link
Contributor

Describe the problem

When building application-like websites, users expect instant feedback when changing pages, even if the page's content isn't ready yet. It's confusing to a user when they click a button and nothing happens, then a second later the page changes. Loading indicators help but are still not ideal.

Describe the proposed solution

Provide an option that will allow a +layout.svelte file to be rendered before the +page.svelte is ready. For example, say clicking a button opens up a page that is a modal. We place the markup for the modal inside the +layout.svelte file and the content inside the +page.svelte file. As soon as the user clicks, we can render an empty modal. Then once the page is ready, the content will appear. While the page is loading, we can utilize the slot default as a loading page or skeleton UI.

<script>
	// declare that this layout will be rendered to the DOM before the page is loaded
	export const optimistic = true;
</script>

<div class="modal">
	<slot>
		<p>skeleton ui or loading indicator</p>
	</slot>
</div>

Alternatives considered

  • Using #await block so that surrounding markup is not dependent on data: This destroys SSR and sveltekit's entire data model.
  • Having a dedicated +loading.svelte file: Seems like unnecessary complexity when it can be done without it. If for some reason someone requires separate default and placeholder page contents they can always use the $navigating store to switch between them.

Importance

would make my life easier

Additional Information

  • If the +layout.svelte also relies on data we can still render it as soon as its own data is ready.
  • This probably shouldn't affect SSR in any way. When we render on the server, we probably want to wait until the entire page is loaded before responding to the client. If we didn't, it would defeat the purpose of SSR in the first place.
  • As far as I know, this doesn't break any existing functionality.
@JacobZwang JacobZwang changed the title option to render layouts before page ready option to render layout before page is loaded Oct 10, 2022
@dummdidumm
Copy link
Member

The problem is that you can use $page.data anywhere on the page and if you access data from the page in a layout that would break. Something like +loading.svelte seems like a safer, more consistent solution to me.

@JacobZwang
Copy link
Contributor Author

That's true, but your layout must already accommodate this lack of data because your layout will still be rendered when there is an error. If error pages inherit layouts then a loading page must inherit layouts as well. I am not against +loading.svelte but I don't think that it solves the issue you present.

@GuillermoCasanova
Copy link

+1

Not sure if related, but when clicking on links there's a slight lag before the page moves away on my end: I currently have my internal links trigger a page fade out and then a page fade in of the new page (via ready = false onDestroy() ready = true onMount()), but there's still a slight lag on click which makes it feel less snappier. The onDestroy() doesn't get called immediately it seems, only about 200-400mms later?

@dummdidumm
Copy link
Member

that sounds unrelated. the animation slows this down because the page fades out before being destroyed (which you notice by the delayed onDestroy)

@GuillermoCasanova
Copy link

GuillermoCasanova commented Oct 12, 2022

I don't think the animation is an issue here.

I'm attaching a video where there is no animation between pages. What it shows: clicking a link takes a second too long for anything to change for the user, which doesn't feel snappy/performant. Could it be the page.js load() function is running before the page changes and only once data is loaded does it trigger a chance? I'd hope not, as it's best if the page changes or triggers a fade out immediately on click and then shows the new page to the user. This is expected behavior of quick sites/SPAs/html-only sites.

EXAMPLE:
https://user-images.githubusercontent.com/9218601/195445798-580e2b3b-30c6-48b3-b8ad-b2fee3ddf8ed.mp4

And for when I have the animation on: a click on a link doesn't trigger fade out immediately on click, it takes a second too long, like @JacobZwang describes, then triggers the animation and then shows the page.

Seems like if I want something to happen immediately I'd have to cover the page with a separate "loading screen" element, trigger a fade in on click and then fade it out once the other page has loaded?

@dummdidumm
Copy link
Member

The page redirect happens after your load resolved, so if you have something slow in there, it's noticeable. This feature request here would help you mitigate that by having some kind of noticeable page change earlier. For now, if you know it always takes some time, you could do some kind of loading spinner or else using the navigating store from $app/navigation, to show that spinner on that site when a navigation is in progress.

@GuillermoCasanova
Copy link

@dummdidumm Understood. I ended up doing exactly that and working with navigating store and show/hiding an overlay -- worked like a charm for perceived performance.

Ditto that the feature request would be cool to have then for more app-like stuff. Thank you!

@dummdidumm
Copy link
Member

For inspiration: https://reactrouter.com/en/main/guides/deferred

@Rich-Harris
Copy link
Member

I think this needs to be a +loading.svelte file, rather than fallback <slot /> content, for two reasons:

  • Granularity. If I want different loading screens for each of /a, /b and /c it's easier to achieve that with src/routes/a/+loading.svelte etc than a big {#if ...} block in src/routes/+layout.svelte. (You could get an equivalent result by adding src/routes/a/+layout.svelte and only putting <slot>loading</slot> inside it, but that's less clear and more roundabout)
  • SvelteKit needs to know which nodes have loading screens. If you navigate from /a to /b, we can't just remove the slotted content from src/routes/+layout.svelte unless we know that it has fallback content. We could determine that, but it would be finicky, especially when dealing with funky preprocessors. +loading.svelte is unambiguous

@Rich-Harris Rich-Harris added this to the post-1.0 milestone Nov 17, 2022
@JacobZwang
Copy link
Contributor Author

@Rich-Harris Just throwing another idea out there. If we could define default data and then add a store to know when page data is loading, we could show our UI before the data arrived. I believe SvelteKit already has this functionality when using invalidate() to update the data on the page?

<script>
	import { loading } from '$app/stores';

	export let data = {
		users: []
                // or maybe even something like:
                // users: localStorage.getItem('users')
	};
</script>

<h1>Users</h1>
{#each data.users as user}
	<div>
		{user.name}
	</div>
{:else}
	{#if $loading}
		loading users...
	{:else}
		no users found
	{/if}
{/each}

I suppose this could also be solved using a service worker to return default or cached data immediately, then when invalidate is called with some extra identifier, the service worker queries the data?

Semi-related, I don't think SvelteKit has a way of loading "more" data. For instance loading 10 users, then loading 10 more users. Our only option would be to make those requests separately and store their results separately from page data.

@Conduitry Conduitry added the feature / enhancement New feature or request label Dec 11, 2022
@swyxio
Copy link
Contributor

swyxio commented Dec 11, 2022

will point out that nextjs (which people will invariably compare sveltekit to) has loading.js https://app-dir.vercel.app/loading

it appears to be the only major feature we lack in comparison

kingstefan26 added a commit to kingstefan26/mrm-clone-new that referenced this issue Jan 16, 2023
@dummdidumm
Copy link
Member

Bullet points out of a discussion with Rich:

  • we probably want this to be a client-only feature, because if you want it for both the client and the server render, you can just use onMount and {#await ..}
  • updating the url should happen eagerly, but updating the data should happen only after the page is fully loaded. This probably means multiple updates to $page during one navigation
  • afterNavigate is only fired after navigation has truly finished (all +loading.svelte files are "gone")
  • it's probably not a good idea to flash the loading screen immediately, maybe it should be in a ~50ms timeout, which would be configurable

@Rich-Harris
Copy link
Member

which would be configurable

I'm thinking maybe something like export const loadTimeout = 50 in +(layout|page)(.server)?.js files, so that you can have a global or a per-page setting as needed. Though we'd want to consider any naming decisions in the context of #6274

@kyjus25
Copy link

kyjus25 commented Jan 17, 2023

Looks like I made a duplicate discussion on #8573 which can probably be marked as resolved in favor of this. Linking to it for posterity.

Seeing it already actively being talked about is exciting 😅

@swyxio
Copy link
Contributor

swyxio commented Jan 18, 2023

btw to match the UX that React is delivering we shouldn't just expose the "loading screen" for timeouts, we may also want to offer a store or something that tracks the navigation process. hopefully more intuitive than startTransition.

i dont know if its too much to ask but i was wondering if regular <a href links might benefit from this too?

edit: i think what dummdidumm mentions below is correct, so withdrawing this

@dummdidumm
Copy link
Member

What specifically do you want to track in the navigation process? There's $navigating already which tells you if a navigation is currently in progress.

What do you mean by "regular <a href links might benefit from this too"?

@kyjus25
Copy link

kyjus25 commented Jan 18, 2023

I'm thinking maybe something like export const loadTimeout = 50 in +(layout|page)(.server)?.js

I think this needs to be a Boolean. I'm not sure this fixes the blipping problem. If I set my loadTimeout to 50, is it supposed to hang for 50ms, time out and trigger the loading UI, and then the real data happens to load in at 52ms? So if I want optimistic I set the loadTimeout to zero? The longer the timeout, the longer the page hangs unresponsive. The shorter the timeout, the more chance of blip. And it's all arbitrary based on internet speeds.

Having the loadTimeout on layout does introduce a gotcha. Both layout and page must agree to be optimistic in order to get the loading UI. Kind of weird for DX but not so much UX. Thankfully, this is a problem Next would have already solved.

UPDATE: Silly me, the answer was right in my face. Next solved the aforementioned race-condition easily because if Layout is busy showing a loading UI, the page is not even rendered yet. The whole layout gets the loading UI, including the place where the page would be

@Antonio-Bennett
Copy link

This is awesome! Just a question this does not tackle granular control I.e streaming the data correct? Just the whole route/page control of what to show while data is awaited on? I.e a suspense with fallback provided which can be a loading component for specific UI on the page like in Next or as @dummdidumm mentioned it could be like defer in react router and similar to the feature just released in Remix https://github.com/remix-run/remix/releases/tag/remix%401.11.0

@Rich-Harris
Copy link
Member

I think this needs to be a Boolean

I'm not sure I understand — what would the boolean indicate?

I take your point about delaying 50ms only for the data to come in after 52ms. As far as I can see this is unavoidable; the main goal is to prevent loading UI appearing when the data is available instantly (because it's cached, or whatever), rather than to eliminate flashes entirely which is an impossible problem. A default of 50ms prevents loading UI in those cases where data is available more-or-less-immediately while still providing instantaneous feedback in other cases (anything below ~100ms is perceived as instantaneous by humans, with their inefficient wet brains).

Both layout and page must agree to be optimistic in order to get the loading UI.

We haven't really articulated the behaviour in detail yet, and there's a few different approaches we could take, so I'm going to try and sketch it out now.

Suppose you had files like this (I'm omitting the +page.js/+page.server.js/+layout.js/+layout.server.js files to keep things tidy, but obviously no loading states would apply without them so just pretend they're there):

src/routes/
├ a/
│ ├ b/
│ │ ├ +page.svelte
│ │ └ +loading.svelte
│ ├ c/
│ │ ├ +page.svelte
│ │ └ +loading.svelte
│ ├ +page.svelte
│ └ +loading.svelte
├ d/
│ ├ e/
│ │ ├ +page.svelte
│ │ └ +loading.svelte
│ ├ f/
│ │ ├ +page.svelte
│ │ └ +loading.svelte
│ └ +page.svelte
├ +page.svelte
└ +layout.svelte

One possibility is that you'd show the +loading.svelte file at or above any changed node, and only that one:

  • If you navigate from / to /a, the /a node is new, so the src/routes/a/+loading.svelte file would apply
  • If you navigated from there to /a/b, we're still using /a so that would not be rendered, but src/routes/a/b/+loading.svelte would apply
  • From there to /a/c, /a is again unchanged, but src/routes/a/c/+loading.svelte would apply
  • If we go from there to /d/e, the top-most changed node is /d. But that doesn't have a +loading.svelte file, so no loading UI would be shown, even though src/routes/d/e/+loading.svelte exists

Another possibility is that we show loading UI for any invalid node rather than any changed node:

  • If you navigate from /a to /a/b and that invalidates the load in src/routes/a/+layout.js, src/routes/a/+loading.svelte would apply

In other words, we need to decide whether loading UI applies to changed or invalid nodes. Initially I leaned towards changed, since it would be jarring to temporarily replace layout UI that's supposed to persist across a navigation. But perhaps there are situations where it makes sense? Hoping we can collectively figure out the 'right' answer here rather than punting it to configuration.

Another question to resolve is whether we show one loading state per navigation — the +loading.svelte at or above the changed/invalid node — or any applicable loading UI, as in this version:

  • If you go from /a/c to /d/e, nothing happens until /d loads, because we don't have loading UI. But when /d loads, if /d/e has not yet loaded then we could show src/routes/d/e/+loading.svelte .

In the /a/c to /d/e case it probably makes sense, but you could imagine going from / to /products/clothes/children/novelty-hats and seeing a rapid succession of loading states as each of the +layout.js load functions resolve one after the other.

Eager to hear people's thoughts on these two questions in particular.

@Rich-Harris
Copy link
Member

Just a question this does not tackle granular control I.e streaming the data correct? Just the whole route/page control of what to show while data is awaited on?

Correct. One idea we've been pondering — and this is just an idea, it may not happen — is whether we should serialize all non-top-level promises returned from load (top-level promises are already awaited, to make it easier to avoid unnecessary waterfalls):

// +page.js or +page.server.js
export function load() {
  return {
    a: Promise.resolve(1),
    b: {
      c: Promise.resolve(2)
    }
  };
}
<!-- +page.svelte -->
<script>
  export let data;
</script>

<h1>{data.a}</h1>

{#await data.b.c}
  <p>loading...</p>
{:then c}
  <p>resolved: {c}</p>
{/await}

@kyjus25
Copy link

kyjus25 commented Jan 19, 2023

what would the boolean indicate?

Thank you, that clears things up in my head. Boolean in the sense that either the +loading.svelte file is there or it isn't. I am not seeing the point of having a loadTimeout configuration accessible to the user if the timeout depends heavily on internet speeds. If I want my application to hang, rather than setting a large loadTimeout I would just remove the loading.svelte file. Hanging for a long time just to be met with a loading UI seems weird to configure that way. I agree it's necessary to have a load timeout, it's just exposing it to be configurable is what i'm confused about. What would be the use case for wanting to change it?

seeing a rapid succession of loading states as each of the +layout.js load functions resolve one after the other.

Maybe +loading.svelte should behave like a layout and have a <slot /> for child layouts to still be visible? Ideally I'd want a way to maintain my page structure, just substitute in loading skeletons where data will be. Kinda wondering if maybe it becomes less of a full +loading.svelte thing and more of just a Svelte block in the places where the content actually swaps. Food for thought. I think I just described an Await though.

changed or invalid nodes

Taking another bite off SolidStart here, but I would think I would want to show a loading UI only on changed nodes when routing around, but all invalid ones if I specifically call a load refetch (which I don't think exists yet, but like refetchRouteData in SolidStart).

@Rich-Harris
Copy link
Member

It exists — https://kit.svelte.dev/docs/load#invalidation. In fact that's another question: should programmatic invalidation cause loading UI to appear? Intuitively it feels like the answer is 'no' (for example you probably don't want loading UI to appear while you're waiting for an enhanced form action), but it's hard to articulate exactly why it should appear in some cases but not others. All of which makes me wonder if...

maybe it becomes less of a full +loading.svelte thing and more of just a Svelte block

...this really is a desirable feature after all. I'm starting to have doubts. There are too many design questions it throws up where the answer is 'it depends'.

@kyjus25
Copy link

kyjus25 commented Jan 19, 2023

It exists

Heh. You guys continue to impress; I'll definitely be using that.

In my opinion there is definitely merit to having a SPA-like experience with the security of having all of your data processed by the server; I hope these design questions can be answered.

In practice, it doesn't really make sense to substitute the entire page when loading. It's usually only a small section of the page that is waiting; some navigation items, a list, or maybe a table. Funny enough, it almost seems like maybe components should be the ones to have a +loading.svelte and not pages. I'm entirely joking, but conceptually makes more sense.

Personally I think I'm going back to @JacobZwang's original suggestion of just having an export const optimistic = true; page option and the user can await or handle it however they want as long as it's clear that they will be getting a pending promise.

@dummdidumm
Copy link
Member

After starting to implement this in #8582, more and more questions came up whether or not this is the right approach. I'll try to summarize our findings and technical things to consider here:

+loading.svelte is nice when ..

  • easily discoverable in the file tree that this page/section has loading UI
  • you have a page whose loading screen looks completely different than what the resulting page looks like (no code shared)
  • you are using a +page.js load function

+loading.svelte has flaws when ..

  • you only want to show a loading screen for parts of your UI. For example you could show all your tiles with the fast loading data already and only place a loading UI on the details part that takes more time to load (more granular loading)
  • you want to use a +page.server.js load function: It's not possible then to show +loading.svelte while that data is loading. To do that, we would need to stream the __data.json response, which we can't because people can use await parent() between server load functions, so we have to wait for all to resolve. Furthermore, there's cookies etc which can't be set once the response has started streaming, which would be very confusing to explain in the docs ("make sure to synchronously set cookies/headers").

Something like defer might be better

As already discussed in posts above, something like

export function load({ fetch }) {
  return defer({
    waitForThis: await fetch('/foo'),
    dontWaitForThis: fetch('/bar')
  });
}

could be the better API after all.

The only disadvantage is the advantage of +loading.svelte, that the latter is easier to discover in the file tree ("ah this page has loading UI") and cleanly separated which can be a plus if your loading UI is very different (but also a minus if it isn't and you duplicate code between +loading/page.svelte).

The advantages are

  • more fine grained behavior possible
  • we can stream this. While we still need to await all server loads, we only need to await the inner promises of defer returns and can start streaming sooner. This also solves the "can't set cookies after streaming started" problem because all server loads have run by then

@Antonio-Bennett
Copy link

Really love the idea of having that defer option as well for granular control. It would be super awesome

@dmf78
Copy link

dmf78 commented Jan 30, 2023

Something like defer might be better

Yes Sir, it will be better. Right now the lengthy loader in +(page/layout).server is making users experience "unresponsive".. Advantages of defer approach you've listed are great. Remix have a similar solution with defer already implemented.

Using load function with lengthy promises in +(page/layout) is not a problem also providing good UX thanks to Svelte {#await...}.

Example To avoid Promise auto resolving simply put them "deeper" in returning object:
export const load: PageLoad = async () => {
	return {
		loong_promises: {
			bigData: new Promise<string>((resolve) => {
				setTimeout(() => {
					resolve('long awaited data');
				}, 3000);
			})
		}
	};
};

..and handle with {#await...}:

<script lang="ts">
	import type { PageData } from './$types';
	export let data: PageData;
</script>

{#await data.loong_promises.bigData}
	loading.. (or some fancy ui)
{:then bigD}
	loaded: {JSON.stringify(bigD)}
{/await}

IMO +loading.svelte is looking redundant if defer will come on board :) Can't wait to start using it..

@HikaruHokkyokusei
Copy link

HikaruHokkyokusei commented Mar 21, 2023

Hi,
I am putting two pieces of code where the relevant part of the code is highlighted.

1st is +layout.svelte file: -
image

2nd is +page.svelte file: -
image

When the website is opened, the layout starts and shows a loading screen till a WebSocket connection is established. Then, the condition changes and now, the slot takes place which loads the page.svelte.

This page uses a video as a bg., which, for the time being, is 10MB. But the file is not fetched when the loading window is being shown, it is only fetched after the loading component is removed and the page(slot) is loaded.

What happens is that till the video is fetched from the server, there is a black bg. and except for the video, everything can be seen on the page. Is there a way so that page is only shown after the video is fetched?

A bit bad explanation, but I hope I was able to convey what I am trying to achieve.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature / enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

10 participants