Tuesday, June 13, 2017

Polymer in Production / Part 2 - Building, bundling, lazy-loading

Continuing from my previous blog post about including web components and Polymer in a huge legacy web application, I want to focus on optimizing the performance of your web app using the Polymer CLI and at least the L part of the PRPL pattern in this post. There are several things you can do to improve the initial load time, even if your app doesn't follow the recommended app shell architecture.


Lazy loading

When I first started playing with web components and Polymer (well over 2 years ago now), I used to create an elements.html file that included all the HTML-imports I needed and included that file in my index.html. The elements.html could be bundled (or vulcanized as it was called back then) to be served as one file with all the imports merged together, which is important for serving from web servers without HTTP/2 support to reduce the number of requests. This works well for small web apps, but as the web app gets bigger and you start building and importing various web components for different parts of the application, the initial load caused by the one huge elements.html import gets unacceptable. And even without bundling, using HTTP/2 for serving, loading all the elements as separate imports at the beginning causes quite a bit of overhead during the first page load.

To work around this issue, the idea proposed by the PRPL Pattern and the App Shell Architecture is to initially only load the bare essentials for displaying the layout and navigation of the web app and then lazily load the other components as they are needed due to user interactions.

While a legacy web app in most cases won't strictly follow an App Shell Architecture you can still use lazy loading to improve the performance. The easiest way to do so is to separate the web-component dependencies for each part of your application following your menu structure. elements.html will only contain the layout elements (if you are using any) and should be imported in your index file. elements-part1.html, elements-part2.html, ... will include all the imports necessary for their respective parts in the web app. You don't have to care about duplicate imports since HTML imports only import elements once by default (but we'll get back to that in a bit when talking about bundling again).

Loading the dependencies when a user navigates to a page that needs them is a simple call to the importHref method provided by Polymer in whatever function you use to handle navigation.

Polymer 1.x:
Polymer 2.x:

Building and bundling

As mentioned above bundling elements together is important when serving from a non HTTP/2 server, e.g. IIS except for the latest version. You can't just bundle each package separately because this will result in duplicate element definitions. While it is designed for pure app shell architectures you can still use the Polymer CLI to help you with this issue.

To get started you will need to create a polymer.json file in the root folder of your source code like this:

entrypoint is your main page. This doesn't necessarily have to be an HTML file. If you are using an index.php or index.asp this will work as well. shell is the import of the core components you will need on page-load, and fragments are the separate imports for each part of your page.

Calling the Polymer CLI with polymer build will analyze your imports, move web-components that appear in more than one fragment to the shell import so they get included only once, and then create bundles for the shell and the fragments. It will also minify all the resources. You can add all the other sources (JavaScript/CSS/images/...) you are using in your web app to sources and they will be minified (if possible) and put in the output folder which defaults to build/default/ together with the element bundles. You then can deploy the web app from there.

If you already have a Node.js-based build-pipeline you can also look into using polymer-build directly. Or you can build your legacy web app and the web-components parts separately and then merge the results.


Polymer 2.x with older browsers

Polymer 2.x follows the Custom Elements v1 Specification, where elements are declared using ES6/ES2015 class syntax. This works in most recent browsers but if you need to support browsers like IE11 you will have to compile the source code to ES5. The Polymer CLI does this for you, using the following setting in polymer.json:

Ideally you should be serving two different versions one compiled to ES5 and one using ES6 based on the user agent, which would result in better performance on modern browsers and a working app on older browsers. But if you are on a static web server, or you have a distaste for user-agent-specific serving, or this is too much work, you will have to make sure to include custom-elements-es5-adapter.js which comes with the webcomponents polyfills, because browers that support custom elements natively will have problem with elements declared using the compiled ES5 code. The Polymer CLI will try to add this script automatically when you have the compile option activated, but depending on your index file/entrypoint, it might not be able to do so (e.g. if it's not an HTML file). So much sure the script is included after building and include it manually if necessary.

More information about this topic can be found in the official documentation.


Summary

Using the methods mentioned in this article I was able to significantly improve the performance of the legacy web app, which suffered a bit at first due to the "one import to load them all" approach. I have since started extracting parts of the web app into their own elements keeping most of the original source code as it is, to benefit from lazy-loading for the legacy parts of the app as well.


References