How to do code splitting using Svelte without Sapper
Code splitting is actually a fancy name for dynamic imports. Here's how to do it with Rollup (you'll also get killer tree-shaking in the process!).
Reminder on dynamic imports:
// "normal" static ES import
//
// - statically analytisable
// - must be called at top level
// - will be greedily resolved (and most often inlined) by your bundler
//
import Foo from './Foo.svelte'
// dynamic import
//
// - called like a function
// - returns a promise
// - default export is accessible on key `default` of the result
// - will be bundled into its own chunk by your bundler (hence code splitting)
//
import('./Foo.svelte').then(module => {
const cmp = module.default
console.log(module.myNamedExport)
})
Note that dynamic imports are a native ES feature, like normal imports. This means they are supported natively by non-outdated browsers.
Rollup has been supporting "code splitting from dynamic imports" for a while (see docs).
So, if you want code splitting in your project, it's mainly a matter of configuring Rollup so that it chunks dynamic imports (another option would be to resolve and inline them, which would not result in code splitting).
Here are the steps to do this, starting from Svelte's official template.
- change
output.format
to'es'
- change
output.file
tooutput.dir
(e.g.'public/build'
) - change the
<script>
tag inindex.html
to point to the new entry point/build/main.js
, and usetype="module"
- write some code with dynamic imports
- add support for legacy browsers
Rollup config: output.format
and output.dir
Not all output formats available in Rollup can support dynamic imports. Default from the Svelte template, iife
does not, so we need to change.
output.format: 'es'
won't rewrite import
statements in your code. This means we will rely on the browser's native module loader. All browsers supports ES import
or dynamic import(...)
these days, and legacy browsers can be polyfilled.
Another option could be, for example, output.format: 'system'
, for SystemJS, but that would require us from shipping the third-party module loader in addition to our code.
We also need to change output.file
to output.dir
because code splitting will not produce a single bundle.js
file, but multiple chunks. (And you can't write separate files to a single file, obviously...)
So, here's the relevant part of our Rollup config now:
input: 'src/main.js', // not changed
output: {
format: 'es',
dir: 'public/build/',
},
If you run yarn build
(or npm run build
) at this point, you'll see that you application now gets split in multiple .js
files in the `/public/build/ directory.
index.html
We now need to change the <script>
tag in our index.html
(located in `public/index.html, in the Svelte template) to consume this.
<script defer type="module" src="/build/main.js"></script>
First, we need to change the src
from bundle.js
(which was our old output.file
) to the new entry point of our application. Since our entry point in the Rollup config (input
) is src/main.js
, the main entry point of our app will be written to main.js
(configurable with Rollup's entryFileNames
option).
Since our code is now full of ES import
statements (because we're using output.format='esm'
), we also need to change the type of script from script
(the default) to module
by adding the type="module"
attribute to our script tag.
That's it for modern browsers, you now have fully working code splitting support!
Actually split your application
Code splitting support is not enough to get actual code splitting. It just makes it possible. You still need to separate dynamic chunks from the rest (main) of your application.
You do this by writing dynamic imports in your code. For example:
import('./Foo.svelte')
.then(module => module.default)
.then(Foo => { /* do something with Foo */ })
.catch(err => console.error(err))
This will result in Rollup creating a Foo-[hash].js
chunk (configurable with chunkFileNames
option), and possibly another chunk for dependencies of Foo.svelte
that are shared with other components.
In the browser, this file will only get loaded when the import('./Foo.svelte')
statement is encountered in your code (lazy loading).
(Notice, in the waterfall, how Foo
and Cmp
-- a common dep -- get loaded long after the page load, indicated by the vertical red bar.)
Legacy browsers
Edge (before recently becoming Chrome) does not support dynamic imports. Normal ES imports, yes, but dynamic import(...)
no. That's usually why you have to include some polyfill for outdated browsers.
One solution, like in the rollup-starter-code-splitting example, is to use a third party module loader (e.g. SytemJS) in the browser.
Another, probably simpler, solution available these days is to use the dimport
package. It polyfills support for ES imports and dynamic imports as needed by the host browser.
In order to use it, we're replacing our <script>
tag in index.html
with the following:
<script defer type="module" src="https://unpkg.com/dimport?module"
data-main="/build/main.js"></script>
<script defer type="nomodule" src="https://unpkg.com/dimport/nomodule"
data-main="/build/main.js"></script>
And voilà. Full fledged code splitting. (Simpler than you thought, isn't it?)
Complete example
Here's a complete example implementing all the different bits covered in this answer. You might be particularly interested in this commit.
Attention! Please notice that the example lives on the example-code-splitting
branch of the repository, not master
. You'll need to checkout the right branch if you clone the repo!
Example usage:
# install
npx degit rixo/svelte-template-hot#example-code-splitting svelte-app
cd svelte-app
yarn # or npm install
# dev
yarn dev
# build
yarn build
# serve build
yarn start
This repo might be a good place to start https://github.com/Rich-Harris/rollup-svelte-code-splitting