This post is a quick write-up of a POC where I use the Closure compiler to create lazy loaded Svelte bundles.

Closure Compiler

I have previously written about how to combine Svelte with the Closure compiler. You may want to read this article first if you are new to Closure. You may be interested in my article about Closure and lazy loading as well.

Application

Closure compiled bundles are already very optimized, but in my examples so far I’ve only showed scenarios where the entire application is served as a single bundle. In this experiment I want to show how to split the bundle up into multiple chunks that can be loaded lazily based on application routing.

In my demo application I am using Page.js to define two routes, /friends and /form, where the form page depends on an external form library. For users who only visit the friends page, loading the form dependencies is unnecessary, but with lazy loading we can avoid that. Only users who click the form link will load the form dependencies since the JS code is isolated based on the needs of each page. In addition to the chunks per route there is a common bundle containing dependencies required by both routes.

I have included my Closure config below to show how I am directing the Closure compiler to output the correct chunks:

--compilation_level=ADVANCED_OPTIMIZATIONS --language_out ECMASCRIPT_2015 --language_in ECMASCRIPT_2020 --output_manifest=public/build/manifest.MF --variable_renaming_report=public/build/variable_renaming_report --property_renaming_report=public/build/property_renaming_report --create_source_map=%outname%.map --rewrite_polyfills=false --warning_level=QUIET --rewrite_polyfills=false --jscomp_off=checkVars --package_json_entry_names es2015,module,jsnext:main --module_resolution=node --externs externs.js --js node_modules/svelte/package.json --js node_modules/svelte/index.mjs --js node_modules/svelte/internal/package.json --js node_modules/svelte/internal/index.mjs --js node_modules/page/package.json --js node_modules/page/page.mjs --js src/Main.svelte.js --js src/main.js --js src/mount-util.js --chunk common:9 --js src/friends/Friends.svelte.js --js src/friends/friends-service.js --js src/friends/main.js --chunk friends:3:common --js node_modules/svelte-forms-lib/build/index.mjs --js node_modules/svelte-forms-lib/package.json --js src/form/Form.svelte.js --js src/form/main.js --chunk form:4:common --module_output_path_prefix public/build/

Svelte

There are probably many ways to configure lazy loading in Svelte. I consider the following approach very experimental, but I’ll show the necessary code below:

At the top level I bootstrap a normal Svelte application to create an application shell around a router outlet where the routed content will be inserted.

<script> import page from 'page'; import { onMount } from 'svelte'; let activeRoute = {friends: 'active', form: 'inactive'}; onMount(async () => { page('/', async () => navigate('friends').then(() => {resetNav(); activeRoute.friends = 'active'})); page('/form', async () => navigate('form').then(() => {resetNav(); activeRoute.form = 'active'})); page.start(); }); function resetNav() { for(let k in activeRoute) { activeRoute[k] = 'inactive'; } } </script> <nav> <a class="{activeRoute.friends}" href="/">Friends</a> <a class="{activeRoute.form}" href="/form">Form</a> </nav> <div id="router-outlet"> <div></div> </div> <footer> <strong>Svelte Lazy Loading Sample (Closure Compiler)</strong> </footer>

The routed components are then inserted into the router outlet using the following helper method:

const replaceContainer = function ( Component, options ) { const frag = document.createDocumentFragment(); const component = new Component( Object.assign( {}, options, { target: frag } )); options.target.innerHTML = ''; options.target.appendChild(frag); return component; } function mountComponent(component) { replaceContainer(component, {target: document.querySelector('#router-outlet')}); }

I am using dynamic import() statements to load the chunks based on the above route configuration, but ran into an issue with dynamic imports in Closure. Apparently they are not supported yet, but I have seen a PR that will allow Closure to ignore dynamic import statements. For now, as a workaround I have defined a small global function to hide the import statement from the Closure compiler. Hopefully I will be able to move this code into the compiled part of application code soon.

async function navigate(module) { if(module !== 'friends' && module !== 'form') { throw new Error('Unsupported route'); } await import(`/build/${module}.js`); window[module](); }

The full source is available on Github (Branch: lazy).

I’ve also deployed a version of the app here.