My latest web project [1] is setting up a real estate site in Indonesia with GWT. While the number of internet users grow at an astonishing rate, much of the internet connectivity is achieved through slow dial ups or mobile connections. Thus the primary technical goal was keeping network payload to a minimum. GWT RPC seemed from the beginning a natural winner, not only because of the excellent Java integration but also because of it’s much more compact format when compared to Json or XML [3].
And the best part is: it turns out that this very good starting point can be further optimised.
Using GET over POST
The Yahoo performance team has put together a worthwhile read [5] about website performance. One of their finding was that POST requests are transmitted over the network with more packets than GET requests. This is not only slower, but increases on a bad network connection the chance of a transmission retry due to lost packets. I’ve covered this particular optimization (with code examples!) in [6] already.
A further positive side effect is that GET requests are cacheable, provided that your webserver is returning the correct HTTP headers (i.e. Cache-Control, Expires etc). The Bazarooma network layer makes good and thorough use of this caching technique which, when combined with a reverse web proxy such as Varnish considerably lightens server load.
Removing type names
The RPC format includes fully qualified Java class names which are not really necessary, since the compiled javascript code is obfuscated and doesn’t know (or rather need to know) about the original class names anyway. A rather scarcely documented feature is type name elision [7] which substitutes type names with short placeholders. In order to enable it, you must:
1. Inherit it in the module xml:
<inherits name="com.google.gwt.user.RemoteServiceObfuscateTypeNames"><br /></inherits>
2. Instruct the server side payload generator to elide typenames. Since Bazarooma is using GETs for RPC, we were running a custom RPC generator anyway, so this was a one-liner:
RPC.encodeResponseForSuccess(serviceMethod, methodArgument, policy, AbstractSerializationStream.FLAG_ELIDE_TYPE_NAMES);
Please note that use of a serialization policy is absolutely a must. Also in our case, the production deployment will occasionally and without any apparent reason refuse RPC requests with an exception complaining about type name elision. The solution to this turns out to be as annoying as trivial: redo the entire deployment.
Packing variables
Variable packing means representing several members with a single variable, i.e. boolean values as bits in an integer. This requires a change in the implementation of the domain objects (or data transfer objects) used, which should not be much of a problem as the interface doesn’t change.
For example, this:
class Offer{
boolean active;
boolean published;
boolean isActive(){
return active;
}
boolean published(){
return published;
}
}
becomes
class Offer{ int bitmap; boolean isActive(){ return (bitmap & 1)!=0; } boolean published(){ return (bitmap & 2)!=0; } }
Please note that in our case this reduced the payload by a marginal 5% after GZipping while increasing the deserialization time on the client (FF 3.6) by 15%.
Improving compression
GWT’s RemoteServiceServlet performs standard Gzip compression on the RPC payload by using standard JDK settings. We disabled RPC compression and instead performed our own Gzip compression (Java’s GzipOutputStream) with bigger buffers and higher compression settings which reduced payload size on average by 5%. Please note that the compression time will most likely drastically rise in this case (I don’t have any numbers though), thus not always making it a clear win with regard to server load.
Batching requests
The idea behind batching is to group multiple logical RPC requests into one physical payload and HTTP request which results in one physical payload and HTTP response. There are frameworks that take care of this [8], we wrote our own (for no particular reason, it was just too easy to do 🙂
Batching allowed us even to eliminate unnecessary requests, for instance when a user was clicking fast through search results, not really spending time on them. A naive implementation would have requested all results from the server, while batching allowed us to send only the last, valid command to the server.
[Update 31.03.2011] As Gal Dolber points out [9], batching strongly inhibits code splitting/deferred loading.
[Update 31.03.2011] Removing HTTP headers
If you’ve inspected an RPC request with Firebug (or similar), you’ll notice that there are many request headers which are not really usefull, such as the host name, user-agent, encodings etc. I tried to remove these by replacing XmlHttpRequest (you can’t meaningfully extend it, need to replace it in the GWT jar) but it turns out that most headers are defaulted by the browser and cannot be removed. With Firefox, for example, it turns out that only the User-Agent header can be removed.
You may also want to remove the custom X-GWT-* headers, but this will most certainly cause RemoteServiceServlet to complain about them missing. I solved this re-introducing these header on the server side with a custom filter (see GET vs POST earlier)
[1] Bazarooma
http://bazarooma.com
[2] Internet users by country
http://www.internetworldstats.com/top20.htm
[3] GWT RPC protocol
http://www.gdssecurity.com/l/b/2009/10/08/gwt-rpc-in-a-nutshell/
[4] Yahoo performance blog
http://developer.yahoo.com/performance/rules.html
[6] GWT RPC over HTTP GET
[7] Obfuscating GWT RPC type names
https://groups.google.com/group/google-web-toolkit/browse_thread/thread/4c383cb40b2fa931
[8] Batching GWT RPC requests with GWT-Dispatch
https://code.google.com/p/gwt-dispatch/
[9] Discussion on the GWT list about batching
https://groups.google.com/group/google-web-toolkit/browse_thread/thread/a95c1eaeb55d2104/cc39e8cc01e1e188#cc39e8cc01e1e188
Thanks for the post. Learnt about type eliding.
LikeLike
Hi George,
You can also make optimization on Accept-Language header when using a RequestBuilder:
RequestBuilder requestBuilder = new RequestBuilder(…);
// following line optimizes Accept-Language header from “en-US,en;q=0.8,it;q=0.6,tr;q=0.4” to “-“
requestBuilder.setHeader(“Accept-Language”, “-“);
Note that you shouldn't set “Accept-Language” to empty string. Otherwise you get a “value cannot be empty” error.
LikeLike