Build for production

When building an app that includes LitElement components, you can use common JavaScript build tools like Rollup or webpack.

We recommend Rollup because it’s designed to work with the standard ES module format.

For a set of sample build configurations using Rollup, see Building with Rollup.

If you’re interested in building with a different tool, or integrating LitElement into an existing build system, see Build requirements.

Building with Rollup

There are many ways to set up Rollup to bundle your project. This section describes a two basic builds:

If you only need to support evergreen browsers, you can use the modern build by itself. If you want to support the widest range of browsers with a single build, you can use the backwards-compatible build.

The last part of this section discusses ways to use the two builds together to provide the faster, smaller modern build to browsers that support it while also supporting older browsers.

The example configurations here use the Shop demo app as an example. You can find all of the configurations described here in a branch of the Shop repo:

Modern browser build

The modern browser build uses the following npm packages:

The Rollup configuration file for this build looks like this: \

import resolve from '@rollup/plugin-node-resolve';
import { terser } from 'rollup-plugin-terser';
import minifyHTML from 'rollup-plugin-minify-html-literals';
import copy from 'rollup-plugin-copy';

// Static assets will vary depending on the application
const copyConfig = {
  targets: [
    { src: 'node_modules/@webcomponents', dest: 'build-modern/node_modules' },
    { src: 'images', dest: 'build-modern' },
    { src: 'data', dest: 'build-modern' },
    { src: 'index.html', dest: 'build-modern' },
  ],
};

// The main JavaScript bundle for modern browsers that support
// JavaScript modules and other ES2015+ features.
const config = {
  input: 'src/components/shop-app.js',
  output: {
    dir: 'build-modern/src/components',
    format: 'es',
  },
  plugins: [
    minifyHTML(),
    copy(copyConfig),
    resolve(),
  ],
  preserveEntrySignatures: false,
};

if (process.env.NODE_ENV !== 'development') {
  config.plugins.push(terser());
}

export default config;

Even simpler starter

The rollup-starter-app repo is a bare-bones starter project for building an app with Rollup. Although the repo doesn’t include LitElement, it includes everything you need to bundle a LitElement app. If you want to use this project as a model for your own project, the most important files to look at are package.json and rollup.config.js.

Note that in addition to the required plugins, rollup-starter-app includes the CommonJS plugin, @rollup/plugin-commonjs. This plugin isn’t required for LitElement, but can be useful if you want to import packages that are only distributed as CommonJS modules.

Universal build

The universal build is compiled to ES5 for older browsers (primarily IE11), and uses System.js as a module system (because older browsers don’t support native JavaScript modules).

The universal build requires all of the packages used by the modern build, plus the following:

Babel:

Polyfills used by Babel:

Rollup plugins:

SystemJS module loader:

If you just want to look at the code, here are the most important parts of the universal build:

The following sections describe aspects of the build.

Babel build and polyfill bundle

Unlike the modern build, this build produces two separate bundles: one for the application code, and one for the polyfills required by Babel. Many Babel configurations produce a single bundle, which includes the polyfills as well as the application code. However, this can cause problems with other polyfills, including the Web Components polyfills. Loading the Babel polyfills separately, prior to loading the Web Components polyfills, allows both sets of polyfills to work correctly. The Babel polyfills are supplied as separate Common JS modules.

Babel configuration

The Babel configuration for this build is fairly small. It tells Babel to use the @babel/preset-env plugin to compile to code compatible with Internet Explorer 11.

import babel from 'rollup-plugin-babel';
...

const babelConfig = {
  babelrc: false,
  ...{
    presets: [
      [
        '@babel/preset-env',
        {
          targets: {
            ie: '11',
          },
        },
      ],
    ],
  },
};

Application bundle

The Rollup configuration for the application bundle looks similar to the configuration for the model build:

