JS and CSS Minification With WRO4J

The main two contributing factors (after Adobe Flash) to the performance of mobile web pages are the number of request and the size of requests as explained in my previous article Reducing & Minifying Requests.

This article demonstrates how it is possible to minify and bundle JavaScript and CSS using WRO4J. For a working example in code see the github project Base Spring MVC Web Application: https://github.com/jamesdbloom/base_spring_mvc_web_application

Perhaps the easiest way to use WRO4J is from a maven build script using the WRO4J plugin, as follows:

<plugin>
    <groupId>ro.isdc.wro4j</groupId>
    <artifactId>wro4j-maven-plugin</artifactId>
    <version>1.6.2</version>
    <executions>
        <execution>
            <phase>compile</phase>
            <goals>
                <goal>run</goal>
            </goals>
            <configuration>
                <minimize>true</minimize>
                <ignoreMissingResources>true</ignoreMissingResources>
                <contextFolder>${basedir}/src/main/webapp/</contextFolder>
                <extraConfigFile>${basedir}/src/main/webapp/WEB-INF/wro.properties</extraConfigFile>
                <wroFile>${basedir}/src/main/webapp/WEB-INF/wro.xml</wroFile>
            </configuration>
        </execution>
    </executions>
</plugin>

I prefer using WRO4J using at runtime because this enables the list of unbundled and bundled resources to be pragmatically written into the HTML. This can easily be done using the javax.servlet.Filter called ro.isdc.wro.http.WroFilter. Runtime bundling makes it easier to enable / disable bundling and minification, either on a per request basis or for all pages, which helps when debugging and resolving issues.

