0
votes

For a couple of reasons, I'm interested in writing a hybrid application which is partially coded in Java (via Google Web Toolkit) and partially coded in JavaScript. I'm planning on calling the Java library from JavaScript by using GWT Exporter.

The trouble is that this destroys a lot of the opportunities for code optimization and compression. GWT is mostly just designed to optimize the JavaScript it generates, and third-party Javascript compression libraries will probably break down when given GWT output.

Is there a way to tell the GWT compiler "hey, pass these Javascript files into your optimization pass as well"? GWT has a flag for using the Closure Compiler under the hood (which obviously supports optimizing regular javascript), so it feels like this should be possible.

3

3 Answers

0
votes

Insofar as you can ask Closure to compile source, it is possible, but you have to get those 'other' sources into the code that GWT (and later closure) is compiling. Presently, this means putting that code in JSNI, otherwise it is just another file on the filesystem, and the compiler can't know what the dependencies in and out are, since it likewise can't tell when/how you are loading that file.

If I remember correctly, standard usage of dependencies in Closure is via a goog.require() method call in JavaScript at the top of your file - this both declares the dependency, and if needed loads the file. Without this, your base HTML page needs to have <script> tags for each file you might possibly use, and without actually running that page, Closure won't know what order to expect those files to load in, or how far and wide to run when compiling your source.


GWT itself (i.e. outside of Closure) only makes a very small set of optimizations to the raw JS included as JSNI in its Java sources:

  • com.google.gwt.dev.js.JsStaticEval - simple constant folding, and other expression simplification
  • com.google.gwt.dev.js.JsInliner - inline functions under a certain complexity threshold, and other assorted cleanup
  • com.google.gwt.dev.js.JsUnusedFunctionRemover - simple pruning of unreferenced functions/variables
  • com.google.gwt.dev.js.JsDuplicateCaseFolder - look for the same body in more than one case block and merge them into one fall-through case.

GWT has three primary ways to include JS source: JSNI, tags in the .gwt.xml (not supported by all linkers) and com.google.gwt.core.client.ScriptInjector to pull from a string constant or from a remote url. Only the first considers the code as actual source - the second/third let the code come from any source, and don't count on the code being statically available at compile-time. JSNI has its own limitations - it doesn't support with blocks, and must use $wnd and $doc to refer to the host page's window and document.

0
votes

First, although this is not your question, you have to be aware that to use gwt-exporter you have to call GWT.create per each class you want to populate, or call the exportAll() method for exporting everything marked as exportable. That implies that you are saying to the compiler that you are going to use those classes even your JS app would eventually not use them. So you wont take advantage of the removal of unused code causing large js output. You could use code-splitting for separate code in fragments though.

Second, and related with your question, as @Colin says in his answer only code written in JSNI blocks will be optimised by the GWT compiler, but the default optimisation is very trivial, although if you use the closure compiler it is a bit stronger. I have not tried, but I think closure annotations are not allowed since probably GWT compiler removes them before passing the js to the closure compiler.

Anyway, the main problem for including those files in JSNI blocks, is that you have to copy and paste the code in your java classes manually, and then perform some other tricks for addressing $wnd, etc.

We at gwt-query have a JsniBundle generator able to take .jsfiles from filesystem or from any url, and in compile time include that code in JSNI fragments and make a couple of tricks to make it work in the iframe where GWT runs. It works for almost libraries and plugins I have used, but sometimes I had to modify the javascript source for allowing sand-boxing it.

Here you have an example of how to include jquery and highcharts:

public interface JQueryBundle extends JsniBundle {
  @LibrarySource(value = 
    "http://ajax.googleapis.com/ajax/libs/jquery/1.8.1/jquery.min.js")
  public void initJQuery();
}

public static abstract class HighCharts implements JsniBundle {
  @LibrarySource("js/highcharts.src.js")
  public abstract void initHighcharts();

  public void drawChart(String id, JavaScriptObject props) {
    JavaScriptObject $container = JsUtils.runJavascriptFunction(window, "$", "#" + id);
    JsUtils.runJavascriptFunction($container, "highcharts", props);
  }
}

public void testHighCharts() {
  JQueryBundle jQuery = GWT.create(JQueryBundle.class);
  HighCharts highCharts = GWT.create(HighCharts.class);

  jQuery.initJQuery();
  highCharts.initHighcharts();
  highCharts.drawChart("chart", charData);
}

Some of the advantages of using this method are enumerated in this slide of our GWT.create-2013 presentation.

0
votes

You can optimize arbitrary JavaScript code using the GWT compiler, just use the appropriately named LinkerContext.optimizeJavaScript(TreeLogger, String) method. LinkerContext objects are available inside Linkers, which are pieces of custom code run during the compilation. Here's a minimal example on how to write one:

@LinkerOrder(LinkerOrder.Order.POST)
public class ScriptOptimizer extends AbstractLinker {

    @Override
    public String getDescription() {
        return "Optimizes external JavaScript files.";
    }

    @Override
    public ArtifactSet link(TreeLogger logger, LinkerContext context,
            ArtifactSet artifacts) throws UnableToCompleteException {

        // This is some arbitrary JavaScript code you'd probably want to read
        // from a static file in your classpath
        String script = "var foobar = 1; for (var i = foobar; i < 5; i++) alert(1);";

        // Do the optimizations
        script = context.optimizeJavaScript(logger, script);

        // Create an Artifact from the optimized JavaScript string
        ArtifactSet newArtifacts = new ArtifactSet(artifacts);
        newArtifacts.add(emitString(logger, script, "example.js"));

        return newArtifacts;
    }

}

Then, to include the Linker in your GWT compilation process, add this to your *.gwt.xml:

<define-linker name="scriptoptimizer"
    class="com.example.ScriptOptimizer" />
<add-linker name="scriptoptimizer" />

The result will be a compiled file called example.js. You can of course generate as many files as you want or, for optimal results, concatenate all your scripts into one and compile that into a single output file.