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.

  1. change output.format to 'es'
  2. change output.file to output.dir (e.g. 'public/build')
  3. change the <script> tag in index.html to point to the new entry point /build/main.js, and use type="module"
  4. write some code with dynamic imports
  5. 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).

enter image description here

(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

Tags:

Svelte