kaisermann / svelte-loadable

Dynamically load a svelte component
MIT License
320 stars 13 forks source link

SSR stuff #13

Closed CaptainN closed 5 years ago

CaptainN commented 5 years ago

I'm looking to create a package which wraps this one for Meteor to be able to do SSR and hydration. There are a couple of parts to make that work with svelte-loadable:

Server-side: A. A way to preload all the defined loaders before attempting to render on the server, so that promises will resolve synchronously instead of asynchronously. This is probably the same for WebPack (it's how react-loadable works). B. A component to capture the specific loadables used for a given route during SSR.

Client-side: C. Preload the list of loadables we got from the server before hydration to avoid flashes of "loading" or no content.

For A, I'll create a register method that will intercept the loader, and add it to a list. Svelte Loadable may need to accept a resolved component pathname to be used by B.

For B, I think a context could be used for Svelte (similar to how react-loadable does it). It would allow me to create a CaptureProvider svelte component, which would use a context supplied by svelte-loadable, to provides a method to report the loadable import path. This probably requires a resolved import path. React Loadable sets this up with babel plugin, but maybe there's a way to do that with svelte itself, since it's a compiler? How we do that is not important for svelte-loadable, just that it may need to keep track of another property.

For C. It's just a utility method which would receive a list of modules (resolved paths for meteor, or bundle ids for webpack), and load them in a Promise.

I think this can be done in a way that adds very little code or overhead, while retaining the current API perfectly (I'd extend it to accept a resolved path in addition to the loader).

I'll make a PR.

CaptainN commented 5 years ago

So I got pretty far with minimal changes to the codebase. Basically, this is enough of a hook to be able to build everything on top:

  const capture = getContext('svelte-loadable-capture')
  if (typeof capture === 'function') {
    capture(loader)
  }

(I had written a bunch of observations here which were incorrect, based on a simple mistake I made, so I deleted the comments.)

CaptainN commented 5 years ago

I got it working! There are 3 changes to svelte-loadable to make it work:

  1. Make the loading happen before onMount (#12)
  2. Have a hook to be able to capture all the used loadables for a given route in SSR (the above comment).
  3. This one is complex - on the server, use require instead of dynamic imports, to gain synchronous module loading. In order for this to work, we actually need additional information beyond just the standard loader function. I've solved this in my work using a register paradigm, to preprocess the loaders. It looks like this:
<script context="module">
import { register } from 'meteor/npdev:svelte-loadable'

// Loaders must be registered outside of the render tree.
const PageLoader = register({
  loader: () => import('./pages/Page.svelte'),
  resolve: () => require.resolve('./pages/Page.svelte')
})
const HomeLoader = register({
  loader: () => import('./home/Home.svelte'),
  resolve: () => require.resolve('./home/Home.svelte')
})
</script>

<script>
import { Router, Link, Route } from 'svelte-routing'
import { LoadableCapture, Loadable } from 'meteor/npdev:svelte-loadable'
import MainLayout from './layouts/MainLayout.svelte'

export let loadableHandle = {}
export let url = ''
</script>

<LoadableCapture handle={loadableHandle}>
  <Router url="{url}">
    <MainLayout>
      <Route path="/pages/:slug" let:params>
        <Loadable loader={PageLoader} slug={params.slug}>
          <div slot="loading">Loading...</div>
        </Loadable>
      </Route>
      <Route path="/">
        <Loadable loader={HomeLoader} />
      </Route>
    </MainLayout>
  </Router>
</LoadableCapture>

(NOTE: Those resolve functions are what gets us a resolved import URL, and they will be inserted at compile time by a babel plugin, so the user will not have to type all that every time - they'll just have to pass either a loader, or an object with a loader.)

In server code, we have to use the import path and call require directly.

Changing everything to use require is a significant enough change, that I wanted to check with you on how or if you'd like to proceed with that. I can make it so svelte-loadable can accept the object described in the code above or a loadable compatible with the current API both. I'm not sure how we'd detect that we are running in SSR code (Svelte does have different code paths for that, so there's probably a way to determine). It may be that this code is too domain specific - a WebPack or Rollup compatible solution might look completely different from my Meteor solution. Maybe that means we should not incorporate any specific changes to your module.

In my Meteor package, I've copied the loadable.svelte file, and modified it heavily for server side use, since Meteor works that way (actually, your code can be used as is on the client side - which is pretty sweet - I will probably ship it that way). I'm happy to work with you to add these facilities to your project if you are interested in them - otherwise, I'll simply use a modified copy for the server path as I've described.

CaptainN commented 5 years ago

I ended up using a local copy of your svelte component in my package, because I changed the API requirements to get SSR working. I thought I'd share.

Check out my version of svelte-loadable. This is a Meteor package, but hopefully it makes sense. The changes I made to your component are that I added a cache as described in #12, and added the context hook for the capture functionality.

You can see this working in my meteor svelte starter (you'd need meteor installed in order to run it). The change to the way loadables are defined is easy to spot in App.svelte. The main thing is that there's a bit of additional information needed for SSR and hydration (which you can find in /server and /client folders if you are interested), and they need to be captured at definition time, instead of simply contained in the render tree.

kaisermann commented 5 years ago

I'm going to check out your package when I get some free time. I want to thank you again for taking your time to solve this SSR issue. As I said on #12, I have little experience with SSR and any help is extremely welcome!

CaptainN commented 5 years ago

What I'll try to do is finish up the planned modifications I have for the component in my project, then write up some documentation for that, and then see what can be ported back to this repo. I think there may be a set of tools which can at least be incorporated to make it easier to build SSR solutions on top of this. There's no universal way to do it which would work on every platform, but there are probably ways to at least make it easier.

BTW, I ended up using the described cache on the server, instead of using require. It ended up being a much more elegant solution. I think this is one of the things we can add to the core package, with some explanation about how to set it up in module level code.

CaptainN commented 5 years ago

I wrote up a big readme for npdev:svelte-loadable, and I think I see a clean integration point. I wrote it up as a loadable cache, which solves the "flash of loading" problem on re-initialized loadables (like when changing between routes, and then back).

This provides tangible benefit to Svelte Loadable by itself, and is more than simply adding support for SSR - though it's definitely needed for that.

Other things that come along would be the preloadAll method, which is directly required for SSR, but can also be used to simply load the entire application after it's up and running (this is actually common for offline-first solutions with a Service Worker).

Only one addition to Svelte Loadable would be strictly for SSR support, and that's the introduction of the 'svelte-loadable-capture' context, which is actually only 3 lines of code. (I'd add a few additional notes for SSR implementer on other platforms about how to use that to the readme, beyond what I have in the npdev:svelte-loadable readme.)

So everything else I'd add would be useful for the project as a standalone! That's pretty neat.

For a patch I need to do a few things:

That's actually it! Then it's ready to be integrated.

CaptainN commented 5 years ago

BTW, in thinking through all this, I did find a reason to leave the loading kick off to start in onMount - in SSR, for non-preloaded components, putting the load in onMount prevents it from trying to load the component, which is not needed and won't do anything there, and just renders the loading state (which is what will be hydrated anyway, so that's perfect). It would be optimal if there was a way to check if Svelte is doing SSR work (there might be, since it's an entirely different compilation process), and skip the load in that case, and still avoid onMount client side.

I don't think it'll leak memory or anything, it'll just save some cycles on the server to avoid loading. I'll put that back in any SSR PR.

kaisermann commented 5 years ago

Closed by #15