Using Polymer with Closure Compiler - Part 2: Maximizing Renaming

Monday, June 20, 2016 | 5:40 AM

This is the second post in a 3-part series about using Polymer with Closure Compiler

UPDATE: goog.reflect.objectProperty is now available as part of the 20160619 compiler and library releases.


Closure Compiler's ADVANCED mode property renaming and dead code elimination put it in a class all its own. In ADVANCED mode, the compiler performs “whole world” optimizations. Polymer apps can take advantage of these optimizations without losing functionality.

How Closure Compiler Renames Properties

Closure Compiler property renaming occurs in two primary ways. The first is quite straightforward: all properties with the same name are renamed in the same way. This is ideal because it doesn't require any type information to work. All instances of .foo are renamed .a regardless of on which object they are defined. However, if any property with the same name is found on any object in the externs, the compiler cannot rename it with this strategy. The more extern properties included in your compilation, the fewer properties can be renamed with this method.

The second method for property renaming was created to address the shortcomings in the first method. Here, the compiler uses type information to rename properties so that they are unique. This way, the first method can happily rename them as they no longer share the name of an extern property. This method is called type-based renaming and as its name suggests, it can only work with proper type information. It will decline to rename a property if it finds the same property on an object for which it cannot determine type information. The better type information provided, the better this method works.

Finally, for property renaming to work at all, properties must be consistently referenced. Properties accessed using an array style bracket notation (such as foo['bar']) are called quoted properties and they will never be renamed. Properties accessed using a dot notation (such as foo.bar) are called dotted properties and may be renamed. Your code can break if you access the same property using both methods - so make sure you choose one and are consistent.

Renaming Polymer Properties

The Polymer library itself is considered an external library. A well maintained externs file for Polymer is hosted within the compiler repository (and distributed in the npm version). Lifecycle methods (such as created , ready , attached , etc) are externally defined and therefore not renameable. Also, as mentioned in part 1 of this series , declared properties defined as part of Polymer's properties object can never be renamed.

That leaves non-lifecycle standard properties as eligible for renaming - as long as they are not quoted. However, since Polymer's listeners and observers are specified as strings, that breaks the consistent access rule for properties and forces you to quote those properties. There are, however, other options.

Observers and Listeners

A Polymer element declares a property observer like:

Polymer({
  is: 'foo-bar',

  properties: {
    foo: {
      type:String,
      observer: 'fooChanged_'
    }
  }

  /** @private */
  'fooChanged_': function(oldValue, newValue) {}
});

In this case, our fooChanged_ method is a private implementation detail. Renaming it would be ideal. However for that to be possible, we would need to have access to the renamed name of fooChanged_ as a string. Closure Library has a primitive that Closure Compiler understands to help in just this case: goog.reflect.object.

By using goog.reflect.object we can rename the keys of an object literal in the same way that our Polymer element is renamed. After renaming, we can use goog.object.transpose to swap the object keys and values enabling us to easily lookup the name of our now renamed property.

var FooBarElement = Polymer({
  is: 'foo-bar',

  properties: {
    foo: {
      type: String,
      observer: FooBarRenamedProperties['fooChanged_']
    }
  }

  /** @private */
  fooChanged_: function(oldValue, newValue) {}
});

var FooBarRenamedProperties = goog.object.transpose(
  goog.reflect.object(FooBarElement, {
    fooChanged_: 'fooChanged_'
  })
);

We can use the same technique to rename listener methods:

var FooBarElement = Polymer({
  is: 'foo-bar',

  listeners: {
    'tap': FooBarRenamedProperties['tapped_']
  }

  /** @param {!Event} evt */
  tapped_: function(evt) {}
});

var FooBarRenamedProperties = goog.object.transpose(
  goog.reflect.object(FooBarElement, {
    tapped_: 'tapped_'
  })
);

Triggering Property Change Events

