/ MINIFICATION, PERFORMANCE, HTML

Minification of HTML in Java EE webapps

Minification is the process of removing all unnecessary characters from the source codes of interpreted programming languages or markup languages without changing their functionality. These unnecessary characters usually include white space characters, new line characters, comments, and sometimes block delimiters, which are used to add readability to the code but are not required for it to execute. Minification reduces the size of the source code, making its transmission over a network (e.g. the Internet) more efficient.

— Wikipedia

Minification applies to text resources such as HTML, JavaScript and CSS stylesheets. In Java EE webapps, the way to implement minification is through a filter. It can be a custom filter relying on an existing library or an out-of-the-box filter, such as wro4j. I already wrote (a long time ago) about wro4j’s minification of CSS and how to integrate wro4j with WebJars.

Minification at runtime

Whatever the approach one selects, implementing the filter by yourself or using the wro4j one, I believe it’s the wrong approach for static resources. The reason for that is that filter will be invoked at every request. While it might be acceptable for dynamic resources, such as generated HTML, it’s not for static resources, such as static HTML, JavaScript and CSS. When a resource doesn’t change, it’s just extra work for nothing. This is akin to the dynamic website e.g. WordPress vs. static website e.g. Jekyll comparison.

Of course, there are possible mitigations. One such mitigation is to add HTTP cache-related headers:

  • Cache-Control
  • Expires
  • ETag
  • etc.

Another is to gzip the returned payload. Both can be achieved with more filters, or even better, through a web server located in front of the application server.

However, while taking those steps actually help, they can be supplemented with another approach.

Minification at build time

An alternative approach is to actually minify resources at build-time, during the generation of the artifact. This is actually the path chosen when creating archives of JavaScript applications, whether they are built with Grunt or Gulp.

While wro4j allows to minify JavaScript and CSS resources, it doesn’t provide such a feature for HTML. However, it’s not very hard to create a Maven plugin to do just that. The design revolves around two classes:

  1. The Mojo itself - the Maven plugin
  2. A file visitor to compress the source file and create the target compressed file

The visitor itself relies on an existing library to handle minification.

Minification class diagram

The Mojo is just the configuration facade while delegating the work to the visitor:

@Mojo(name = "minify", defaultPhase = LifecyclePhase.PREPARE_PACKAGE)
public class MinifyMojo extends AbstractMojo {

  @Parameter(defaultValue = "src/main/webapp", required = true)
  private File webappDirectory;

  @Parameter(defaultValue = "${project.build.directory}/${project.build.finalName}", required = true)
  private File targetDirectory;

  @Override
  public void execute() throws MojoExecutionException {
    try {
      Files.walkFileTree(
        webappDirectory.toPath(),
        new MinifyVisitor(webappDirectory.toPath(), targetDirectory.toPath(), getLog())
      );
    } catch (IOException e) {
      throw new MojoExecutionException("Exception during execution", e);
    }
  }
}

The visitor explores the src/main/webapp directory, and looks for JSP and HTML files. It minifies their content, and creates a new file with the compressed content in a sub-directory mirrored in the target folder. The following is just an embryo of the implementation:

public class MinifyVisitor implements FileVisitor<Path> {

  private final Path webappDirectory;
  private final Path targetDirectory;
  private final Log log;
  private final HtmlCompressor compressor = new HtmlCompressor();
  private final PathMatcher matcher =
          FileSystems.getDefault().getPathMatcher("glob:**.{html,jsp}");

  public MinifyVisitor(Path webappDirectory, Path targetDirectory, Log log) {
    this.webappDirectory = webappDirectory;
    this.targetDirectory = targetDirectory;
    this.log = log;
  }

  @Override
  public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
    return FileVisitResult.CONTINUE;
  }

  @Override
  public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
    log.info("Evaluating whether " + file + " should be processed");
    if (matcher.matches(file)) {                                                (1)
      log.info("Found " + file + " to be processed");
      List<String> lines = Files.readAllLines(file);                            (2)
      String compressedContent = compressor.compress(String.join("", lines));  (3)
      Path relativePath = webappDirectory.relativize(file);                     (4)
      log.info("Relative path is " + relativePath);
      Path target = Paths.get(targetDirectory.toUri()).resolve(relativePath);  (5)
      Path parentDir = target.getParent();
      Files.createDirectories(parentDir);                                      (6)
      log.info("Target directory created " + parentDir);
      Files.write(target, Collections.singletonList(compressedContent));       (7)
      log.info("Target file created " + target);
    }
    return FileVisitResult.CONTINUE;
  }

  @Override
  public FileVisitResult visitFileFailed(Path file, IOException e) {
    if (e != null) {
      log.error(e);
    }
    return FileVisitResult.CONTINUE;
  }

  @Override
  public FileVisitResult postVisitDirectory(Path dir, IOException e) {
    if (e != null) {
      log.error(e);
    }
    return FileVisitResult.CONTINUE;
  }
}
1 Check whether the file is a HTML or a JSP
2 Get the source content
3 Minify the source content
4 Get the path of the file relative to the web directory
5 Compute the path of the file relative to the target directory
6 Create the target directory structure
7 Write the minified content into the target file

At this point, the build just needs to be configured so that the WAR plugin doesn’t overwrite the minified files:

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-war-plugin</artifactId>
      <version>3.2.3</version>
      <configuration>
          <warSourceExcludes>index.html,WEB-INF/hello.jsp</warSourceExcludes>
      </configuration>
    </plugin>
    <plugin>
      <groupId>${parent.groupId}</groupId>
      <artifactId>minify-maven-plugin</artifactId>
      <version>${project.version}</version>
      <executions>
        <execution>
          <goals>
            <goal>minify</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

An improvement would be to execute the minify-maven-plugin after the maven-war-plugin. Then, overwriting would work in the opposite way, and no configuration would be necessary.

Conclusion

Migrating features from runtime to build time is actually very trendy. It can be seen in the rise of static blogging platforms such as Jekyll or Hugo. It’s also an undergoing process in the Java ecosystem, with frameworks such as Quarkus and Micronaut, which among others, move away from traditional reflection to an alternative relying on classes generated at compile-time. Everything that can be done at build time should be, in the context of high-performance requirements, or just to save on the cloud bill. Minification is a way to do that for static text resources.

The complete source code for this post can be found on Github in Maven format.
Nicolas Fränkel

Nicolas Fränkel

Nicolas Fränkel is a Developer Advocate with 15+ years experience consulting for many different customers, in a wide range of contexts (such as telecoms, banking, insurances, large retail and public sector). Usually working on Java/Java EE and Spring technologies, but with focused interests like Rich Internet Applications, Testing, CI/CD and DevOps. Currently working for Hazelcast. Also double as a teacher in universities and higher education schools, a trainer and triples as a book author.

Read More