Thursday, August 18, 2016

How to use closure compiler advanced optimizations to build a chrome extension

Running the source code for a chrome extension through the closure compiler with advanced optimization is easier than you might think, and I will demonstrate below.


Background

Chrome's extension model is a bit like client-server.  You can have independent pages for UI (client), which communicate with a background page (the server), for business logic, persistence, and global event handling and awareness.  The difference is that while client and server typically communicate using some form of network protocol (e.g. http), the chrome extension UI components can access the background page more-or-less directly using JavaScript.

The closure compiler provides an ADVANCED_OPTIMIZATIONS mode that minifies not only local variables, but also members as well, plus it does inlining and dead code removal, all great stuff that we want it to do for us!  The problems with using the closure compilier on the chrome extension environment stem from the fact that the client UI pages and the (server) background page are all different contexts, yet need to share name minifications.


My code before I was using the Closure Compiler

My background page hooks a number of event handlers, such as those of chrome.windows, chrome.bookmarks, and chrome.tabs (which I will not show).  It also provides an API for my other pages, for the purposes of this example, I will show that in regards to my options page.

background.js:

const _options = {};

const optionsPageInterface = {
getOption ( name ) {
return _options [ name ];
},
setOption ( name, value ) {
_options [ name ] = value;
}
};

options.js:

var opi = chrome.extension.getBackgroundPage().optionsPageInterface;

opi.setOption ( "some-option", 100 );
var someOptionValue = opi.getOption ( "some-option" );
alert ( someOptionValue );

We can see that the options page uses chrome.runtime.getBackgroundPage() to access the background page, and then selects the global variable optionsPageInterface, using a member expression (the .optionsPageInterface part) then stores that value in its own global variable (opi) for later use.

To use the closure compiler, first, we need it to generate separate files for background and options pages.  We'll use the closure compiler's --module option.  We also want it to forgo minification of chrome.* APIs, which we do with the --externs option.  (see closure-compiler chrome extensions declarations)  Here's my command line:

java -jar bin\compiler.jar
--compilation_level ADVANCED_OPTIMIZATIONS
--assume_function_wrapper
--formatting PRETTY_PRINT
--externs chrome_extensions.js
--property_renaming_report propertyRenamingReport.txt
--variable_renaming_report variableRenamingReport.txt
src\background.js --module background:1: 
src\options.js --module options:1:background

Sadly, the result with advanced optimizations enabled is an empty background.js and all that's in options.js is the following:

var a = chrome.extension.getBackgroundPage().a;
a.c("some-option", 100);
var b = a.b("some-option");
alert(b);

Obviously, with the background page having been stripped, that's not going to run.  We need to tell the closure compiler that there is a linkage between the background page and the options page, as we want the closure compiler to keep the background functions used by the options page, and also to minify optionsPageInterfacegetOption, and setOption  to the same value for both the background and options pages.

The closure compiler documentation recommends using exports via string literals as those are never modified.  However, this suggests switching from simple dot-property notation to subscript-property notation with string literals, and also forgoing minification of property names across the chrome extension client-server boundary.  In our case this would mean assigning to:

window [ "optionsPageInterface" ] = { ...

in background.js and consuming in options.js as:

var opi = chrome.extension.getBackgroundPage()[ "optionsPageInterface" ];

This is more restricted than strictly necessary for a chrome extension, if we can get the closure compiler to use the same minified names in the background page as in the client pages, and also if we're willing to live with a restriction that the whole chrome extension is versioned together.


Changes for use with the Closure Compiler

Simply change the declaration of the global interface variable and assignment to it, into a window member assignment instead:

background.js:

const _options = {};

window.optionsPageInterface = { /***was var optionsPageInterface = {***/
getOption ( name ) {
return _options [ name ];
},
setOption ( name, value ) {
_options [ name ] = value;
}
};

For good measure, I also wrap the code in options.js in an anonymous function closure.  It doesn't have any exports, so that keeps all its variables out of the global namespace and from interfering unnecessarily with the global level variables in the background page during the minification process.

options.js:

(function () {
var opi = chrome.extension.getBackgroundPage().optionsPageInterface;

opi.setOption ( "some-option", 100 );
var someOptionValue = opi.getOption ( "some-option" );
alert ( someOptionValue );
})();

Run thru the closure compiler, these files generate the following background.js and options.js:

background.js (minified, with advanced optimizations)

var b = {};
window.a = {b:function(a) {
  return b[a];
}, c:function(a, d) {
  b[a] = d;
}};

options.js (minified, with advanced optimizations)

var c = chrome.extension.getBackgroundPage().a;
c.c("some-option", 100);
alert(c.b("some-option"));

As you can see the closure compiler has minified even the property named  optionsPageInterface and is using the same name on both the client pages and the background page.

I have tested this on a fairly large chrome extension with multiple client UI pages.

Even though components of my chrome extension communicate internally with each other using minified names, that is fine with me because the only consumers of the background page-provided APIs are all versioned together with the provider -- in other words, the background is really providing an internal-only API for the client pages.  The same is also true for uses of sendMessage(), as the closure compiler is able to minify names shared between client UI pages and the background page.

You may likely still need to use subscript-property notation with string literals for any property names / members that escape the extension, for example, query parameters.

I also found -- and fixed in my copy -- a bug of incompleteness in the declaration of chrome.windows.* (from chrome_extensions.js) which made the closure compiler fail to recognize when some property names should not have been minified under advanced optimizations.  See issue closure-compiler issue 1961 at github.