Building a blog with Svelte: Dynamic imports For Svelte components
Chris Jackson
Posted on February 17, 2021
Dynamic imports are a really powerful JavaScript feature. Loading modules only when needed can significantly speed up the initial load of a single page application.
There is a cost, of course. Loading pages that aren't included in your initial bundle will be slower. But if you pick less popular pages to dynamically import—like account settings—most of your users will never have to pay that cost!
So how do we take advantage of dynamic imports with Svelte?
Enter <svelte:component>
Svelte provides a special element for rendering components dynamically, <svelte:component>
! The example from the official tutorial renders different components based on a user interaction, but the fundamentals are exactly what we need!
So let's set up a quick example with a static import first:
<script>
import About from './Routes/About.svelte'
</script>
<svelte:component this={About}>
Making it dynamic
This example isn't very useful yet. Of course, we could always render our about page using the component directly: <About />
. So let's make it dynamic!
<script>
import { onMount } from 'svelte'
import Error404 from './Routes/Error404.svelte'
let dynamicPage = null
onMount(async () => {
try {
dynamicPage = (await import('./Routes/About.svelte')).default
} catch (e) {
// Handle errors if the dynamic route doesn't load:
dynamicPage = Error404
}
})
</script>
<svelte:component this={dynamicPage}>
Let's break down the changes into smaller pieces, to understand what each change is doing with this dynamic route.
Initial component setup
We're using onMount
to trigger the dynamic import when this component is first rendered. My blog uses page.js as a router, so these dynamic imports are triggered by page transitions, but the logic is the same.
I've also imported an error component that is available in the main bundle, just in case there's an issue with the dynamic import: import Error404 from './Routes/Error404.svelte'
.
let dynamicPage = null
is a little unusual, but <svelte:component>
won't render if the value of this
is falsy
. We'll update that value once we've loaded the page contents, but initially this will stop our component from rendering any output.
You can replace null
with other values, if you'd prefer. undefined
, false
, 0
, or many other values will behave the same as null
. Or you could import a loading component to indicate that this content is waiting on a network request.
Dynamically importing About.svelte
await import('./Routes/About.svelte')
is the expression that dynamically imports the About page, but we have two challenges.
First, await
will throw an exception if the promise rejects, so we need a try/catch
to handle that error. In this case, we're setting dynamicPage
to indicate that an error has happened: dynamicPage = Error404
.
Second, import('./Routes/About.svelte')
resolves to a module object, and <svelte:component>
needs a component constructor. Looking at our static import, import About from './Routes/About.svelte'
, we can see that our component is exported as the default export from its module, once it's been bundled. Our dynamic import can access the default export directly on the resolved module: (await import('./Routes/About.svelte')).default
.
Managing bundles
One challenge that's less obvious with dynamic imports is how your bundler handles components that are imported from these dynamic chunks. With my rollup config, moving to dynamic imported Svelte components created significantly more dynamic chunks than I expected!
That might make sense for your use case, but I wanted my shared components to be included in the main
bundle, rather than dynamically imported. I previously split my node_modules
into a separate bundle, with Rollup's manualChunks
option, so let's update that config.
My shared components live in src/Components/
, so we can use that directory to assign modules to chunks:
// rollup.config.js:
//...
output: {
//...
manualChunks: (moduleName) => {
if (moduleName.includes('node_modules')) {
return 'vendor'
}
if (moduleName.includes('src/Components/')) {
return 'main'
}
},
//...
Putting it all together
Our example dynamic About
page is potentially good enough for your app. It has basic error handling, and we discussed how you'd integrate a loading indicator. But I want to show you an example that's a little bit more complex.
My App.svelte
sets up a router with some static pages and some dynamically rendered posts. The paths for the dynamic posts are stored in a config object, along with some metadata and a loader function that does the dynamic import.
// posts.js
export const posts = [
{
path: '/svelte/dynamic-imports',
loader: () => import('../Routes/DynamicImportsForSvelteComponents.svelte'),
// ...
},
// ...
]
<!-- App.svelte -->
<script>
import About from './Routes/About.svelte'
import Error404 from './Routes/404.svelte'
import Home from './Routes/Home.svelte'
import router from 'page'
import { posts } from './Modules/posts'
// Default to showing About:
let page = About
let nextPost = null
// Scroll to top when navigating from the bottom of a post:
router('*', (_, next) => {
window.scrollTo({
top: 0,
left: 0,
})
next()
})
// Set up a dynamic route for each post in the config object:
posts.forEach((post, i) => {
router(post.path, async () => {
// Posts take a `nextPost` prop to link to more content:
nextPost = posts[i + 1]
try {
page = (await post.loader()).default
} catch (err) {
page = Error404
}
})
})
// Set up static routes:
router('/', () => (page = Home))
router('/about', () => (page = About))
router('*', () => (page = Error404))
router.start()
</script>
<svelte:component this={page} {nextPost} />
You can see the dynamic imports in action by visiting a post on my blog. If you open up dev tools, you should see the core bundles loaded on the home page, and a small additional bundle loaded when clicking into any post. They're all around 3kB, because they include the markdown content as well as the Svelte component that renders the post, and they should cache very well as long as the content doesn't change.
Hopefully this was useful for you! If you have questions or comments, you can always find me on twitter!
Posted on February 17, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 26, 2024
November 26, 2024