June 10, 2019

Making a svelte app compatible with Internet Explorer 11

I usually work with react but recently I had a closer look at one of it’s younger competitors: Svelte. Version 3 just came out and compared to react I especially like the following properties of the framework:

  1. Small bundle size
  2. Write fewer lines of code
  3. Scoped styling by default
  4. Faster execution (no virtual DOM)

So when I was asked to create a very simple, but responsive data viz, I thought: this is the perfect opportunity to test it out. 👉🏼 Here’s the result.

I cloned the Svelte Template and created 3 small components that render into three divs. When I built the project for the first time, I was not disappointed. The javascript bundle had a total size of 23kb! 🙌🏼 That’s smaller than the react library alone.

But there was one problem: The app did not run in Internet Explorer 11. The project did not get compiled to ES5 (and unfortunately that’s the only thing IE11 can handle).

I switched off terser (the code minifier) for a minute to see where the error gets thrown:

function instance($$self, $$props, $$invalidate) {
  let { t, data, colors } = $$props;
  $$self.$set = $$props => {
    if ('t' in $$props) $$invalidate('t', t = $$props.t);
    if ('data' in $$props) $$invalidate('data', data = $$props.data);
    if ('colors' in $$props) $$invalidate('colors', colors = $$props.colors);
  };
  return { t, data, colors };
}

We see, Svelte compiles to code that relies heavily on ES2015 features like arrow functions. So to make this code run in IE11, we need to transpile our bundle. To achieve this, I tried two tools:

1. Bublé

Bublé calls itself «the blazing fast, batteries included ES2015 compiler». What I like about it: it’s zero configs. Secondly, Svelte usually comes with rollup and there’s a neat bublé-integration for rollup: rollup-plugin-buble. Install the npm package with npm install or yarn add.

I was surprised how easy it was to set this up. You just add the following to your rollup config right before the terser (before minification!)

import buble from 'rollup-plugin-buble';

export default {
  …
  plugins: [,
    // compile to good old IE11 compatible ES5
    buble(),
    production && terser()
  ],}

Tada: gone are our arrow functions. New bundle size: 24kb. Just 1kb more than before.

For this project, Bublé did a great job, but in bigger projects, you might want to have a bit more control over how your code gets compiled. The industry standard to do this is our second tool:

2. Babel

Babel is Bublé’s source of inspiration and the Rolls Royce of the ECMAScript compilers. We usually use it in combination with webpack, but it works just as well with rollup. We remove buble again from our config (install the package rollup-plugin-babel) and instead add the following:

import babel from 'rollup-plugin-babel';

export default {
  …
  plugins: [,
    // compile to good old IE11 compatible ES5
    babel({
      extensions: [ '.js', '.mjs', '.html', '.svelte' ],
      runtimeHelpers: true,
      exclude: [ 'node_modules/@babel/**' ],
      presets: [
        [
          '@babel/preset-env',
          {
            targets: '> 0.25%, not dead'
          }
        ]
      ],
      plugins: [
        '@babel/plugin-syntax-dynamic-import',
        [
          '@babel/plugin-transform-runtime',
          {
            useESModules: true
          }
        ]
      ]
    }),
    production && terser()
  ],}  

Phu, that’s much more lines of code. To be honest – I copied them from Sapper’s config (a side-project of Svelte similar to Next.js). But it does the job. No more arrow functions, no more template strings. Bundle size: 27kb. Neat.

So are we done here? Unfortunately not…

Two make stuff run in old browsers we not only need to transpile the modern language features like arrow functions and template strings to old syntax, but we also need to add certain browser features and helpers that were integrated into the Javascript language since the old browsers came out.

Polyfills

When we take a svelte app and compile it, we can find the following lines in the (unminified) result:

const resolved_promise = Promise.resolve();function schedule_update() {
  if (!update_scheduled) {
    update_scheduled = true;
    resolved_promise.then(flush);
  }
}

So even if we ourselves do not use modern functions like Array.find (although I strongly encourage you to do so) that are not implemented in IE11, we still need to at least polyfill Promise.

So let’s also here try two approaches – an easy one and a more advanced one:

1. es6-shim

The easiest way to get most of the ES6 functions we use is by npm installing es6-shim and adding it to the very beginning of our app in main.js:

import 'es6-shim';

This increases our filesize to 75kb. That’s triple of what we’ve started with, but we’ll have to swallow the bitter pill if we want our beloved IE11 visitors to be able to use our app. Read more about which functions this adds to our bundle exactly in the docs.

🚧 A side note: This approach is working pretty fine when your app is a page on its own. But as our charts will be displayed on the web page of swissinfo, there is a whole bunch of other scripts running beside our app. Among others: Google’s ReCaptcha. And somehow our polyfill interfered with it, causing the app to initialize but then throw an error:

es6 shim and google recaptcha

Fortunately, with our second approach, that was not the case:

2. core-js

When you work with babel, the more advanced way to polyfill your app is core-js. Over the years it has gotten pretty smart at figuring out which function that your app is relying on and polyfilling exactly these and nothing else.

To use it, we pass the following two options to @babel/preset-env:

'@babel/preset-env',
{
  targets: ...,
  useBuiltIns: 'usage',
  corejs: 3
}

When we try to build the app now, the polyfills will be added automatically, but we’ll also run into errors like this one: (!) Circular dependency: node_modules/core-js/modules/es.promise.js -> node_modules/core-js/internals/microtask.js -> node_modules/core-js/modules/es.promise.js.

To resolve these, we need to tell babel to ignore core-js – to not transpile it. We add to the exclude property:

exclude: [ 'node_modules/@babel/**', 'node_modules/core-js/**' ]

By the way: Don’t we usually ignore all the node_modules from babel? 🤔 We sure do. But remember that svelte (itself also a node package) was not ES5 compatible? That’s what brought us here in the first place. So the trick is, to only exclude babel itself and the core-js package here.

🎉 Ok, now we’re done. We’re at a bundle size of 61kb, which is still much smaller than any react app I’ve ever built. 😅

Summary

So don’t let backward compatibility get in your way: Transpile your svelte app to ES5 with buble or babel and add polyfills with es6-shim or core-js. ⚡️

If you want to learn more about Svelte, I recommend this tutorial and or follow the creator of the Library, @Rich_Harris who is btw also the creator of Bublé and Rollup. 🤓 (How crazy is that?)

You can find the final code in this repo.


PS: At srf.ch we still get around 16% of your desktop visits from Internet Explorer 11. On an article, this might result in about 5% of all visits (75% of the people visit our content on a smartphone). That’s not a whole lot, but still enough to make sure stuff works more or less fine also in IE11.

Other sites will get other browser shares. To get the perfect balance between backward compatibility and bundle size, make sure you check your visitors’ statistics.


Angelo Zehr

Written by Angelo Zehr, data journalist at SRF Data and teacher.


Further reading