1. Prerequisites:

  • node (used here v14.15.0)

2. Project initialisation with Typescript

Init the project (named here svelte-project-starter) using degit:

npx degit sveltejs/template svelte-project-starter

Navigate inside the directory of the newly created project:

cd ./svelte-project-starter

Convert the project to Typescript:

node scripts/setupTypeScript.js

Install the node modules:

npm i

(Optional) Add Prettier config:

It’s useful to first install prettier-plugin-svelte:

npm i -D prettier-plugin-svelte

The .prettierrc file:

{
  "printWidth": 120,
  "singleQuote": true,
  "useTabs": false,
  "tabWidth": 2,
  "semi": false,
  "bracketSpacing": true,
  "trailingComma": "es5",

  "svelteSortOrder": "scripts-markup-styles"
}

The .prettierignore file:

rollup.config.js
tailwind.config.js
package.json
package-lock.json
tslint.json
tsconfig.json
public/build
.github/

You should now be able to succesfuly run the app in development mode at localhost:5000:

npm run dev

3. Add tailwindcss

In order to stay compatible with IE 11, we will use tailwindcss@1.9.x. Tailwind css depends on postcss@8, and we will install autoprefixer & postcss-nesting to make development more pleasurable.

npm i -D tailwindcss@1.9.x autoprefixer postcss-nesting postcss@8

Tailwindcss configuration

The tailwind.config.js file:

const production = !process.env.ROLLUP_WATCH;

module.exports = {
  future: {
    purgeLayersByDefault: true,
    removeDeprecatedGapUtilities: true,
  },
  plugins: [],
  purge: {
    content: [
      './src/**/*.svelte',
      // may also want to include base index.html
    ],
    enabled: production // disable purge in dev
  }
}

Update the Rollup configuration

Add the postcss config in the sveltePreprocess plugin:

The rollup.config.js file:

// ...
export default {
  // ...
  plugins: [
    // ...
    svelte({
      // ...
      preprocess: sveltePreprocess({
        // ...
        sourceMap: !production,
        postcss: {
          plugins: [
            require('tailwindcss'),
            require('autoprefixer'),
            require('postcss-nesting'),
          ],
        },
      })
    })
  ]
}

Import Tailwindcss in the components

Next, we need to import tailwindcss somewhere in our app code.

This is best done in the root component of the app, in order to have access to the CSS classes in every component the app uses.

In the ./src/App.svelte file:

<script lang="ts">
</script>

<style global lang="postcss">
  /* purgecss start ignore */
  @tailwind base;
  @tailwind components;
  /* purgecss end ignore */

  @tailwind utilities;
</style>

<main>
  <h1 class="text-xl text-red-200">Svelte, Typescript, Tailwindcss</h1>
</main>

Test everything works

Now, if you run npm run dev one more time, the compilation should be successful, and the heading should appear as a red text, with the size of 1.25rem.

Test that the CSS purging works in production mode

In development mode, the size of the css bundle exceeds 1M.

However, if everything in the setup is correct, the size in production should be much smaller because every unused CSS class and asset from tailwindcss will be purged from the final build.

For example, at the time of this writing, after I created a production build with npm run build, the size of the files found in ./public/build were:

File Size
bundle.css 3.6K
bundle.js 2.4K

Note: The production build can be served with npm start.

4. Add IE 11 compatibility

Add babel to transpile the code to ES5

Install the required packages:

npm i -D @babel/core @babel/preset-env @babel/plugin-transform-runtime \
         @babel/plugin-syntax-dynamic-import @rollup/plugin-babel

We also need to install core-js for ES6 features in ES5:

npm i -D core-js

Update the Rollup configuration to use babel

The rollup.config.js file:

// ...

import babel from '@rollup/plugin-babel';

// ...

export default {
  // ...
  plugins: [
    // ...

    // for IE 11 compatibility
    babel({
      extensions: ['.js', '.mjs', '.html', '.svelte'],
      babelHelpers: 'runtime',
      exclude: ['node_modules/@babel/**', 'node_modules/core-js/**'],
      presets: [
        [
          '@babel/preset-env',
          {
            targets: '> 0.25%, not dead',
            useBuiltIns: 'usage',
            corejs: 3,
          }
        ]
      ],
      plugins: [
        '@babel/plugin-syntax-dynamic-import',
        [
          '@babel/plugin-transform-runtime',
          {
            useESModules: true
          }
        ]
      ]
    }),
  ]
}

Test everything works

If everything went as intended, if you run npm run dev again, you now should be able to see the app running without errors in a IE11 browser.

How the bundle size was affected

After running npm run build for a production build, the sizes of the generated files are as follows:

File Size
bundle.css 3.6K
bundle.js 46K

