Skip to content
Tillmann Vogt edited this page Dec 1, 2020 · 16 revisions

Deployment

So your program using GHCJS is done? Great! This guide contains some tips for deploying a program. Please note that the instructions are preliminary. In particular keeping track of required externs for closure compiler is not yet done automatically and might change.

Compile for the browser

Normally, programs compiled by GHCJS have code for multiple environments. This way, a program run with node.js can read and write files and start other programs. If you want a program that only needs to run in the browser, compile your jsexe with -DGHCJS_BROWSER to remove all the non-browser code (this only affects executables).

File locations

GHCJS bundles JavaScript sources from packages and the runtime system in the lib.js and lib1.js files. The compiler generates a file list for the files contained in them, in the same order, respectively lib.js.files and lib1.js.files. If you want to load some of the sources in a different way, for example from a content delivery network, just generate a new lib.js/lib1.js without them by collecting the other files.

(TODO: files are run through cpp when they are collected. the file lists should contain the cpp options)

rts.js contains the automatically generated parts for the runtime system. The file is always the same for the same compilation settings (there are some differences if you enable profiling or assertions) so you can choose to load it from a static location where it can be cached.

out.js contains the compiled Haskell code. See out.stats for information about how much code comes from each package / module.

Minification

GHCJS produces pretty-printed output that is optimized for readability rather than for size. Simple minifiers can already reduce the size considerably by just stripping whitespace and comments, but closure compiler can do a far better job. With the ADVANCED_OPTIMIZATIONS compilation level, closure compiler will globally rename your program and strip all unused code. Using closure compiler requires some care, sometimes you need annotations in the code, and you have to tell the compiler about external libraries you use by supplying externs. See the documentation for more details.

If you already have a working node.js installation, an outdated, but convenient closurecompiler is available as a NPM package

$ npm install closurecompiler -g

Since GHCJS programs use the node.js libraries, you need to tell closure to use the externs for them (unless you compiled your program with -DGHCJS_BROWSER). Note, however, that the extern files are outdated (at least buffer.js, see e.g., https://github.com/google/closure-compiler/issues/3263). Use the following command to minify a file:

$ ccjs all.js --compilation_level=ADVANCED_OPTIMIZATIONS --jscomp_off=checkVars --externs=node --externs=all.js.externs > all.min.js

The newest https://www.npmjs.com/package/google-closure-compiler package can be used without installation via

$ npx google-closure-compiler --compilation_level=ADVANCED_OPTIMIZATIONS --jscomp_off=checkVars --externs=all.js.externs all.js > all.min.js

but it doesn't understand --externs=node so you need to manually include every needed extern file from node_modules/google-closure-compiler/contrib/nodejs/ (these are just as outdated as the externs bundled in closurecompiler).

The all.js.externs file is required for ADVANCED_OPTIMIZATION. It makes sure that Closure Compiler does not rename some things that need to stay the same for the GHCJS RTS. Note that while the --externs=node is node specific, the --externs=all.js.externs need to be passed in even if the program was compiled with -DGHCJS_BROWSER.

For debugging, one can generate a source map. This lets the JavaScript engine print the original line numbers for exceptions and show the original locations in the debugger. You can also use source maps in node.js:

$ npm install source-map-support
$ ccjs all.js --compilation_level=ADVANCED_OPTIMIZATIONS --jscomp_off=checkVars --externs=node --externs=all.js.externs --create_source_map=all.js.min.map > all.min.js.tmp
$ echo "//# sourceMappingURL=all.min.js.map" > all.min.js
$ echo "if(typeof require !== 'undefined') require('source-map-support').install();" >> all.min.js
$ cat all.min.js.tmp >> all.min.js
$ node all.min.js

If you experience problems with the minified result, you can use the --debug option to investigate. This still renames everything, but does not minify the file, and the names are still recognizable:

$ ccjs all.js --debug --compilation_level=ADVANCED_OPTIMIZATIONS --jscomp_off=checkVars --externs=node --externs=all.js.externs > all.min.js

Note: If you use other external (not included in the minified javascript file) libraries you'll need to supply more externs. GHCJS currently does not keep track of required externs, this may change in the future.

Globals and modules

The code generated by GHCJS consists of global names. All names are prefixed with h$ to avoid clashing with other JavaScript code. If you want to reduce the number of global names, which can be particularly useful if you use closure compiler for minification, since closure will remove the prefix, you wrap all of the code in a function:

$ echo "(function(global) {" > all.new.js
$ cat all.js >> all.new.js
$ echo "})(typeof global !== 'undefined' ? global : this);" >> all.new.js

The library code depends on the global argument being passed in. You can also use this object to make some of the code available to the outside, for example:

function h$runMainAction() {
  h$main(h$mainZCMainzimain);
}
global['runMainAction'] = h$runMainAction;

If you use the square bracket notation, closure compiler will not rename the property, so you can use global.runMainAction from other (non-minified) scripts.

Another way to obtain the function wrapper is to call Closure Compiler with --isolation_mode=IIFE --assume_function_wrapper.

Compression

After minification you can further reduce the payload size by compressing the program. You get the best performance if you use static compression instead of letting the web server compress on the fly. The following configuration makes nginx automatically serve x.js.gz when x.js is requested:

location ~* \.(html|css|js|xml)$
{
    gzip_static on;
}

Use the zopfli compressor to get better compression than gzip:

$ zopfli -i1000 x.js

This produces x.js.gz. The files are fully compatible with zlib.

Multiple pages / incremental linking

If you have a website with multiple pages that share most of their code, you can use GHCJS' incremental linking feature to have visitors load the bulk of the code only once, and have each page load only the code specific to that page. This can be particularly useful if you have an IDE or REPL style application where all libraries can be preloaded and only the user code is refreshed. This approach is used in codeworld and also in the GHCJS test suite

First, you generate the shared code from a module that depends on all parts of the library that you want included. The shared code will not contain the code for the module itself, but it does contain all dependencies from other modules and packages (including external js-sources):

$ ghcjs -O -o sharedBits -generate-base TestLinkBase TestLinkMain.hs

This compiles TestLinkMain, but instead of linking a full jsexe, it builds a bundle of all the code that the TestLinkBase module depends on.

To use this bundle, run:

$ ghcjs -O -o specificBits -use-base sharedBits.jsexe/out.base.symbs SpecificPageMain.hs

This compiles the SpecificPageMain program, but leaves out everything that is already in sharedBits. To use the result on a page, you need to include in this order:

- sharedBits.jsexe/lib.base.js
- sharedBits.jsexe/rts.js
- sharedBits.jsexe/lib1.base.js
- sharedBits.jsexe/out.base.js
- specificBits.jsexe/lib.js
- specificBits.jsexe/lib1.js
- specificBits.jsexe/out.js

Of course you can still minify the result and combine some of the files. Consult the closure compiler documentation for minifying a program that spans several files.