kit
kit copied to clipboard
option to render layout before page is loaded
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.
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.
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.
+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?
that sounds unrelated. the animation slows this down because the page fades out before being destroyed (which you notice by the delayed onDestroy)
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?
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.
@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!
For inspiration: https://reactrouter.com/en/main/guides/deferred
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 withsrc/routes/a/+loading.svelte
etc than a big{#if ...}
block insrc/routes/+layout.svelte
. (You could get an equivalent result by addingsrc/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 fromsrc/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 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.
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
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
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
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 😅
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
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"?
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
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
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 thesrc/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, butsrc/routes/a/b/+loading.svelte
would apply - From there to
/a/c
,/a
is again unchanged, butsrc/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 thoughsrc/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 theload
insrc/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 showsrc/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.
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}
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).
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'.
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.
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 useawait 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
Really love the idea of having that defer option as well for granular control. It would be super awesome
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..
Hi, I am putting two pieces of code where the relevant part of the code is highlighted.
1st is +layout.svelte file: -
2nd is +page.svelte file: -
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.