[//]: # TOC
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.
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",
)
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.
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",
)
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.
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"],
)
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.
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.
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 returnfalse
.
Enum constant names can be obfuscated or stripped (TODO(goktug): document how). Class names can be obfuscated (TODO(goktug): document how).
Closure compiler flag:
"--define=jre.logging.logLevel=OFF|SEVERE|WARNING|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.
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"],
)
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, in general APT based code generation such as Dagger 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.
Note: An exception to this is AutoValue which specifically understood and optimized by J2CL to generate very efficient code.