J2CL & the Future

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against GWT 2.13.1 (released 2026-06-19), gwt-maven-plugin 1.3.0 (net.ltgt.gwt.maven), JDK 21 running the compiler with -sourceLevel 17, the J2CL repository release v20260402 (2026-04-02) and j2cl-maven-plugin 0.22.0 (2023-11-13), checked on 2026-10-09. It concerns shared code (Java that is compiled by javac, by the GWT compiler and by J2CL), client code (the part that each of the two compilers turns into JavaScript) and build/tooling (Maven, Bazel, the module XML). Every build, error message, size and browser result in the worked example was produced by running the Bookshelf scenario on this page: one shared module, one application built by gwt-maven-plugin 1.3.0 with GWT 2.13.1 and one built by j2cl-maven-plugin 0.22.0, both loaded in headless Chrome. Statements about projects that were not run (the J2CL Bazel workflow, J2Wasm, the watch goal) come from the project documentation and say so.

GWT 2.13.1 is the toolkit you can ship with today. J2CL is a second, different Java-to-JavaScript compiler from Google, and the org.gwtproject libraries are the part of GWT that was rewritten to run on both. This page explains how the three relate, what is and is not available, and how to keep the code you write today portable.

Two compilers, one language

The GWT compiler and J2CL solve the same problem, running Java in the browser, with different designs.