<!-- resource bundling filter -->
<filter>
    <filter-name>webResourceOptimizer</filter-name>
    <filter-class>ro.isdc.wro.http.WroFilter</filter-class>
    <init-param>
        <param-name>targetBeanName</param-name>
        <param-value>wroManagerFactory</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>webResourceOptimizer</filter-name>
    <url-pattern>/bundle/*</url-pattern>
</filter-mapping>

To simplify the configuration and support the use of a Spring loaded bean use ro.isdc.wro.extensions.http.SpringWroFilter

<!-- spring loaded resource bundling filter -->
<filter>
    <filter-name>webResourceOptimizer</filter-name>
    <filter-class>ro.isdc.wro.extensions.http.SpringWroFilter</filter-class>
    <init-param>
        <param-name>targetBeanName</param-name>
        <param-value>wroManagerFactory</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>webResourceOptimizer</filter-name>
    <url-pattern>/bundle/*</url-pattern>
</filter-mapping>

The wroManagerFactory bean can then be configured as follows:

@Configuration
@PropertySource("classpath:web.properties")
public class RootConfiguration {
    @Resource
    private Environment environment;

    @Bean
    public WroManagerFactory wroManagerFactory() {
        ConfigurableWroManagerFactory wroManagerFactory = new ConfigurableWroManagerFactory();

        wroManagerFactory.setConfigProperties(new Properties() {{
            setProperty("debug", environment.getProperty("bundling.enabled"));
            setProperty("preProcessors", "cssImport,semicolonAppender,conformColors");
            setProperty("postProcessors", "yuiCssMin,googleClosureAdvanced");
            setProperty("cacheGzippedContent", "true");
            setProperty("hashStrategy", "MD5"); // should drive the naming strategy to fingerprint resource urls
            setProperty("namingStrategy", "hashEncoder-CRC32"); // should drive the naming strategy to fingerprint resource urls
        }});

        return wroManagerFactory;
    }

}

Using runtime bundling also enables listing bundled or unbundled resources pragmatically into the HTML. To do this you need to add an additional javax.servlet.Filter and javax.servlet.ServletContextListener. These additional classes ensure the WroModel is added to each incoming HttpServletRequest. The WroModel contains all the information needed to write the list of unbundled or bundled resources into the HTML.

<!-- add bundling model to servlet context so JS and CSS links can be pragmatically written into freemarker -->
<listener>
    <listener-class>ro.isdc.wro.http.WroServletContextListener</listener-class>
</listener>
<filter>
    <filter-name>WroContextFilter</filter-name>
    <filter-class>ro.isdc.wro.http.WroContextFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>WroContextFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

The list of CSS and JavaScript URIs are added into the model so the view template can write them into the HTML. This can be achieved using a HandlerInterceptor

public class AddBundlingModelToViewModelInterceptor extends HandlerInterceptorAdapter {

    public static final String JS_RESOURCES = "jsResources";
    public static final String CSS_RESOURCES = "cssResources";
    private static final Function<Resource, String> RESOURCE_TO_URI = new Function<Resource, String>() {
        @Override
        public String apply(Resource resource) {
            return resource.getUri();
        }
    };
    private final WroModelHolder wroModelHolder;
    private final String bundlingEnabled;

    public AddBundlingModelToViewModelInterceptor(WroModelHolder wroModelHolder, String bundlingEnabled) {
        this.wroModelHolder = wroModelHolder;
        this.bundlingEnabled = bundlingEnabled;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        modelAndView.addObject(JS_RESOURCES, getListOfUnbundledResources(ResourceType.JS, wroModelHolder.getWroModel(), request));
        modelAndView.addObject(CSS_RESOURCES, getListOfUnbundledResources(ResourceType.CSS, wroModelHolder.getWroModel(), request));
    }

    private Map<String, List<String>> getListOfUnbundledResources(ResourceType resourceType, WroModel wroModel, HttpServletRequest request) {
        Map<String, List<String>> resources = new HashMap<>();
        if (wroModel != null) {
            for (Group group : wroModel.getGroups()) {
                if (Strings.isNullOrEmpty(request.getParameter("bundle")) ? Boolean.parseBoolean(bundlingEnabled) : Boolean.parseBoolean(request.getParameter("bundle"))) {
                    resources.put(group.getName(), Arrays.asList("/bundle/" + group.getName() + "." + resourceType.name().toLowerCase() + (request.getQueryString().contains("minimize=false") ? "?minimize=false" : "")));
                } else {
                    resources.put(group.getName(), Lists.transform(wroModel.getGroupByName(group.getName()).collectResourcesOfType(resourceType).getResources(), RESOURCE_TO_URI));
                }
            }
        } else {
            resources.put("all", new ArrayList<String>());
        }
        return resources;
    }
}

To simplify the testing of this HandleInterceptor WroModelHolder has been separated out as Request scoped bean

@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class WroModelHolder {

    @Resource
    private ServletContext servletContext;
    private WroModel wroModel;

    public WroModel getWroModel() {
        if (wroModel == null) {
            ServletContextAttributeHelper helper = new ServletContextAttributeHelper(servletContext);
            if (helper.getManagerFactory() != null) {
                wroModel = helper.getManagerFactory().create().getModelFactory().create();
            }
        }
        return wroModel;
    }

}

These two beans are configured as follows

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"org.jamesdbloom.web"})
public class WebMvcConfiguration extends WebMvcConfigurerAdapter {

    @Resource
    private Environment environment;
    @Resource
    private WroModelHolder wroModelHolder;

    ...

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AddBundlingModelToViewModelInterceptor(wroModelHolder, environment.getProperty("bundling.enabled")));
    }
}

The list of JavaScript and CSS files can be written into the HTML using FreeMarker

<#macro page_html>
    <@compress single_line=true>
        <#escape x as x?html>
            <!DOCTYPE html>
            <html lang="en_GB">
                <head>

                    <@page_css/>

                    <title>some silly title to have on this page</title>
                    <link rel="shortcut icon" href="/resources/icon/favicon.ico" />
                    <link rel="apple-touch-icon" href="/resources/icon/icon-57.png" />
                    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=yes" />
                </head>
                <#flush>
                <body onunload="">

                    <@page_body/>

                    <@page_js/>

                </body>
            </html>
        </#escape>
    </@compress>
</#macro>

<#macro page_css>
    <#list cssResources["all"] as cssFile>
        <link rel="stylesheet" type="text/css" href="${cssFile}">
    </#list>
</#macro>

<#macro page_body>
       ...
</#macro>

<#macro page_js>
    <script type="text/javascript">
        window.onload = function() {
            setTimeout(function() {
                <#list jsResources["all"] as jsFile>
                    <#local node = "node_${jsFile_index}">
                    var ${node} = document.createElement('script');
                    ${node}.setAttribute('type', 'text/javascript');
                    ${node}.setAttribute('src', '${jsFile}');
                    document.body.appendChild(${node});
                </#list>
            }, 50);
        };
    </script>
</#macro>

To see the full working example see the github project Base Spring MVC Web Application: https://github.com/jamesdbloom/base_spring_mvc_web_application

Google