Faster, smaller, better
Compiling your application together with OpenLayers 3
Tobias Sauerwein - Guillaume Beraudo
Camptocamp
How do you like your OpenLayers?
full build(ol.js)
custom build
compiled with application
How can you use ol3?
1. full build: pre-compiled, e.g. from NPM or a CDN
2. custom-built: compile ol3 with only the modules that you need
3. compiled with application
JavaScript development today
When JavaScript was invented, no-one knew that it would be used
to build complex web-applications, like it is nowadays.
Writing simple JS snippets to validate a form or to basic animations,
is very different from building full-blown applications.
Challenges of writing large JS applications
namespaces/modules?
visibility control?
type checking?
static checking?
testing?
- namespaces? (modules, organizing your code)
- visibility control? (public/protected/private)
- type checking? (duck-typing vs. static-typing,
which properties/methods does that object have)
- static checking? (errors only detected at run time)
- Testing (not covered by this talk)
Google Closure Tools
Google Closure Compiler
Google Closure Library
Google has "some" experiences in building complex JS applications.
Apps like Google Maps or Google Mail are built using the Google Closure
Tools, a set of tools and libraries to ease the development of JS apps.
The two most important are the Closure Compiler and the Closure library.
The Closure Library is a library similar to jQuery which provides UI
widgets and a standard library with useful functions and classes.
We are going to focus on the Closure Compiler.
Google Closure Compiler
compiles JavaScript to better JavaScript
code checks (syntax, variable references)
checks for common pitfalls
static type checking
Transpiler (ES 6 > ES 5)
advanced optimizations (inlining, dead-code)
Most build system for JS apps have some kind of pre-processor
step for minifying JS code. Newer apps are using a transpiler
that converts from ES 6/7 to ES 5, which is understood by most
browsers.
The Closure Compiler is a minifier, actually a quite good one, but
that's not all.
The compiler parses and analyzes the code and does a number of checks
and optimizations:
- syntax check, undeclared variables (similar to ESLint)
- common pitfalls (division by 0, array index out of bound, ...)
See https://developers.google.com/closure/compiler/docs/error-ref
- optimizations: function-inlining, dead-code removal, renaming
variable, function, class names
- static type checking
All these checks help to detect problems that otherwise would only
be detected when running the code.
The optimizations of the compiler produce more efficient and smaller
code.
Dead-code removal + Inlining
goog.provide('app');
app.printHello = function() {
console.log('Hello');
};
app.someUnusedFunction = function() {
console.log('Unused function');
};
app.run = function() {
app.printHello();
};
app.run();
Compiled
(function(){console.log("Hello");})();
Optimization: unused function "someUnusedFunction" is removed,
other functions are inlined.
In this case, removing the unused function was not a huge win.
But when using a big library (like OpenLayer 3), many unused
functions can be removed.
Renaming
goog.provide('app');
function hello(o) {
alert('Hello, ' + o.firstName + ' ' + o.lastName);
}
app.run = function() {
var person = {firstName: 'New', lastName: 'user'};
hello(person);
console.log(person);
};
app.run();
Compiled (pretty_print)
(function(){var a = {a:"New", b:"user"};
alert("Hello, " + a.a + " " + a.b);
console.log(a);
})();
The property names of the object are renamed.
For the difference between `a.name`
vs `a['name']` see the docs .
Exports
<button onclick="app.doSomething()">...</button>
How to prevent that app.doSomething
is renamed or removed?
/**
* @export
*/
app.doSomething = function() {
...
};
As we saw, functions that are not used are removed or can get renamed.
But what if we are writing an API, which provides functions that are
not directly used? Or what if we simply want to call a function from
HTML or an Angular directive?
The annotation `@export` makes sure that functions, properties or
classes are available.
Type annotations
/**
* @constructor
* @param {number} x X.
* @param {number} y Y.
* @param {number=} opt_z Z.
*/
app.Point = function(x, y, opt_z) { ... };
/**
* @param {!app.Point} other An other point.
* @return {number} The distance.
*/
app.Point.prototype.distanceTo = function(other) { ... };
Code compiled with the Closure Compiler can be annotated with
types. In this example we see:
- @constructor: to mark the constructor of a class
- @param: for function parameters (`=` for optional parameters and
`!app.Point` for not null)
- @return
JSDoc Tags: A selection
@const @constructor @enum @export
@extends @final @implements @interface
@nosideeffects @param @private
@protected @return @throws @type ...
Reference
- Tags for classes: @constructor, @implements, @extends, @interface
- Visibility: @private, @protected, @package, @public
- Behavior: @nosideeffects
- ...
Why bother with types?
Isn't JavaScript all about flexibility? Why should I waste time caring
about types? For strongly typed languages like Java or C++ the compiler
needs types for the memory management (e.g. to reserve enough space
for a variable). But the JS interpreter doesn't care.
So, you are not using types for the compiler, but only for you and
your fellow developers that have to work with your code.
Why types?
Understanding code
entries.forEach(function(entry) {
entry.data.validate();
});
What does this code do?
A big part of developing software is spent trying to understand code.
Types can be huge help for that.
Consider the example:
- What does ´validate´ do?
- Where is the function defined?
- There might be multiple implementations!
- Need to know what `entry.data``is.
- Need to know what `entry` is.
- Need to know what `entries` is.
- Where does `entries` come from?
- If a parameters, try to find call sites...
Example from MyPy talk at PyCon 2016:
https://us.pycon.org/2016/schedule/presentation/2266/
https://www.dropbox.com/s/efatwr0pozsargb/PyCon mypy talk 2016.pdf
Why types?
Static type check
var point = new app.Point([0, 0]);
ERR! compile src/main.js:14: WARNING - Function app.Point:
called with 1 argument(s). Function requires at least
2 argument(s) and no more than 3 argument(s).
ERR! compile var p5 = new app.Point([0, 0]);
ERR! compile ^
ERR! compile
ERR! compile src/main.js:14: WARNING - actual parameter 1 of
app.Point does not match formal parameter
ERR! compile found : Array
ERR! compile required: number
ERR! compile var p5 = new app.Point([0, 0])
ERR! compile 0 error(s), 2 warning(s)
ERR! compile 95.5% typed
When type annotations are provided, the Closure Compiler will do
type checks. For example it will check if a function is called
with correct arguments or if an object really has the method that is
called.
It is not required that an application is 100% typed. The compiler
tries to figure out the types if no types are provided
(partial/optional typing).
Why types?
IDE integration, refactorings
Some IDEs read the type annotation, so that the documentation can
be displayed or going to an implementation, getting code completion,
etc is possible.
Externs files
var map = L.map('map').setView([0, 0], 13);
ERR! compile src/main.js:4: ERROR - variable L is undeclared
externs
file
/** @const */
var L = {};
/**
* @param {string} div
* @return {LeafletMap}
*/
L.map = function(div) {};
/** @constructor */
var LeafletMap = function() {};
...
For example when using Leaflet in an application, the compiler would
complain that Leaflet is not defined. To make Leaflet known to the
compiler, an 'externs' file has to be passed to the compiler. This
file documents the interface of the library.
There are externs available for many popular libraries (e.g. for
jQuery, Angular or Bootstrap).
And OpenLayers?
OpenLayers uses the Closure Compiler (and did use some parts of the
Closure Library). This makes it possible to compile your application
together with OpenLayers.
Simple example with ol3
goog.provide('app');
goog.require('ol.Map');
goog.require('ol.View');
goog.require('ol.layer.Tile');
goog.require('ol.source.OSM');
var map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({source: new ol.source.OSM()})
],
view: new ol.View({center: [0, 0], zoom: 4})
});
A namespace `app` is created with `goog.provide` `goog.require` defines
the classes and namespaces that are used. The rest is normal
JavaScript code.
Compiler configuration
{
"lib": [
"node_modules/openlayers/src/**/*.js",
"node_modules/openlayers/build/ol.ext/**/*.js",
"src/**/*.js"
],
"compile": {
"closure_entry_point": "app",
"externs": [
"node_modules/openlayers/externs/bingmaps.js",
...
],
"define": ["ol.ENABLE_DOM=false", "ol.ENABLE_WEBGL=false"],
"compilation_level": "ADVANCED",
"output_wrapper": "(function(){%output%})();",
...
}
}
Compiler configuration file for 'closure_utils':
- `lib` defines the sources files, of OpenLayers and the application
itself.
- `closure_entry_point`: Used to determine which source files are
included in the compilation.
- `externs` from OpenLayers
See https://github.com/openlayers/closure-util/blob/master/compiler-options.txt
Why compile with OpenLayers?
Only pay for what you use (build size)
Easier to extend OpenLayers
Benefit from the advantages of the Closure Compiler (static/type checking, efficient code, ...)
Build sizes
This diagram compares a full ol3 build ("pre-build") with a build that
compiles a simple "Hello World" application with OpenLayers. Version 3.16.0 of ol3 was used.
Resources: How to get started
Future / Alternatives
Support for ES 6 modules in OpenLayers 3
(module bundlers: tree-shaking )
Closure Compiler is a transpiler (ES 6 > ES 5)
TypeScript
This talk
Slides bit.ly/ol3-closure
Find us on GitHub/Twitter
Tobias: @tsauerwein
Guillaume: @gberaudo
Credits for these great photos!