Permalink
Branch: master
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
250 lines (190 sloc) 8.71 KB

J2CL Best Practices

[comment]: # TOC

Build Performance

To improve build performance we recommend adding a .bazelrc file in your workspace root, which will be checked into version control and shared with your team and CI. Start by copying the one from J2CL.

Library Naming: use -j2cl suffix.

Instances of j2cl_library should be named the same as the corresponding java_library with a -j2cl suffix. This brings consistency across different project and makes it easier for other developers and macros that generate j2cl_library to predict the names of dependencies.

java_library(
  name = "utils",
  deps = ":mydep",
)

j2cl_library(
  name = "utils-j2cl",
  deps = ":mydep-j2cl",
)

Super Sourcing: writing platform specific code

One of J2CL's greatest benefits is that your Java code runs on many platforms. You may want to have slightly different implementations for code that runs on Android vs code that runs on the web (different best practices or runtime environment differences). Super sourcing refers to the practice of swapping out the implementation of a class that implements the same api but runs differently in another environment.

When super sourcing, don't write stubs that fail at runtime. Instead, write fully compatible super-source replacements that work at runtime and have usage of incompatible apis fail at compile time.

Sample Implementation:

There are many examples for code you may want to super source, for example, network calls, writing to a local database for persistence, or in this case, logging to the console. In the JVM logging writes to stdout, in the browser, it calls console.log.

// Logger.java
// Other examples could be wiring to a local db, or making a network call.
class Logger {
  /** Logs to stdout with a timestamp. */
  public static void log(String s) {
    System.out.println(getTime() + " " + s);
  }

  private static String getTime() {
    return System.currentTimeMillis() + "";
  }
}

The convention is to place super sources in a folder called super-j2cl/ :

// super-j2cl/Logger.java
// Elemental imports omitted.
class Logger {
  /** Logs to stdout with a timestamp. */
  public static void log(String s) { // Maintain same public api.
    Console.log(getTime() + " " + s);
  }

  private static String getTime() {
    return new Date().getTime() + "";
  }
}

Swap out the Java implementation in the build.

java_library(
  name = "logger",
  srcs = [ "Logger.java", ],
  deps = ":mydep",
)

j2cl_library(
  name = "logger-j2cl",
  srcs = [
    "super-j2cl/Logger.java", # swaperoo
  ],
  deps = ":mydep-j2cl",
)

@GwtIncompatible:

The @GwtIncompatible annotation allows you to strip specific elements from your code including classes, methods, and members. It's designed to strip incompatible methods from your code before they are seen by the J2CL compiler. @GwtIncompatible is effective for removing parts of your API that you don't want to be available to J2CL code. It is usually a good idea to document the reason in the annotation e.g.:

@GwtIncompatible("java.lang.Thread is unavailable in web").

In general, it is not recommended to use @GwtIncompatible in application logic to disable specific code paths, for example by disabling an overriding method, since it can lead to confusing differences in behavior as well as causing @GwtIncompatible to propagate further and further through your code. It is better to restructure code and abstract out incompatible parts and super source the code such that it does something meaningful on the web.

Code Size and Runtime Performance

Closure Compiler Flags

One of J2CL's core advantage is tight integration with the Closure Compiler. Code generated by J2CL is fully type-safe JavaScript code that can be compiled by Closure Compiler with advanced optimization flags. In addition to that Closure Compiler also detects J2CL generated code and enables special passes that optimize patterns that are specific to code generated from Java, which all together yields excellent code size and code splitting.

For the best results, use J2CL_OPTIMIZED_DEFS which enables all the advanced optimizations:

load("//build_defs:rules.bzl", "J2CL_OPTIMIZED_DEFS")

js_binary(
    name = "optimized_j2cl_app",
    defs = J2CL_OPTIMIZED_DEFS,
    deps = [":js_lib"],
)

JRE Configuration

There are JRE configuration options which can be tuned to reduce code size and improve runtime performance. By default, these options are set to their most conservative values as to accurately preserve Java semantics and behavior.

JRE Runtime Checks Configuration

Closure compiler flag: --define=jre.checks.checkLevel=NORMAL|OPTIMIZED|MINIMAL

Allows the suppression of certain runtime checks in the Java Standard library which allows for the code for those checks to be completely removed in production.

Group Description Common Exception Types
BOUNDS Checks related to bound checking in collections. IndexOutBoundsException, ArrayIndexOutOfBoundsException
API Checks related to the correct usage of various APIs. IllegalStateException, NoSuchElementException, NullPointerException, IllegalArgumentException, ConcurrentModificationException
NUMERIC Checks related to numeric operations. ArithmeticException
TYPE Checks related to runtime Java type consistency. ClassCastException, ArrayStoreException
CRITICAL Checks for cases where not failing-fast will keep the object in an inconsistent state and/or degrade debugging significantly. Currently disabling these checks is not supported. IllegalArgumentException

Following table summarizes predefined check levels:

Check level BOUNDS API NUMERIC TYPE CRITICAL
NORMAL (default) x x x x x
OPTIMIZED x x
MINIMAL x

Note that this configuration should be set to same value for both debugging and production in order to detect user code that incorrectly relies on specific exceptions to be thrown.

Class Metadata Stripping

Closure compiler flag: --define=jre.classMetadata=SIMPLE|STRIPPED

Allows the compiler to remove metadata associated with java.lang.Class and reduce code size.

Note that stripping class metadata has several implications:

  • Class names are replaced with auto-generated obfuscated names that are not maintained across different runs of the application.
  • Class.isEnum(), Class.isPrimitive(), Class.isInterface() always return false.

Enum constant names can be obfuscated or stripped (TODO(goktug): document how). Class names can be obfuscated (TODO(goktug): document how).

Logging

Closure compiler flag: "--define=jre.logging.logLevel=OFF|SEVERE|WARNNG|INFO|ALL

When using java.util.logging.Logger, you may specify a logging level. You can disable these logging statements (and have them dead-code stripped) in production JavaScript code.

Custom Compile-Time Code Stripping

You can implement your own configuration based stripping with System.getProperty(). In the following example, the compiler will statically evaluate the condition and remove the entire if/else control statement.

if (System.getProperty("some.define") == "YES") {
  ...
} else {
  ...
}
js_binary(
    name = "optimized_j2cl_app",
    defs = ["--define=some.define=YES"] + J2CL_OPTIMIZED_DEFS,
    deps = [":js_lib"],
)

Much Java, such size

When developing in Java, its natural to reach for common libraries such as Guava. However, the use of such libraries should be evaluated with some caution. It can come with a non-trivial increase in code size (and compile time) in your fully optimized application. In general, you may want to be more conservative in pulling in non-critical J2CL dependencies than in regular Java projects. Remember that you are writing code that runs in a web browser!

Additionally, APT based code generation such as Dagger or AutoValue can have surprising impact on compile time and code size if used without awareness of the implications. Generated code is rarely inspected which can lead to hidden code verbosity. These APTs are designed for convenience, without code size in mind.