Polymer provides three different methods to indicate that a property has changed and data-binding expressions should be re-evaluated: set, notifyPath and notifySplices. All three have one unfortunate thing in common: they require us to specify the property name as a string. This would also break the consistent access rule for properties and once again we need access to the renamed property as a string. While the goog.object.transpose(goog.reflect.object(typeName, {})) technique would also work for this case, it requires us to know the globally accessible type name of the object. In this case, Closure Library has another primitive to help: goog.reflect.objectProperty . This method is very new. As of this writing, goog.reflect.objectProperty has yet to be released in either Closure Compiler or Closure Library (though it should be soon). goog.reflect.objectProperty allows us to call the notification methods with a renamed string.

Polymer({
  is: 'foo-bar',

  baz:'Original Value',

  attached: function() {
    setTimeout((function() {
      this.baz = 'New Value';
      this.notifyPath(
          goog.reflect.objectProperty('baz', this), this.baz);
    }).bind(this), 1000);
  }
});

goog.reflect.objectProperty simply returns the string name (first argument) in uncompiled mode. Its use comes solely as a Closure Compiler primitive where the compiler replaces the entire call with simply a string of the renamed property.

Summary

By reserving Polymer's declared properties for cases where the special functionality offered is actually needed, quite a bit of renaming can be obtained on an element. In addition, use of goog.reflect.object and goog.reflect.objectProperty allows us to rename properties which are required to be used with strings.

However now we find ourselves in a case where all this renaming has broken references in our template data-binding expressions. Time for Part 3: Renaming in Polymer Templates.

5 comments:

Peter StJ said...


I am wondering: how do we structure the polymer element definition in such a way as to allow the compiler to run smoothly as part of a build process.

The default way of creating polymer elements is to inline the script in the html file. I guess it would not be ideal for closure compiler. I have used the approach of linking the script via src of script tag and define everything in standard closure compatible JS. This has the disadvantage of having half the code compiled (via closure compiler) and other half is not compiled (polymer itself, but also the elements available freely like iron and paper elements etc).

I use closure library for some data structures, custom code written in the same style for logic in the app and only wrap it as polymer elements, polymer renamer for the templates but there it is also a bit unclear, should I run each template via the renamer separately or it is better to run the vulcanized version directly with the map from the build etc... I would be really great if those can be described (i.e. how the tools work together and in what succession, how can one combine the default recommended tools and closure ecosystem tools etc).

Thanks

Chad Killingsworth said...

I talk about some of this in post 3. With use of Cripser, it's developer choice whether to use inline scripts or external references. My team is experimenting with non-trivial definitions being an external JS file to limit the length of a component HTML file, but that's for maintainability reasons - not technical build constraints.

As for mixing elements, I'm not using any of the publicly provided Polymer Elements (iron-pages, etc) in my compiler project - just the library myself. However, mixing those components in is going to be tricky unless they are fully compatible with the compiler's ADVANCED optimization mode. Template renaming is currently an all or nothing approach. It should be possible to treat some elements as external and some not, but I haven't personally tried that. I'd to start seeing the polymer element library be made compatible with the compiler natively.

Whether to use Closure-Library or not typically comes down to how widely I want to share the component. If it's internal only for a single app - then absolutely. If I'm going to be sharing the component among any projects not already using Closure-Library - I try to avoid it and use as few dependencies as possible.

Tyler Breisacher said...

Instead of the code snippet where you use goog.object.transpose and goog.reflect.object, would this work?


var FooBarElement = Polymer({
is: 'foo-bar',

properties: {
foo: {
type: String,
observer: goog.reflect.objectProperty('fooChanged_', this),
}
}

/** @private */
fooChanged_: function(oldValue, newValue) {}
});

Tyler Breisacher said...

Ah, no it wouldn't because 'this' is the wrong value at that spot.

Chad Killingsworth said...

For this exact reason I started a discussion on overloading `goog.reflect.objectProperty` so that the 2nd argument could be a string literal. In that case it would be used as a named type.