const configs = [
  // The main JavaScript bundle for older browsers that don't support
  // JavaScript modules or ES2015+.
  {
    input: ['src/components/shop-app.js'],
    output: {
      dir: 'build-universal/nomodule/src/components',
      format: 'systemjs',
    },
    plugins: [
      minifyHTML(),
      babel(babelConfig),
      resolve(),
      copy(copyConfig),
    ],
    preserveEntrySignatures: false,
  },

There are three main differences from the modern configuration:

Polyfill bundle

The entrypoint for the Babel polyfills bundle is a JavaScript file that imports two Common JS modules, which in turn import a number of smaller modules:

import 'core-js/stable';
import 'regenerator-runtime/runtime';

The Rollup config bundles all of these modules into a single file:

const configs = [

   ... 

  // Babel polyfills for older browsers that don't support ES2015+.
  {
    input: 'src/babel-polyfills-nomodule.js',
    output: {
      file: 'build-universal/nomodule/src/babel-polyfills-nomodule.js',
      format: 'iife',
    },
    plugins: [commonjs({ include: ['node_modules/**'] }), resolve()],
  },
];

Loading it all

The index.html file loads all of the bundles in the correct order:

(The example index.html also includes a small application-specific polyfill for the fetch API.)

With extra material removed, the portion of index.html that loads script looks like this:

<!-- Babel polyfills--need to be loaded _before_ Web 
     Components polyfills -->
<script src="nomodule/src/babel-polyfills-nomodule.js"></script>

<!-- Load Web Components polyfills, if needed. -->
<script src="node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js"></script>

<!-- SystemJS loader -->
<script src="node_modules/systemjs/dist/s.min.js"></script>

<!-- Use SystemJS to load the application bundle -->
<script>
  System.import('./nomodule/src/components/shop-app.js');
</script>

Using multiple builds

In theory, it’s ideal to deliver a modern ES6 bundle to modern browsers and the larger ES5 bundle to older browsers. In practice, this can be challenging. Here are three possible techniques for delivering different builds to different browsers:

These approaches are discussed in the following sections.

Module/nomodule gambit

Since browsers that don’t support modules won’t execute module scripts (<script type="module">), and browsers that support modules won’t load nomodule scripts (<script nomodule>), you can include the legacy bundles using nomodule. In practice, the browsers that support modules also support the other ES6 language features used by LitElement, and don’t require transpilation.

You can see this in practice in the Shop example app. See the index-modnomod.html file. The advantage of this technique is that it doesn’t require any logic on the server side.

The main drawback to this technique is that Edge versions 16–18 support JavaScript modules, but don’t support dynamic imports. That’s not an issue if your application can be packaged in a single bundle, but if you have a larger application that uses dynamic imports and you need to support older versions of Edge, this technique won’t work for you.

Another drawback is that IE 11 (and possibly some other older browsers) may download (but not execute) <script type="module"> bundles, and Edge 16-18 may download (but not execute) <script nomodule> bundles. This represents a performance penalty on these older browsers.

Client-side feature detection

In this technique, the initial page load includes a small script that runs feature detection code and then imperatively loads one of the bundles.

This is another technique that doesn’t require any server logic. However, it can introduce delays, since the browser won’t start downloading the application bundle until after the initial JavaScript payload has downloaded and executed.

Differential serving

In this technique, the server uses the User-Agent request header to determine which bundle to serve to the browser. This technique (also called “browser sniffing”) has drawbacks. It’s less correct than client-side feature detection, in that it relies on a static list of features supported by each browser. Also, some browsers may send an incorrect User-Agent header. However, it tends to have a performance advantage over the other approaches, since the browser only receives the bundles it needs.

The prpl-server project is a Node.js web server that supports differential serving.

Build requirements

This section describes the requirements for building applications using LitElement. Use this section if you’re creating your own build setup.

LitElement is packaged as a set of ES modules, written in modern JavaScript (ES 2017). These are supported natively in modern browsers (Chrome, Safari, Firefox, and Edge, for example). LitElement also uses bare module specifiers, which are not supported by any browsers yet.

When building an app using LitElement, your build system will need to handle the following:

For older browsers, you’ll also need to load certain polyfills:

Bare module specifiers

LitElement uses bare module specifiers to import modules from the lit-html library, like this:

import {html} from 'lit-html';

Browsers currently only support loading modules from URLs or relative paths, not bare names referring to e.g. an npm package, so the build system needs to handle them: either by transforming the specifier to one that works for ES modules in the browser, or by producing a different type of module as output.

Webpack automatically handles bare module specifiers; for Rollup, you’ll need a plugin (@rollup/plugin-node-resolve).

Why bare module specifiers? Bare module specifiers let you import modules without knowing exactly where the package manager has installed them. A standards proposal called Import maps would let browsers support bare module specifiers. In the meantime, bare import specifiers can easily be transformed as a build step. There are also some polyfills and module loaders that support import maps.

Supporting older browsers

Supporting older browsers (specifically Internet Explorer 11), requires a number of extra steps:

You may need other polyfills depending on your application.

Transpiling to ES5

Rollup, webpack and other build tools have plugins to support transpiling modern JavaScript for older browsers. Babel is the most commonly used transpiler.

Unlike some libraries, LitElement is delivered as a set of ES modules using modern JavaScript. When you build your app, you need to compile LitElement as well as your own code.

If you have a build already set up, it may be configured to ignore the node_modules folder when transpiling. If this is the case, we recommend updating this to transpile LitElement and lit-html. For example, if you’re using the Rollup Babel plugin, you might have a configuration like this to exclude the node_modules folder from transpilation:

exclude: [ 'node_modules/**' ]

You can replace this with a rule to explicitly include folders to transpile:

include: [ 'src/**', 'node_modules/lit-element/**', 'node_modules/lit-html/**']

Babel uses a set of helpers and polyfills for implementing various modern JavaScript features. Babel can package these polyfills in with your application code. However, this causes issues with the Web Components polyfills. To avoid issues, bundle the Babel polyfills separately. See Polyfill bundle for an example of building this bundle with Rollup, and see Loading it all for an example index.html showing the load order.

Why no ES5 build? The LitElement npm package doesn’t include an ES5 build because modern JavaScript is smaller and generally faster. When building an application, you can compile modern JavaScript down to create the exact build (or builds) you need based on the browsers you need to support.

If LitElement included multiple builds, individual elements could end up depending on different builds of LitElement—resulting in multiple versions of the library being shipped down to the browser.

Transforming modules

When producing output for IE11, there are three common output formats:

The IIFE format works fine if all of your code can be bundled into a single file. To use code splitting with older browsers like IE11, you’ll need to produce output in either the AMD or SystemJS module format.

The universal build in the Building with Rollup section uses SystemJS format. You can see an example of loading the main SystemJS module here:

https://github.com/Polymer/shop/blob/rollup-examples-v2/index-universal.html#L109

Polyfills

In addition to any application-specific polyfills, you’ll need to load the Babel polyfills and the Web Components polyfills.

Note that the Babel polyfills should be bundled separately from the application bundle, and loaded before the Web Components polyfills. This is discussed in Babel build and polyfill bundle. To see an example of generating the Babel polyfill bundle in Rollup, see Polyfill bundle. For an example of loading the bundles, see Loading it all.

Optimizations

LitElement projects benefit from the same optimizations as other web projects:

In addition, there are a few smaller optimizations that are more specific to LitElement:

The example builds presented in Building with Rollup include most of these optimizations.

Code minification

There are many options for code minification. Terser works well with the modern JavaScript used by LitElement. For more information, the terser package description is an excellent overview.

Minify template literals

Lit-html templates are defined using template literals, which don’t get processed by standard HTML minifiers. Adding a plugin that minifies template literals can result in a modest decrease in code size. (Unless your templates are very large, this is a small optimization).

Several packages are available to perform this optimization:

Compile out the shady-render module

If you’re building for modern browsers only, you can remove LitElement’s built-in support for shady DOM, the shadow DOM polyfill, saving about 12KB of bundle size.

To do so, configure your build system to replace the shady-render module with the base lit-html module, which provides a generic version of render. This saves about 12KB from your bundle.

For a Rollup build:

  1. Install the alias plugin.

    npm i -D @rollup/plugin-alias
    
  2. Configure the alias plugin to replace references to the shady-render module with references to the main lit-html module.

    alias({
      entries: [{
        find: 'lit-html/lib/shady-render.js',
        replacement: 'node_modules/lit-html/lit-html.js'
      }]
    }),
    

For a webpack build:

TypeScript

The TypeScript language extends JavaScript by adding types and type checking. The TypeScript compiler, tsc, compiles TypeScript into standard JavaScript.

While it’s possible to run the TypeScript compiler as part of the bundling process, we recommend running it separately, to generate an intermediate JavaScript version of your project. Because of issues with the TypeScript compiler’s output for older browsers. We recommend configuring TypeScript to output modern JavaScript (ES2017 target and ES modules).

For example, if you have a tsconfig.json file, you’d include the following options to output modern JavaScript:

{
  "compilerOptions": {
    "target": "es2017",
    "module": "es2015",
    ...

Use this intermediate version as input to your build tools. To support older browsers, use Babel to recompile the intermediate, modern JavaScript version into backward-compatible JavaScript, as described in Transpiling to ES5 and Transforming modules.