If you want to distribute a small and performant component, Svelte is just great.
It is simple, clean and the generated bundle is extremely small, because Svelte is a compiler, it turns your code into a standalone JavaScript code (there is no framework library to load to have your app running).
Moreover it is extremyly simple to generate a custom element from a Svelte component.
But what if your component turns out to be too big?
Nuclia was facing this problem with its search widget. This widget allows to embed Nuclia’s search feature in in web page or any web application. It is provided as a custom element, and obviously, we want it to be as small as possible, so it does not impact the performances and the user experience of the hosting page.
On the other hand, we also want it to as featureful as possible, for example when previewing a search result, the options we offer (video player, PDF reader, etc.) are pretty rich. Consequently the JavaScript bundle started to grow.
To mitigate this problem, a good solution is to have a slim (or should I say svelte? 😉) main component covering the initial scenario (in our case, allowing to perform a search and get suggestions or results), and just load the rest only when you need it. That’s what is called lazy loading, and it is a well known practice.
Let’s see how to achieve that with Svelte.
Lazy loading in dev mode
First, let’s create our main component, for the purpose of the example, it will a component displaying a counter:
<script lang="ts">
let count: number = 0;
const increment = () => {
count += 1;
};
</script>
<button on:click={increment}>
Clicks: {count}
</button>
To turn it into a custom element, you need to add:
<svelte:options tag="my-counter" />
That is the way you declare to the Svelte compiler you want to obtain a custom element.
Let’s say now you have a big sub component:
<script>
$: console.log('I am a big component');
</script>
<div>Big component</div>
You could use it directly in the Counter component like that:
<script lang="ts">
import Big from './Big.svelte';
let count: number = 0;
const increment = () => {
count += 1;
};
</script>
<button on:click={increment}>
Clicks: {count}
</button>
<Big></Big>
But if you do so, at compile time it will be compile in the same and unique bundle and that’s what you want to avoid.
So let’s create a lazy loader component:
<script lang="ts">
import { onMount } from 'svelte';
let loaded = false;
let component: any;
onMount(() => {
import('./Big.svelte').then((module) => {
component = module.default;
loaded = true;
});
});
</script>
{#if loaded}
<svelte:component this={component} {...$$props} />
{:else}
<slot />
{/if}
It uses the import()
function to load the big component implementation and renders it using svelte:component
.
Imagine you want to load the big component only when the counter reaches 1, you would do:
<svelte:options tag="my-counter" />
<script lang="ts">
import LazyLoader from './LazyLoader.svelte';
let count: number = 0;
const increment = () => {
count += 1;
};
</script>
<button on:click={increment}>
Clicks: {count}
</button>
{#if count > 0}
<LazyLoader>Loading…</LazyLoader>
{:else}
<div>Increase the counter to load the component</div>
{/if}
That’s nice, try it out, you will see in your browser dev tool that Big.component.svelte
is not loaded until you click on the button.
But wait, how will you compile that properly to keep the lazy loading behavior?
Compiling Svelte components for lazy loading
To compile a regular custom element with Svelte, here are the steps:
- you need a file exporting the Counter component,
src/lib.ts
:
export * from "./lib/Counter.svelte";
- and you need a Vite config like this,
vite.lib.config.js
:
import { defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import sveltePreprocess from "svelte-preprocess";
export default defineConfig({
build: {
lib: {
entry: "./src/lib.ts",
name: "MyLibrary",
fileName: "main-component",
},
},
plugins: [
svelte({
include: ["src/lib/Counter.svelte"],
preprocess: sveltePreprocess(),
compilerOptions: {
css: true,
customElement: true,
},
}),
svelte({
include: ["src/lib/*.svelte"],
exclude: ["src/lib/Counter.svelte"],
preprocess: sveltePreprocess(),
compilerOptions: {
css: true,
},
}),
],
});
As you can see, this config will compile the Counter component as a custom element, and the rest as regular Svelte components.
Now you can run the compilation:
vite build -c=vite.lib.config.js
And you get a .umd.js
and a .es.js
.
You can use the UMD in any HTML page like that:
<html>
<head>
<script src="main-component.umd.js"></script>
</head>
<h1>Hello</h1>
<my-counter></my-counter>
</html>
Obviously, that is not good enough to support lazy loading, the LazyLoader
component will try to load something called Big.svelte
which will not there in a non-dev context.
The good thing with Svelte is it’s a compiler, so we can control the compilation process:
<script lang="ts">
import { onMount } from 'svelte';
let loaded = false;
let component: any;
const isProd = import.meta.env.PROD;
onMount(() => {
if (isProd) {
import(/* @vite-ignore */ `/my-big-component.umd.js`).then((module) => {
loaded = true;
});
} else {
import('./Big.svelte').then((module) => {
component = module.default;
loaded = true;
});
}
});
</script>
{#if loaded}
{#if isProd}
<my-big-component {...$$props} />
{:else}
<svelte:component this={component} {...$$props} />
{/if}
{:else}
<slot />
{/if}
Two noticeable things here:
import.meta.env.PROD
allows to know if we are in prod or dev contextimport(/* @vite-ignore */ '/my-big-component.umd.js')
tells to Vite to not try to resolve the module you are importing.
Mow, you have to compile the big component in a secondary bundle that will correspond to this expected my-big-component.umd.js
.
So let’s go:
- you mark the big component as a custom element by inserting:
<svelte:options tag="my-big-component" />
- you export it,
src/big-lib.ts
:
export * from "./lib/Big.svelte";
- you create a new config,
vite.big-lib.config.js
:
import { defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import sveltePreprocess from "svelte-preprocess";
export default defineConfig({
build: {
lib: {
entry: "./src/big-lib.ts",
name: "MyBigLibrary",
fileName: "my-big-component",
},
},
plugins: [
svelte({
include: ["src/lib/Big.svelte"],
preprocess: sveltePreprocess(),
compilerOptions: {
css: true,
customElement: true,
},
}),
],
});
- and finally, you change the main component config to declare
my-big-component.umd.js
as external:
import { defineConfig } from "vite";
import { svelte } from "@sveltejs/vite-plugin-svelte";
import sveltePreprocess from "svelte-preprocess";
// https://vitejs.dev/config/
export default defineConfig({
build: {
lib: {
entry: "./src/lib.ts",
name: "MyLibrary",
fileName: "main-component",
},
rollupOptions: {
external: ["/my-big-component.umd.js"],
},
},
plugins: [
svelte({
include: ["src/lib/Counter.svelte"],
preprocess: sveltePreprocess(),
compilerOptions: {
css: true,
customElement: true,
},
}),
svelte({
include: ["src/lib/*.svelte"],
exclude: ["src/lib/Counter.svelte"],
preprocess: sveltePreprocess(),
compilerOptions: {
css: true,
},
}),
],
});
So now you can compile your two components:
vite build -c=vite.lib.config.js
vite build -c=vite.big-lib.config.js
(Be careful, each call to vite
command will clean up the dist
forlder, so make sure to copy the files you want to keep before running the command again).
The full source code for this example can be found here, and it also shows how to have a shared component used in both the main component and the lazy loaded one.
Curious about how to use Nuclia in your own code or project?
Curious about how to use Nuclia in your own code or project?
Join our Early adopters community to start exploring Nuclia
Join our Discord channel
Let’s build the new search standard!