Although the bundle.js size increased dramatically, it is still way better than the other popular frameworks like Angular, React or Vue. I guess this is the price we have to pay for having to support IE11.

5. Add routing

Install

For the routing we are going to use the declarative svelte-routing library:

npm i -D svelte-routing

Create some page components

Let’s create some demo pages (Svelte components):

The ./src/page/Home.svelte file:

<h1 class="text-xl">Home Page</h1>

The ./src/page/About.svelte file:

<h1 class="text-xl">About Page</h1>

Define the router viewport and routes

The ./src/App.svelte file will look like this:

<script lang="ts">
import { Router, Link, Route } from 'svelte-routing'
import Home from './page/Home.svelte'
import About from './page/About.svelte'

export let url = ''
</script>

<main>
  <h1 class="text-xl text-red-500">Svelte, Typescript, Tailwindcss</h1>
  <Router ulr={url}>
    <!-- Navigation bar with links to the different pages -->
    <nav>
      <Link to="home">Home</Link>
      <Link to="about">About</Link>
    </nav>

    <!-- Viewport where the routes are linked to the component pages -->
    <div>
      <Route path="home" component={Home} />
      <Route path="about" component={About} />
    </div>
  </Router>
</main>

<!-- The style content remains unchanged -->
<style global lang="postcss">
  // ...
</style>

Update the npm scripts to redirect to index.html on 404

To allow direct navigation by putting the url in the browser (for example trying to navigate directly to http://localhost:5000/home), we need to tell svelte server (sirv) to redirect to index.html when the requested page is not found.

We do this by updating the start script in package.json:

{
  // ...
  "scripts": {
    // ...
    "start": "sirv public --single"
  }
}

Important note: This will only work for serving the project after building it with npm run build.

I don’t have a solution for npm run dev yet, but I will update this article once I’ll find it.

Test everything works

If everything went as intended, if you run npm run build and npm start you should be able to see the Home and About links and to navigate to them.

How the bundle size was affected

File Size
bundle.css 3.6K
bundle.js 64K

6. Add lazy loading (bundle chunking with ES native modules)

Create a component that we will lazy load from one of the previous defined routes:

The component ./src/component/LazyLoaded.svelte:

<section>
  <h1>Lazy loaded component!</h1>
</section>

Update the About page to lazy load the component

Lazy load the component using ES dynamic imports.

The updated ./src/page/About.svelte:

<script lang="ts">
  const LazyLoadedP = import('../component/LazyLoaded.svelte')
    .then(({ default: C }) => C)
</script>

<h1 class="text-xl">About Page</h1>
{#await LazyLoadedP}
  ...Loading lazy loaded component
{:then LazyLoaded}
  <LazyLoaded />
{/await}

Update rollup configuration and index.html

Update the rollup.config.js file as follows:

// ...

export default {
  // ...
  output: [
    // The order counts! Otherwise, we get an error at build time!
    {
      sourcemap: true,
      format: 'esm',
      name: 'app',
      dir: 'public/build'
    },
    {
      sourcemap: true,
      format: 'iife',
      name: 'app',
      file: 'public/build/main.iife.js',

      // this is important, otherwise we get an error:
      // can't use IIFE format with dynamic imports!
      inlineDynamicImports: true,
    },
  ],
}

Update the ./public/index.html file as follows:

<!DOCTYPE html>
<!-- ... -->
<head>
  <!-- ... -->

  <!-- We indicate that the file type is module (it contains ES import statements) -->
  <script defer type="module" src="/build/main.js"></script>

  <!-- the only browsers that will load this file are -->
  <!-- the ones which don't support ES modules -->
  <!-- nomodule indicates to modern browser that this file should not be loaded -->
  <script nomodule src="build/main.iife.js"></script>
</head>

Test everything works

To test everything works as intended, run npm run build and then npm start. When you navigate to the app in a browser that supports ES modules, in the network tab you should see that already the browser loaded two JS files: main.js and another chunk main-[hash].js

Now, keeping the network tab open, go to the About page. In the network tab, another request for the LazyLoaded-[hash].js should appear. This is the JS file for our LazyLoaded.svelte component.

Now, if you open the app in IE 11 everything should work as it previously did. The only difference will be that the file loaded by IE 11 will be called main.iife.js instead of bundle.js. IE 11 will also load the main.js file, but this should not interfere.

How the bundle size was affected

Files loaded at startup (modern browsers):

File Size
bundle.css 3.6K
main.js 79B
main.[hash].js 66K

Module files:

File Size
LazyLoaded.[hash].js 0.9K

IE 11 files:

File Size
bundle.css 3.6K
main.iife.js 67K

7. Conclusion

If you followed the steps, now you should have a completly working project starter in Svelte, with routing, TailwindCSS setup and lazy loaded modules, that also works on IE 11.