flowchart LR subgraph G["GWT 2.13.1"] direction LR gsrc["Java sources
+ module XML"] --> gjdt["JDT front end
whole program"] gjdt --> gdb["Deferred binding
generators, permutations"] gdb --> gopt["GWT optimiser
per permutation"] gopt --> gjs["permutation.cache.js
+ nocache.js"] end subgraph J["J2CL"] direction LR jsrc["Java sources
per library"] --> jtr["J2CL transpiler
one library at a time"] jtr --> jclos["Closure-style JavaScript
cached per library"] jclos --> jcc["Closure Compiler
whole application"] jcc --> jjs["optimised bundle"] end

J2CL’s own README describes it as a transpiler from Java to Closure style JavaScript, tightly integrated with the Closure Compiler and built with Bazel. It is the technology behind several large Google applications (the README names Gmail, Inbox, Docs, Slides and Calendar) and says it is used in production inside Google. The same README also says that the workflow for open-source users is not finalised, that Windows is not supported outside WSL, and in its disclaimer section that the project is an alpha developer preview and not an official Google product. Its Wasm backend is described as working but still work in progress.

Table 1. GWT 2.13.1 and J2CL side by side
Aspect GWT 2.13.1 (this section) J2CL

What it is

A Java-to-JavaScript compiler plus a runtime library, widget set, UiBinder, RPC and tooling, all in one SDK

A transpiler from Java to Closure-style JavaScript; the optimisation is done by the Closure Compiler; no widget set and no RPC

Unit of compilation

The whole application, described by a module XML file and compiled in one run into one or more permutations

One library at a time (the Maven plugin hashes sources and dependencies and caches each result), then one Closure Compiler run for the application

Entry point

EntryPoint.onModuleLoad() named by <entry-point>

None. Plain Java classes plus JavaScript that calls them (the sample archetype uses a .native.js file that does new Bookshelf().onModuleLoad())

Deferred binding, generators, linkers

GWT.create, <replace-with>, <generate-with>, user agent and locale permutations

No equivalent in the J2CL documentation. Code generation is done with annotation processors (the README names Dagger and AutoValue among the libraries it supports)

JSNI

Supported (legacy, see JSNI & Overlay Types)

The J2CL documentation uses JsInterop and .native.js files next to the Java class instead of JSNI

JsInterop

Annotations from jsinterop-annotations; exports only with -generateJsInteropExports; @JsAsync and @JsEnum compile but have no effect in 2.13.1

Same annotations, J2CL semantics; exports use goog.export; a few documented differences (see below)

JRE emulation

gwt-user emulation library, Java 11 level collections, streams and more (see JRE Emulation & Shared Code)

Its own jre library; no reflection, no array bounds checks, float computed as double, long as goog.math.Long (J2CL documents these)

Java language level

Up to Java 17 syntax with -sourceLevel 17 since 2.12.0; the roadmap targets Java 21

Whatever the J2CL build in use supports. j2cl-maven-plugin 0.22.0 (J2CL build v20230718-1) rejected var in the lab (see Pitfalls)

Build tool

Maven (gwt-maven-plugin), community Gradle plugins, the Compiler command line

Bazel (Google’s j2cl repository); Maven through j2cl-maven-plugin

UI

Widgets, panels, UiBinder, cell widgets, Editors, ClientBundle, i18n, RPC and RequestFactory

None. Bring Elemental2 and a UI library (the repository has samples such as helloworld and guava)

Status checked 2026-10-09

Stable, latest 2.13.1 (2026-06-19)

Repository active (last push the day this page was checked), releases v20250623, v20250630, v20260402; README says alpha developer preview

The practical consequence is that the two compilers do not replace one another feature by feature. A GWT application that uses widgets, UiBinder, GWT-RPC, ClientBundle, i18n Messages and GWT.create cannot be handed to J2CL; the code that CAN be moved is the plain Java underneath it (model, validation, state machines, event bus, parsing) and the libraries that were rewritten to be independent of gwt-user.

Where J2CL differs from GWT in the language

The J2CL documentation lists the places where Java semantics are deliberately different. They matter when shared code runs on both compilers, because the same method can behave differently.

Table 2. Documented J2CL behaviours to keep in mind for shared code
Topic J2CL behaviour (from its documentation)

Reflection

java.lang.reflect and class introspection are not supported; Class.getEnumConstants and Enum.valueOf are not supported either

Array access

No bounds check on array access

float

Emulated with a JavaScript number, so it has double precision

long

Mapped to goog.math.Long

Boxed Double, Boolean, String

Represented by the JavaScript value itself (unboxed), while Integer stays boxed

String.format

Not emulated, because it needs large localisation code; the J2CL documentation suggests Guava Strings.lenientFormat

java.net.* and similar

Intentionally left out of the emulation

Switch on an enum

Does not trigger class initialisation, unlike javac

JsInterop namespaces

A non-native @JsMethod may not declare a namespace outside its module; JsPackage.GLOBAL is special; subclasses of a class with a @JsConstructor must also have one

GWT 2.13.1 has its own list of differences (long as a composite of doubles, String.split with Java 8 semantics, configurable jre.checks.*), documented in JRE Emulation & Shared Code. Code that is correct on a JVM, correct in GWT and correct in J2CL avoids all of these corners: no reflection, no String.format, no reliance on float rounding or array-bounds exceptions.

What "GWT 3" means today

"GWT 3" was the name given to a planned next major version built on J2CL. Every public source checked for this page agrees on one fact: that release does not exist.

Table 3. What the sources say about GWT 3 (checked 2026-10-09)
Source Statement

Maven Central, org.gwtproject:gwt

Versions published: 2.10.0, 2.10.1, 2.11.0, 2.12.0, 2.12.1, 2.12.2, 2.13.0, 2.13.1. No 3.x

GitHub releases of gwtproject/gwt

Latest 2.13.1 (2026-06-19); milestones 2.14 (open, 23 open and 29 closed issues) and 2.15 (empty)

J2CL README, "J2CL vs. GWT?"

The GWT team decided in early 2015 to build a separate product from scratch; the strategy later turned GWT 3 into an SDK focused on libraries and enterprise tooling rather than a compiler

GWT roadmap page

Names Java 21 language and JRE emulation as the goal of the next releases, and says GWT explores supporting some J2CL-exclusive JsInterop features in its own compiler. It does not announce a GWT 3

org.gwtproject module READMEs

Describe each module as a future-proof port of a com.google.gwt module with no dependency on gwt-user, meant to prepare for GWT 3 and J2CL, and carry a GWT 3 / J2CL compatibility badge

j2cl-maven-plugin Readme

Calls a J2CL-built application a GWT 3 project, and its cache parameter is named gwt3BuildCacheDir

Today "GWT 3" therefore means a direction, not a product:

  • GWT 2.x remains the SDK. Releases 2.10 to 2.13.1 (2022 to 2026) added Java 11 to 17 syntax, Jakarta artifacts, org.gwtproject coordinates and many JRE additions. The roadmap commits to Java 21 parity.

  • J2CL is a separate compiler that Google maintains for its own applications, usable by outsiders through Bazel or the community Maven plugin.

  • The org.gwtproject libraries are the shared part: they compile with GWT 2.9 or later and with J2CL, and are what a "GWT 3" would be assembled from.

  • Shared compatibility work in GWT itself. The 2.9.0 release notes say GWT can compile projects that use jsinterop-base 1.0.0, Elemental2 1.0.0 and jsinterop-annotations 2.0.0 (everything except @JsAsync and @JsEnum), and the 2.10.0 notes mention JRE emulation changes that keep the GWT and J2CL code bases consistent for J2CL’s Wasm work.

An application started today should be written as GWT 2.13.1 code, with the J2CL-friendly rules from the later section applied to whatever is cheap to apply, and without any plan that depends on a "GWT 3" release date.

The org.gwtproject modules

Since the GWT 2.10.0 release the Maven groupId of the SDK is org.gwtproject. A second family of artifacts lives under the same groupId prefix: libraries in the form org.gwtproject.<area>:gwt-<area>, each in its own repository in the gwtproject GitHub organisation. The READMEs I read (core, event, dom, editor, safehtml, http, json, timer, xhr, regexp, aria, layout, animation) describe each module as a port of a com.google.gwt module with no dependency on gwt-user apart from the Java runtime emulation, aimed at GWT 3 and J2CL; the gwt-core README requires GWT 2.9.0 or newer. gwt-core goes further: its repository contains separate GWT 2 and J2CL test modules and its README says mvn clean verify runs the tests on the JVM, with J2CL and with GWT 2.

All of them are still release candidates. The table lists the artifacts that were on Maven Central on 2026-10-09 (module XML names read from the jars).

Table 4. Published org.gwtproject modules (all pre-1.0)
Artifact Latest (Central date) GWT module to inherit

org.gwtproject.core:gwt-core

1.0.0-RC2 (2023-11-04)

org.gwtproject.core.Core

org.gwtproject.event:gwt-event

1.0.0-RC1 (2020-05-22)

org.gwtproject.event.Event

org.gwtproject.event:gwt-event-compat

1.0.0-RC1 (2020-05-22)

org.gwtproject.event.compat.EventCompat

org.gwtproject.event:gwt-event-legacy

1.0.0-RC1 (2020-05-22)

org.gwtproject.event.legacy.EventLegacy

org.gwtproject.event:gwt-event-dom

1.0.0-RC2 (2022-04-16)

org.gwtproject.event.dom.DomEvent

org.gwtproject.event:gwt-logical-event

1.0.0-RC1 (2020-05-22)

org.gwtproject.event.logical.LogicalEvent

org.gwtproject.dom:gwt-dom

1.0.0-RC2 (2022-04-14)

org.gwtproject.dom.DOM (plus org.gwtproject.dom.builder.BuilderDOM, org.gwtproject.canvas.dom.CanvasDOM, org.gwtproject.media.dom.MediaDOM)

org.gwtproject.dom:gwt-dom-style-definitions

1.0.0-RC1 (2020-10-05)

org.gwtproject.dom.style.Style

org.gwtproject.safehtml:gwt-safehtml

1.0.0-RC1 (2020-10-26)

org.gwtproject.safehtml.SafeHtml

org.gwtproject.safehtml:gwt-safecss

1.0.0-RC1 (2020-10-26)

org.gwtproject.safecss.SafeCss

org.gwtproject.editor:gwt-editor and gwt-editor-processor

1.0.0-RC1 (2020-10-05)

org.gwtproject.editor.Editor

org.gwtproject.aria:gwt-aria

1.0.0-RC2 (2022-04-16)

org.gwtproject.aria.Aria

org.gwtproject.animation:gwt-animation

1.0.0-RC2 (2022-04-16)

org.gwtproject.animation.Animation

org.gwtproject.callback:gwt-callback

1.0.0-RC1 (2020-10-05)

org.gwtproject.user.AsyncCallback

org.gwtproject.geolocation:gwt-geolocation

1.0.0-RC1 (2020-10-26)

org.gwtproject.geolocation.Geolocation

org.gwtproject.http:gwt-http

1.0.0-RC3 (2022-02-28)

org.gwtproject.http.HTTP

org.gwtproject.json:gwt-json

1.0.0-RC1 (2020-12-03)

org.gwtproject.json.JSON

org.gwtproject.layout:gwt-layout

1.0.0-RC1 (2020-12-03)

org.gwtproject.layout.Layout

org.gwtproject.place:gwt-places

1.0.0-RC1 (2020-05-22)

org.gwtproject.place.Place

org.gwtproject.regexp:gwt-regexp

1.0.0-RC1 (2020-10-05)

org.gwtproject.regexp.RegExp

org.gwtproject.timer:gwt-timer

1.0.0-RC1 (2020-10-05)

org.gwtproject.timer.Timer

org.gwtproject.typedarrays:gwt-typedarrays

1.0.0-RC2 (2020-10-24)

org.gwtproject.typedarrays.TypedArrays

org.gwtproject.user.history:gwt-history

1.0.0-RC1 (2020-05-22)

org.gwtproject.user.history.History

org.gwtproject.user.window:gwt-window

1.0.0-RC3 (2022-11-19)

org.gwtproject.user.window.Window

org.gwtproject.xhr:gwt-xhr

1.0.0-RC1 (2020-10-26)

org.gwtproject.xhr.XMLHttpRequest

org.gwtproject.xml:gwt-xml

1.0.0-RC1 (2020-10-05)

org.gwtproject.xml.XML

Three facts shape how much to rely on this family:

  • No stable release. There is no 1.0.0 for any of them, and the newest artifact on Central is gwt-core 1.0.0-RC2 from November 2023. The newest commit in any of the module repositories at the time of the check was 2024-12-17 (gwt-editor); gwt-editor has commits after its last Central release.

  • The big UI libraries were not ported. The Central listing has no artifact for widgets, panels, UiBinder, cell widgets, ClientBundle, i18n, GWT-RPC or validation. The only org.gwtproject.web artifacts are the RequestFactory ones (org.gwtproject.web.bindery:requestfactory*), which are part of the 2.13.1 SDK, not J2CL-ready ports.

  • Mixing is allowed. The module XML names do not collide with the com.google.gwt ones, and gwt-event-compat is a bridge: its CompatEventBus wraps a new-style event bus as a com.google.gwt.event.shared.EventBus, so a code base can move to the new event classes while libraries still use the old ones.

The libraries bring Elemental2 with them: the gwt-core 1.0.0-RC2 POM declares elemental2-core and elemental2-dom 1.1.0, and gwt-dom additionally depends on gwt-safehtml, gwt-safecss, gwt-regexp and gwt-dom-style-definitions. An application that wants a newer Elemental2 declares it itself; Maven’s nearest-wins rule then picks the application’s version.

Using a ported module in a GWT 2.13.1 project

The migration recipe in the README of every module is the same three steps, shown here for gwt-event (it is the one used in the worked example):

Replace the bindery event module
<!-- pom.xml -->
<dependency>
  <groupId>org.gwtproject.event</groupId>
  <artifactId>gwt-event</artifactId>
  <version>1.0.0-RC1</version>
</dependency>

<!-- module .gwt.xml: replaces com.google.web.bindery.event.Event -->
<inherits name="org.gwtproject.event.Event"/>
Imports change from com.google.web.bindery.event.shared to org.gwtproject.event.shared
import org.gwtproject.event.shared.Event;
import org.gwtproject.event.shared.EventBus;
import org.gwtproject.event.shared.HandlerRegistration;
import org.gwtproject.event.shared.SimpleEventBus;

Events & Handlers covers the com.google.gwt.event handler model itself, which is what most widget code uses. Because the widget set still depends on that model, the ported event module is most useful for non-UI code: models, services and presenters that fire domain events. Likewise MVP, Activities & Places and Editors use the gwt-user versions; gwt-places and gwt-editor exist as the J2CL-ready alternatives.

Writing code that works with both

The rules below are the ones that kept the Bookshelf core module compiling with javac, GWT 2.13.1 and J2CL.

Table 5. Rules for Java that must compile with both compilers
Rule GWT 2.13.1 J2CL

Keep shared code free of com.google.gwt.* types

Allowed, but then the module is GWT-only

gwt-user classes are not available; any dependency that drags gwt-user in is a problem (see Pitfalls)

No JSNI

Legacy JSNI works

Not supported; use JsInterop plus .native.js

No GWT.create, no generators

Deferred binding works

No equivalent; use constructors, factories, annotation processors

Use JsInterop and Elemental2 for the browser API

Elemental2 1.3.2 is the latest stable and the one the other pages of this section use

The same libraries, but in the lab the Maven plugin needed Elemental2 1.1.0 (see Pitfalls)

Strip server-only code with @GwtIncompatible

Honoured by name, whatever package it is in

Honoured by name too; the Maven plugin replaces the stripped members with whitespace

Platform differences through replacement classes

<super-source path="..."/> in the module XML (see JRE Emulation & Shared Code)

The J2CL documentation recommends a replacement file in a super-j2cl folder selected by the build rule (Bazel)

Avoid reflection, String.format, java.io, java.net

Not emulated either (No source code is available for type ...)

Not emulated (The import ... cannot be resolved)

No dependence on long, float, array-bounds or class-initialisation corner cases

Each compiler has its own differences

Documented differences listed above

Use Java 8 level syntax unless both build tools are verified for more

-sourceLevel 17 in the Bookshelf scenario

j2cl-maven-plugin 0.22.0 rejected var

The J2CL best-practices page also warns against using @GwtIncompatible to switch application logic on and off, because the divergence then spreads through the code. Its advice is to hide the incompatible part behind an interface and supply a different implementation per platform. The GWT-side detail of @GwtIncompatible and super-source is explained in JRE Emulation & Shared Code, and the JsInterop annotations used for the browser edge in JsInterop and DOM & Elemental2.

Worked example: one core module, two compilers

The Bookshelf scenario gets a small module that both compilers build: an ISBN-13 checker, a catalog and a BookAddedEvent that uses the ported gwt-event module. Three Maven modules make the dual build:

flowchart TD parent["bookshelf-dual (pom)
imports org.gwtproject:gwt 2.13.1 BOM"] core["bookshelf-core (jar + sources)
Isbn, Catalog, BookAddedEvent
gwt-event 1.0.0-RC1, JUnit test"] gwtapp["bookshelf-gwt-app (gwt-app)
gwt-maven-plugin 1.3.0
GwtApp entry point"] j2clapp["bookshelf-j2cl-app (war)
j2cl-maven-plugin 0.22.0
J2clApp + native.js"] parent --> core parent --> gwtapp parent --> j2clapp core --> gwtapp core --> j2clapp gwtapp --> out1["gwtapp.nocache.js
2 permutations"] j2clapp --> out2["j2clapp.js
1 optimised file"]

The shared module

The core module has no dependency on gwt-user. Its only dependencies are the ported event module, the JsInterop annotations (they ship with GWT and J2CL alike) and JUnit for the JVM test.

bookshelf-core/pom.xml (dependencies and sources jar)
<dependencies>
  <dependency>
    <groupId>org.gwtproject.event</groupId>
    <artifactId>gwt-event</artifactId>
    <version>1.0.0-RC1</version>
  </dependency>
  <dependency>
    <groupId>com.google.jsinterop</groupId>
    <artifactId>jsinterop-annotations</artifactId>
    <version>2.1.0</version>
  </dependency>
  <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
    <scope>test</scope>
  </dependency>
</dependencies>

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-source-plugin</artifactId>
      <version>3.4.0</version>
      <executions>
        <execution>
          <id>attach-sources</id>
          <goals><goal>jar-no-fork</goal></goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

The sources jar matters: both compilers read Java source, not bytecode, from the dependencies. The GWT module file sits next to the classes and names the packages that are client code:

bookshelf-core/src/main/resources/com/example/bookshelf/core/Core.gwt.xml
<?xml version="1.0" encoding="UTF-8"?>
<module>
  <inherits name="org.gwtproject.event.Event"/>
  <inherits name="com.google.gwt.core.Core"/>
  <source path="shared"/>
</module>

J2CL ignores the file; it only needs the sources jar.

Isbn.java: JsInterop type, shared logic and one server-only method
package com.example.bookshelf.core.shared;

import java.io.File;
import jsinterop.annotations.JsMethod;
import jsinterop.annotations.JsType;

@JsType(namespace = "bookshelf", name = "Isbn")
public final class Isbn {

  private Isbn() {}

  @JsMethod
  public static String normalize(String raw) {
    return raw.replace("-", "").replace(" ", "");
  }

  @JsMethod
  public static boolean isValid(String raw) {
    String digits = normalize(raw);
    if (digits.length() != 13) {
      return false;
    }
    int sum = 0;
    for (int i = 0; i < 13; i++) {
      char c = digits.charAt(i);
      if (c < '0' || c > '9') {
        return false;
      }
      sum += (c - '0') * (i % 2 == 0 ? 1 : 3);
    }
    return sum % 10 == 0;
  }

  /** Server-only: java.io.File is not part of either emulation. */
  @GwtIncompatible("java.io.File is not emulated")
  public static String coverFileName(String raw) {
    return new File("covers", normalize(raw) + ".jpg").getPath();
  }
}

GwtIncompatible is a six-line annotation declared in the same package (@Retention(CLASS), targets type, method, field and constructor, one String value()): both tools look at its simple name only, so there is no dependency on GWT to import.

BookAddedEvent.java and Catalog.java: shared model on the ported event bus
public class BookAddedEvent extends Event<BookAddedEvent.Handler> {

  public interface Handler {
    void onBookAdded(BookAddedEvent event);
  }

  public static final Type<Handler> TYPE = new Type<>();

  private final String title;

  public BookAddedEvent(String title) {
    this.title = title;
  }

  public String getTitle() {
    return title;
  }

  @Override
  public Type<Handler> getAssociatedType() {
    return TYPE;
  }

  @Override
  protected void dispatch(Handler handler) {
    handler.onBookAdded(this);
  }
}

public class Catalog {

  private final EventBus bus = new SimpleEventBus();
  private final List<String> titles = new ArrayList<>();

  public EventBus events() {
    return bus;
  }

  public boolean add(String title, String isbn) {
    if (!Isbn.isValid(isbn)) {
      return false;
    }
    titles.add(title);
    bus.fireEvent(new BookAddedEvent(title));
    return true;
  }

  public int size() {
    return titles.size();
  }
}

The plain JVM test needs no browser and no compiler:

CatalogTest.java
@Test
public void coverFileNameIsServerOnly() {
  assertEquals("covers/9780441172719.jpg", Isbn.coverFileName("978-0-441-17271-9"));
}

@Test
public void addsOnlyValidIsbns() {
  Catalog catalog = new Catalog();
  final String[] seen = new String[1];
  catalog.events().addHandler(BookAddedEvent.TYPE, e -> seen[0] = e.getTitle());
  assertTrue(catalog.add("Dune", "978-0-441-17271-9"));
  assertFalse(catalog.add("Broken", "978-0-441-17271-0"));
  assertEquals(1, catalog.size());
  assertEquals("Dune", seen[0]);
}

The GWT application

bookshelf-gwt-app/pom.xml (plugin part)
<packaging>gwt-app</packaging>

<dependencies>
  <dependency>
    <groupId>${project.groupId}</groupId>
    <artifactId>bookshelf-core</artifactId>
    <version>${project.version}</version>
  </dependency>
  <dependency>
    <groupId>${project.groupId}</groupId>
    <artifactId>bookshelf-core</artifactId>
    <version>${project.version}</version>
    <classifier>sources</classifier>
  </dependency>
  <dependency>
    <groupId>org.gwtproject</groupId>
    <artifactId>gwt-user</artifactId>
  </dependency>
  <dependency>
    <groupId>org.gwtproject</groupId>
    <artifactId>gwt-dev</artifactId>
  </dependency>
</dependencies>

<build>
  <plugins>
    <plugin>
      <groupId>net.ltgt.gwt.maven</groupId>
      <artifactId>gwt-maven-plugin</artifactId>
      <version>1.3.0</version>
      <extensions>true</extensions>
      <configuration>
        <moduleName>com.example.bookshelf.gwtapp.GwtApp</moduleName>
        <moduleShortName>gwtapp</moduleShortName>
        <sourceLevel>17</sourceLevel>
        <failOnError>true</failOnError>
        <compilerArgs>
          <arg>-generateJsInteropExports</arg>
        </compilerArgs>
      </configuration>
    </plugin>
  </plugins>
</build>
GwtApp.gwt.xml
<module rename-to="gwtapp">
  <inherits name="com.google.gwt.user.User"/>
  <inherits name="com.example.bookshelf.core.Core"/>
  <entry-point class="com.example.bookshelf.gwtapp.GwtApp"/>
  <source path=""/>
</module>
GwtApp.java
public class GwtApp implements EntryPoint {
  @Override
  public void onModuleLoad() {
    Catalog catalog = new Catalog();
    StringBuilder out = new StringBuilder();
    catalog.events().addHandler(BookAddedEvent.TYPE,
        e -> out.append("event=").append(e.getTitle()).append(' '));
    boolean ok = catalog.add("Dune", "978-0-441-17271-9");
    boolean bad = catalog.add("Broken", "978-0-441-17271-0");
    out.append("added=").append(ok).append(" rejected=").append(!bad)
        .append(" size=").append(catalog.size())
        .append(" normalized=").append(Isbn.normalize("978-0-441-17271-9"));
    Document.get().getBody().setInnerText("gwt: " + out);
  }
}

The page is plain HTML with <script src="gwtapp/gwtapp.nocache.js">; setInnerText is used because the string is application data, never setInnerHTML.

The J2CL application

bookshelf-j2cl-app/pom.xml (plugin part)
<packaging>war</packaging>

<dependencies>
  <dependency>
    <groupId>${project.groupId}</groupId>
    <artifactId>bookshelf-core</artifactId>
    <version>${project.version}</version>
  </dependency>
  <dependency>
    <groupId>com.google.elemental2</groupId>
    <artifactId>elemental2-dom</artifactId>
    <version>1.1.0</version>
  </dependency>
</dependencies>

<build>
  <plugins>
    <plugin>
      <groupId>com.vertispan.j2cl</groupId>
      <artifactId>j2cl-maven-plugin</artifactId>
      <version>0.22.0</version>
      <executions>
        <execution>
          <goals><goal>build</goal></goals>
        </execution>
      </executions>
      <configuration>
        <compilationLevel>ADVANCED</compilationLevel>
        <webappDirectory>${project.build.directory}/${project.build.finalName}/j2clapp</webappDirectory>
        <initialScriptFilename>j2clapp.js</initialScriptFilename>
      </configuration>
    </plugin>
  </plugins>
</build>
J2clApp.java
@JsType
public class J2clApp {
  public void run() {
    Catalog catalog = new Catalog();
    StringBuilder out = new StringBuilder();
    catalog.events().addHandler(BookAddedEvent.TYPE,
        e -> out.append("event=").append(e.getTitle()).append(' '));
    boolean ok = catalog.add("Dune", "978-0-441-17271-9");
    boolean bad = catalog.add("Broken", "978-0-441-17271-0");
    out.append("added=").append(ok).append(" rejected=").append(!bad)
        .append(" size=").append(catalog.size())
        .append(" normalized=").append(Isbn.normalize("978-0-441-17271-9"));
    DomGlobal.document.body.textContent = "j2cl: " + out;
  }
}
J2clApp.native.js (same folder as J2clApp.java; this is the entry point)
// Runs after the page has loaded the bundle; J2CL has no EntryPoint interface.
setTimeout(function () {
  new J2clApp().run();
}, 0);

The .native.js pattern is the one the j2cl-archetype-simple archetype generates. Only the browser edge (Document.get().getBody() in GWT, DomGlobal.document.body in Elemental2) differs between the two applications; every line of model and logic comes from the shared jar.

Build and run

Build everything (JDK 21, Maven 3.9)
mvn -B package
Condensed Maven output (cold J2CL cache)
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
   Compiling 2 permutations
   Compilation succeeded -- 3,498s
bookshelf-core ..................................... SUCCESS [  0.601 s]
bookshelf-gwt-app .................................. SUCCESS [  4.737 s]
bookshelf-j2cl-app ................................. SUCCESS [ 20.996 s]
BUILD SUCCESS

Both outputs were served from localhost and loaded in headless Chrome; the text each page wrote into its body is the DOM that Chrome reported after the scripts ran:

Rendered result of the GWT build
<body>gwt: event=Dune added=true rejected=true size=1 normalized=9780441172719</body>
Rendered result of the J2CL build
<body>j2cl: event=Dune added=true rejected=true size=1 normalized=9780441172719

The behaviour of the shared code is identical, including the ported event bus, the StringBuilder and the Isbn checks.

Table 6. Output of the two builds for this tiny application
File Bytes Gzipped

GWT gwtapp.nocache.js (bootstrap, loaded first)

6,658

2,930

GWT one *.cache.js permutation (a browser loads one of the two)

47,905 and 47,907

14,750 and 14,752

J2CL j2clapp.js (ADVANCED)

20,955

6,721

These numbers describe this 30-line application only: the GWT figure includes the runtime and the user agent bootstrap, and the J2CL figure includes Elemental2’s DomGlobal and the JRE parts that the code reaches. They show that both pipelines produce a small file for a small program and say nothing about the size of a real application.

The JsInterop export behaves differently, as the J2CL semantics page predicts. A probe page that calls bookshelf.Isbn.isValid(...) after loading each bundle returned [true,false,"9780441172719"] for the GWT build (compiled with -generateJsInteropExports) and failed with Uncaught ReferenceError: bookshelf is not defined for the J2CL ADVANCED build, which is consistent with the J2CL statement that exports go through goog.export. Code that must be callable from outside JavaScript needs an explicit export on the J2CL side.

What happens without the annotation

Removing @GwtIncompatible from coverFileName made both builds fail, with the messages each tool produces:

GWT 2.13.1
Errors in 'jar:file:core/target/bookshelf-core-1.0-SNAPSHOT-sources.jar!/com/example/bookshelf/core/shared/Isbn.java'
   Line 40: No source code is available for type java.io.File; did you forget to inherit a required module?
Aborting compile due to errors in some input files
J2CL (j2cl-maven-plugin 0.22.0)
bookshelf-core:1.0-SNAPSHOT/transpiled_js: Error:Isbn.java:6: The import java.io.File cannot be resolved
bookshelf-core:1.0-SNAPSHOT/transpiled_js: Error:Isbn.java:40: File cannot be resolved to a type

With the annotation, both builds succeed. The J2CL preprocessor replaces the annotated member by blank lines, so line numbers in later messages still point at the original source.

The j2cl-maven-plugin

The Maven plugin is the way to build J2CL code without Bazel. It is a community project by Vertispan, not part of the GWT SDK or of Google’s J2CL repository.

Table 7. j2cl-maven-plugin facts (checked 2026-10-09)
Item Value

Coordinates

com.vertispan.j2cl:j2cl-maven-plugin, goal prefix j2cl

Latest release

0.22.0 (Central, 2023-11-13); earlier 0.19, 0.20, 0.21.0

Repository

https://github.com/Vertispan/j2clmavenplugin (not archived; last push 2024-10-14)

J2CL build it bundles

v20230718-1 (the logs show com.vertispan.j2cl:jre:v20230718-1 and the matching bootstrap artifacts), not the v20260402 release of Google’s repository

Archetypes

com.vertispan.j2cl.archetypes:j2cl-archetype-simple and j2cl-archetype-servlet (0.22.0); simple = one HTML page and one Java class, servlet = Jakarta servlet server plus a J2CL client

Goals (from the plugin descriptor)

build (default phase prepare-package), test (default phase test), watch and clean (no default phase)

Selected build parameters

compilationLevel, initialScriptFilename, webappDirectory, languageOut, defines, externs, env, enableSourcemaps, checkAssertions, dependencyMode, dependencyReplacements, gwt3BuildCacheDir, workerThreadCount

What the Readme documents (not all of it exercised here):

  • Per-library builds with a cache. Each dependency is hashed together with its dependencies and the plugin version, stripped of @GwtIncompatible members, run through annotation processors, transpiled and cached; only changed libraries are rebuilt. The cache lives in target by default and can be moved to a shared directory with the gwt3.cache.dir property.

  • mvn j2cl:clean removes cache entries, -Dartifact=<artifactId> limits it to one artifact and -Dartifact=* empties the cache directory.

  • Dependency rewriting. By default the plugin replaces the jsinterop-base coordinates with com.vertispan.jsinterop:base, which the build log confirmed (Removing dependency com.google.jsinterop:base:jar:1.0.1:compile, replacing with com.vertispan.jsinterop:base:jar:1.0.1-1).

  • Compilation levels. ADVANCED_OPTIMIZATIONS for production (the sample archetype writes ADVANCED, which worked), SIMPLE_OPTIMIZATIONS, WHITESPACE_ONLY, BUNDLE (concatenation only) and BUNDLE_JAR (one bundle per classpath entry, loaded in order).

  • Dev mode. The watch goal recompiles the changed libraries of a whole reactor, so it is meant to be run from the parent project. It was not run for this page.

The test goal runs JUnit tests compiled to JavaScript (the archetype’s test runs under HtmlUnit); the Readme claims the plugin is the only way to build general J2CL projects outside Bazel. The GWT counterpart of such tests is covered in Testing GWT Applications.

Gradle, Bazel and the rest

The community GWT Gradle plugins mentioned in Build Tools: Maven & Gradle build GWT 2 applications; none of them was checked for J2CL. Google’s own J2CL workflow is Bazel (its getting-started page uses Bazelisk and bazel build/bazel run on the helloworld sample, and says the output of that sample is a single optimised line of JavaScript). The Bazel path was not run for this page. For a Java team that already builds with Maven, the plugin is the lower-friction route; the plugin bundles the older J2CL build v20230718-1, so features added since (the J2CL README mentions Wasm) are reached through Bazel; whether the plugin exposes them was not checked.

Moving a GWT code base toward J2CL

A realistic migration is incremental and always keeps the GWT build green:

  • Isolate the portable core. Move models, validation, formatting and domain events into a module that depends on no gwt-user type, as bookshelf-core does. Compile it with javac and JUnit first.

  • Replace JSNI with JsInterop. The pages JSNI & Overlay Types and JsInterop describe the mapping; JsInterop works in both compilers.

  • Replace GWT.create and generators in the core with factories or annotation processors (see Deferred Binding & Generators).

  • Swap gwt-user classes for ported ones where a module exists and is acceptable to depend on at release-candidate quality (gwt-event, gwt-core, gwt-dom, gwt-safehtml, gwt-editor), or for Elemental2.

  • Add the second build (J2CL) in CI and keep it as a canary. Anything that breaks it shows which GWT assumption crept into the core.

  • Leave the UI and the transport alone. Widgets, UiBinder, cell widgets, ClientBundle, i18n and GWT-RPC have no J2CL-ready counterpart in org.gwtproject.

Mapping of the old coordinates and packages, with the other changes between GWT versions, is collected in What’s Changed & Migration, in particular the recipes for coordinates and packages, Jakarta, JSNI and Elemental. A catalogue of the libraries around GWT, including alternatives for the pieces that were not ported, is in Ecosystem & Libraries, and IDE support for GWT and J2CL projects in Tooling & IDE Support.

The roadmap

The GWT roadmap page is short and says the following (checked on 2026-10-09):

  • planning happens in GitHub milestones; the milestones are the place to look, because priorities change;

  • the focus of the next releases is Java language features and JRE emulation up to Java 21;

  • GWT explores supporting some J2CL-exclusive JsInterop features in its compiler;

  • the SDK is open source and looks for contributors and sponsors; donations go through Open Collective.

The open milestone 2.14 (updated 2026-09-19) contains items that show the direction. They are listed by issue number and title, as of the same date:

Table 8. Selected open items in the 2.14 milestone
Issue Title

9794

Explore @JsEnum support in GWT

10011

Explore replacing build/project setup and modularizing codebase

10236

Remove j2cl $isInstance methods, other unused code

10038

Enable -strict for user tests

10253

OptimizedOnlyCompilerSuite is never run, fails

10323

Improve incremental tracking of method references and lambdas

The @JsEnum item matches the 2.13.1 behaviour found in this section: the annotation compiles and is ignored. Nothing in the milestones or the roadmap promises a date for any of this, and the 2.14 milestone has no due date.

Open Collective for the project: opencollective.com/gwt-project.

Outdated material in older books and posts

Books written for GWT 1.x and 2.0 to 2.5 predate J2CL, which Google began in 2015, and the org.gwtproject modules, so they cannot be used to decide anything about either. Their chapters on events, DOM access, RPC and widgets describe the com.google.gwt packages, which remain valid for GWT 2.13.1; what changed is that the same code is no longer the only option for non-UI libraries.

Pitfalls

Table 9. Problems met while building the same module with both compilers
Symptom (exact message) Cause Fix

Unable to find 'jsinterop/annotations/JsInterop.gwt.xml' on your classpath; could be a typo, or maybe you forgot to include a classpath entry for source?

A <inherits name="jsinterop.annotations.JsInterop"/> line was added to the module; no such module descriptor exists in jsinterop-annotations 2.0.0, 2.1.0 or gwt-user 2.13.1

Remove the line; the annotations compile in GWT 2.13.1 without it

No source code is available for type java.io.File; did you forget to inherit a required module? (GWT) and The import java.io.File cannot be resolved (J2CL)

A JRE class neither emulation provides is used in shared code

Mark the member @GwtIncompatible, or hide it behind an interface with a different implementation per platform

--dependency_mode=SORT_ONLY cannot be used with --entry_point, --closure_entry_point or --common_js_entry_module. followed by Closure Compiler failed, check log for details

The entrypoint element was configured; the log warns Parameter 'entrypoint' is deprecated: Will be removed in 0.21 and the default dependency mode is SORT_ONLY

Delete entrypoint; start the application from a .native.js file as in the archetype

[JSC_UNDEFINED_VARIABLE] variable ElementInternals is undeclared (and the same for MediaStreamTrackGenerator, OverconstrainedError, SubmitEvent, TransitionEvent)

elemental2-dom 1.2.3 with j2cl-maven-plugin 0.22.0, whose Closure externs predate those browser types

Use elemental2-dom 1.1.0 (the version in the plugin’s own archetype)

The import org cannot be resolved and NullMarked cannot be resolved to a type while transpiling elemental2-promise:1.3.2

Elemental2 1.3.2 declares a JSpecify dependency; the transpile step of 0.22.0 could not resolve the annotations although org.jspecify:jspecify:1.0.0 was in the dependency list

Same fix; or build with a newer J2CL than the plugin bundles (untested here)

cannot find symbol with symbol: class var in the bytecode step of the shared module

var (Java 10) in code that the J2CL build compiles; the plugin log shows source value 8 is obsolete for that step

Keep shared code at Java 8 syntax while j2cl-maven-plugin 0.22.0 is the J2CL build

Uncaught ReferenceError: bookshelf is not defined

@JsType classes are not exported to window by an ADVANCED J2CL build

Export explicitly with goog.export (see the J2CL semantics page); GWT needs -generateJsInteropExports

Uncaught ReferenceError: $jscomp is not defined with compilationLevel BUNDLE

The plain concatenated bundle (6.4 MB for this application) referenced $jscomp, which it did not define

Use ADVANCED (21 KB for the same application) or BUNDLE_JAR

The import java.io.BufferedReader cannot be resolved for javax.validation:validation-api:1.0.0.GA:sources

gwt-user was added as a dependency of the J2CL module and the plugin tried to transpile its transitive dependencies

Keep gwt-user and gwt-dev out of the dependency graph of J2CL modules

Build failed, check log for failures from j2cl:build

Generic wrapper; the real cause is an earlier ERROR line in the log. After an aborted build, STALE BUILD DETECTED - build was stale after 10 seconds, deleting it to take over lines appear and the next build may fail with them

Read the first error; delete target and rebuild

What changed recently

  • GWT 2.9.0: compiles against jsinterop-base 1.0.0, Elemental2 1.0.0 and jsinterop-annotations 2.0.0; Elemental (version 1) officially deprecated in favour of Elemental2.

  • GWT 2.10.0: org.gwtproject becomes the groupId; JRE emulation simplifications to keep the GWT and J2CL code bases consistent for J2CL’s Wasm backend.

  • GWT 2.12 and 2.13: Java 17 language level and JRE additions; the roadmap now names Java 21 and J2CL-exclusive JsInterop features as the goals.

  • J2CL releases on GitHub: v20250623, v20250630, v20260402.

  • jsinterop-base 1.2.0 (2026-06-30); Elemental2 1.3.2 stable, 1.4.0-RC1 published 2026-07-07.

  • j2cl-maven-plugin: last release 0.22.0 on 2023-11-13; last repository push 2024-10-14.

  • org.gwtproject modules: newest Central artifact gwt-core 1.0.0-RC2 (2023-11-04); no stable 1.0.0.