An evaluation of polymer-micro as a minimal web component framework
November 2, 2015
Our Basic Web Components project currently creates its components using Google's Polymer framework, but we've been evaluating the use of the smaller polymer-micro core as a replacement for full Polymer. The polymer-micro core appears to be a useful web component framework in its own right, and may provide nearly everything a component library like ours needs.
Is Polymer the best choice for our open project?
We believe that some amount of framework is necessary to create web components. For a very long time, Polymer has been the primary web component framework. We love Polymer! However, we feel that Polymer has grown to the point where writing a Polymer app feels distinctly different from writing a typical HTML app.
Polymer provides numerous helpers that reduce the amount of copy-and-paste boilerplate code required to invoke standard DOM features. In current parlance, it wants to make component code as DRY as possible. For example, Polymer provides a "listeners" key for wiring up event handlers with less code than a direct invocation of the underlying addEventListener(). Polymer's "properties" key similarly simplifies definition of component properties instead of directly defining property getter/setters on the component prototype and marshalling attributes to properties with attributeChanged().
We think Polymer's goal is commendable. If you can afford to train up a team of developers on Polymer's specific way of doing things, your team should be able to crank out web UI code very efficiently.
But as with any higher-level abstraction, these helpers trade off clarity and simplicity for a certain degree of magic and complexity. Each reduction in the amount of component code a developer must write forces an increase in the arcane Polymer-specific knowledge a developer must acquire to write or even read component code. It also hides details that may complicate debugging and maintenance.
For our open source project, those second-order effects reduce the potential pool of project contributors. Our priority is not time-to-market, but rather creating code which is self-evident to our open source users and potential contributors. Although 1 line of Polymer code might do the work of 3 lines of standard web code, if those 3 lines are more understandable to a wider base of developers, we might prefer the longer, clearer version.
Carrying forward the burden of backward compatibility
Another issue we're grappling with is Polymer is very much designed for this era immediately before web components emerge with native support across all mainstream browsers. Polymer wants, quite reasonably, to accommodate browsers that don't yet support web components. At the same time, Polymer also wants to deliver decent performance, notably on Mobile Safari, which at this time does not support native Shadow DOM. Rather than use the full Shadow DOM polyfill, Polymer introduced its own Shady DOM approach for approximating Shadow DOM behavior on older browsers.
Shady DOM is an impressive technical accomplishment. But having written a great deal of Shady DOM code this year, it's our subjective opinion that Shady DOM code feels clunky. Even after months of writing Shady DOM code, wrapping DOM calls with Polymer.dom() still doesn't feel natural. And it's hard to explain to someone why they can't just call appendChild(), but have to call Polymer.dom().appendChild() instead. And while Polymer.dom() is somewhat future-proof, it doesn't feel future-facing. It erodes the original, extremely elegant vision for Polymer and the web components polyfills: to let people to write web components for future web browsers today.
The alternative to Shady DOM today is to use the full Shadow DOM polyfill. That entails slower performance and — given inevitable leaks in the abstraction — a greater potential for mysterious bugs. On the plus side, the full Shadow DOM polyfill lets one write clearer, future-facing code. With all the major browser vendors on board with Shadow DOM v1, the need to download and use the Shadow DOM polyfill on most devices should fade over the course of 2016.
We're also excited about the advent of ES6, with features like arrow functions that let code be more concise. Writing an addEventListener() call is no longer a substantial burden in ES6, or at least not enough to warrant a parallel system for event listener wiring. And using built-in ES6 classes feels better than calling a purpose-focused class factory like Polymer().
Considering polymer-micro instead of full Polymer
It turns out that, underneath all of Polymer's DRY magic, there's a very clean, simple core called polymer-micro. Polymer is helpfully constructed in three layers: full Polymer on top, a smaller polymer-mini below that, then a tiny polymer-micro at the bottom. The documentation describes polymer-micro as "bare-minimum Custom Element sugaring".
Rather than use the full Polymer framework, we've been investigating whether polymer-micro on its own could meet our needs. Building on top of polymer-micro confers a number of advantages over writing our own web component framework:
- Google has already invested an enormous amount of money and resources developing Polymer, including polymer-micro, and will probably continue to do so for the foreseeable future.
- The thousands of people using full Polymer today are also using polymer-micro. That means lots of testing of the polymer-micro core.
- From its commit history, polymer-micro looks fairly stable. A substantial amount of future Polymer work will likely happen in the upper levels.
- In terms of file size, polymer-micro is significantly smaller than full Polymer, although we don't see that as a huge advantage.
- Relying on a small core like polymer-micro makes it easier to migrate to another framework when a better framework comes along.
The polymer-micro layer happens to provide most of the features upon which Basic Web Components depend:
- Custom element registration.
- Lifecycle support.
- Declared properties.
- Attribute deserialization to properties.
- Behaviors.
On the flip side, Basic Web Components use a number of Polymer features which polymer-micro does not provide:
-
Shadow root Instantiation. If you use polymer-micro, you're expected to create a shadow root yourself.
-
Templates. If you want to use the
<template>
element to define initial content of a component's Shadow DOM, you need to manage that yourself. -
Shimming for CSS styles. The full Shadow DOM polyfill requires that CSS be transformed to minimize styles leaking across a custom element boundaries. Full Polymer takes care of that for you, but using polymer-micro directly means that style shimming becomes your concern.
-
Automatic node finding. This lets your component code refer to a sub-element
<button id="foo">
withthis.$.foo
. Complex components need a consistent and easy way to refer to subelements within the local Shadow DOM. Polymer'sthis.$
syntax satisfies those criteria, although we're really torn as to whether that sugar is worth it. It saves keystrokes, but isn't a web-wide convention. It may give an unfamiliar flavor to web component code. -
ready() callback. Many of the Basic Web Components use Polymer's ready callback to initialize themselves. Polymer takes pains to ensure that any Polymer elements inside a component's local Shadow DOM have their own ready callback fired before the outer component's ready callback is fired.
-
CSS mixins. This is Polymer's current answer for visual themes for components. It's based on a not-yet-standard proposal for extensions to CSS. Without full Polymer, you have to invent your own theming architecture.
All the above features are provided at the levels above polymer-micro: either polymer-mini or full Polymer. However, those upper levels bring along a number of features we don't use, or would be happy to drop. Those features include:
- Data binding
- Event listener setup ("listeners" key)
- Annotated event listener setup (in markup)
- Property change callbacks
- Computed properties
- reflectToAttribute
These features all have some appeal, but in our estimation may add more complexity than they're worth to an open source project aiming for a general developer audience.
Lastly, there are a few higher-level Polymer features we have to use, but wish we didn't have to:
-
<dom-module>
. This is used as a wrapper around a<template>
element, but it's hard to fathom why<dom-module>
is necessary. It seems designed to support a use case we don't care about: defining a template in one file, then using it in a component defined in a separate file. Yet by far the most common way to define a Polymer component is to put its template and script definition in the same file. It's unfortunate full Polymer doesn't offer a better way to use a real<template>
directly. (Although a trick does let you accomplish that in an unofficial way; see below.) -
Polymer.dom(). As noted above, this feels awkward, like you're not using the web. It's also confusing to experienced web developers looking at Polymer code for the first time.
Prototyping a minimal component framework on top of polymer-micro
With the above motivation, we considered the question: What is the smallest amount of code that must be added to polymer-micro to create a web component framework that meets our project's needs?
This experiment entailed a fair amount of spelunking in the Polymer codebase. That exploration informed the creation of a little prototype web component framework called polymer-micro-test that uses only polymer-micro as its base. In this prototype framework, we wrote a small amount of code (minimalComponent.js) to implement the 5 numbered features above which we want but are missing in polymer-micro.
We then used the prototype framework to create a couple of sample components, such as a sample test-element component. A live demo of a simple page shows the test-element component in use. By virtue of using the full polyfills, components created in this prototype framework can run in all mainstream browsers.
Overall, the results of this experiment were fairly positive. Looking at each feature in turn:
-
Creating a shadow root yourself is easy. This is only necessary for components with templates (next point).
-
Stamping out a template is easy. The smallest amount of code we could envision for this is for a component to declare a "template" property. This can be used in conjunction with HTML Imports for a fairly clean connection between the script and the template:
<template id="test-element"> ... template goes here ... </template>
<script> Polymer({ is: 'test-element', template: currentImport.querySelector('#test-element') }); </script>
Aside: we really like being able to use a plain
template
to define component content. It turns out that you can actually do this in full Polymer today, although it's something of a trick that depends upon your component defining an undocumented_template
variable. See this gist, which works in full Polymer. -
Shimming CSS styles took a little investigation, but it turns out the full Shadow DOM polyfill exposes its CSS-shimming code as ShadowCSS. The first time this test framework is going to stamp a template, it just invokes ShadowCSS to shim any
<style>
elements found in the template. It then saves the shimmed result for subsequent stamping into the shadow root. -
Automatic node finding. If we conclude we really need this feature, it's not that hard to implement ourselves. Right after the test framework stamps a template, it queries for all the elements in the shadow tree that have an
id
attribute, then adds those elements tothis.$
. This gives us a type of automatic node finding that meets our needs. Polymer's own implementation of the same feature is much more complex. It appears to do a lot of tree-parsing so in preparation for data binding, but since we don't need data binding, we don't need to do that work. -
The ready() method is a bit of a puzzle to us. The Shadow DOM spec already defines two callbacks, createdCallback() and attachedCallback(), that can cover most of what we're currently doing in ready(). One issue is that createdCallback() and attachedCallback() are synchronous, while the Polymer ready() code takes enormous pains to handle asynchronous calls. That is likely necessary to support their asynchronous data binding model. That is, if your component has a sub-component with data bindings, you want all those asynchronous data bindings to settle down first before your top-level component does its own initialization. Since we're not interested in data binding, however, it's not clear whether we need ready(). Our sample element just uses the standard callbacks.
-
CSS mixins. This remains an open question for us. It's hard to imagine what we could do to allow component users to theme our components. At the same time, we're not convinced that the not-yet-standard CSS mixins are going to actually become a standard. The troubled history of vendor-prefixed CSS feature experiments suggests that one company's early interpretation of a hypothetical, future CSS mixin "standard" might significantly complicate things down the road when a real standard is finally established.
This small prototype framework delivers most of the features required by Basic Web Components. The main exception is that it offers no facility for component theming (point #6 above).
Some other notes:
-
Because polymer-micro supports Polymer behaviors (mixins), we were able to implement all of the prototype's features in a behavior. That's quite elegant. It means our sample components can use those features simply by calling the standard Polymer() class factory and listing this prototype behavior in the "behaviors" key. It was a nice surprise that we didn't have to create our own component class factory for that.
-
To take advantage of Polymer's own attribute-to-property marshalling feature, we had to invoke an undocumented internal method in polymer-micro. If more people were building directly on top of polymer-micro, such facilities could probably be promoted to supported features.
-
Taking advantage of existing Polymers facilities (both official and undocumented) and polyfill features like ShadowCSS means that our little prototype framework can be tiny, less than 1K in size. That gets added to the size of polymer-micro, which is currently about 15K uncompressed. Combined, that 16K is a lot smaller than the full Polymer, about 105K uncompressed.
-
Any decrease in framework file size is more than offset by the need to use the full web component polyfills, which are much larger than the "lite" version used with Shady DOM. Still, since we think the need for the full polyfills will drop over the course of 2016, we're not particularly concerned about that.
Conclusions
While this is just an experiment, it's intriguing to consider using polymer-micro as the basis for a minimalist web component framework.
-
A minimalist framework leads to component code which we believe is easier for a general web developer to read.
-
Letting a component developer work at a lower level of abstraction — "closer to the metal" — means they have a greater capacity to diagnose problems when things inevitably go wrong. There's less mystery to clear away, so problems can be understood and fixed, rather than worked around.
Despite these advantages, we're not yet ready to say that we're actually going to use this prototype to create components. As noted above, our goal is to foster a codebase that can be readily comprehensible to a wide audience of web developers. Using a proprietary framework, even a tiny one, impedes that goal. (Basic Web Components traces its ancestry to an earlier component library called QuickUI which never gained critical mass, in part because it was built on a proprietary framework.)
Using polymer-micro as the basis for a proprietary framework would be better than writing a framework from scratch, but every bit of code added on top of polymer-micro runs the risk of producing a framework in its own right — one distinct and unfamiliar to our developer audience.
A minimalist strategy like this would only have meaning to us if it's shared by other people. To that end, we've begun talking with other web component organizations to explore this idea a bit further. We're not sure where that discussion will go, but it's interesting, and might bear fruit in the form of a new, minimalist web component framework. If you'd be interested in participating in that discussion, please ping us at @ComponentK.