By Sumit Chandel, Google Web Toolkit Team
You may be happily coding away your GWT application, when you start to notice that as you keep enriching the application with new UI components, the load time starts to increase. If you're looking for ways to speed up your application, perhaps you should consider lazy loading in GWT application development - what we're coining "On-demand widget creation".
Lazy loading is a technique where a given object holds null references to other objects that later get instantiated only when they are needed. Actually, lazy loading is a concept that you may already be familiar with, but you may not know how it applies to GWT development. Hopefully this post helps you learn about how you can apply "on-demand widget creation" to speed up your own GWT applications.
Imagine your application looks something like a telephone directory listed alphabetically and split into three tabs (A-J, K-R, S-Z). Each tab contains a list of clickable names and phone numbers. Upon clicking an entry, you can access the contact address information as well as map it on an embedded Google Map located on the right side of the page, outside of the three tabs.
Initially, the code for the tab panel containing the list of names and phone numbers might look something like this:
String[] headings = new String[]{"A-J","K-R","S-Z"}; public void onModuleLoad() { VerticalPanel verticalPanel = new VerticalPanel(); initializeGMap(); TabPanel directory = new TabPanel(); FlexTable listingAJ = new FlexTable(); FlexTable listingKR = new FlexTable(); FlexTable listingSZ = new FlexTable(); loadListings(listingAJ, headings[0]); loadListings(listingKR, headings[1]); loadListings(listingSZ, headings[2]); directory.add(listingAJ, headings[0]); directory.add(listingKR, headings[1]); directory.add(listingSZ, headings[2]); verticalPanel.add(directory); verticalPanel.setSize("500px", "500px"); RootPanel.get().add(verticalPanel); } private void loadListings(FlexTable listings, String range) { int numRows = listings.getRowCount(); if("A-J".equals(range)) { listings.setWidget(numRows++, 0, new Listing("John Doe", "555-3092")); listings.setWidget(numRows++, 0, new Listing("Jane Doe", "555-2039")); } else if ("K-R".equals(range)) { listings.setWidget(numRows++, 0, new Listing("John Roe", "555-3123")); listings.setWidget(numRows++, 0, new Listing("John Koe", "555-9583")); listings.setWidget(numRows++, 0, new Listing("Jane Moe", "555-9503")); } else if("S-Z".equals(range)) { listings.setWidget(numRows++, 0, new Listing("Jane Zoe", "555-0945")); } }
This code seems good enough and it will work for what we expect out of our phone book. If you try it out in hosted mode, the phone book is quite responsive and usable. However, it's worthy to note that this example is over a limited population.
What if we wanted to do this for a much larger population - say that of New York City. We would be looking at roughly 8 million potential subscribers whose information we want to list. Assuming an even distribution between names across our three tab alphabetical division, this would mean showing 2.67 million entries in a single tab.
Ignoring the fact that this is an extreme example, you could imagine that trying to show that many entries on one tab is likely to freeze browsers and end your users' relationship with your application. However, the truth is that you don't even have to reach numbers in the 10^6 range for your users to start experiencing slowdowns, and our phone book application users will start feeling the pain even at a few hundred entries.
Let's suppose we were creating a phone book for a smaller population size, say that of Smalltown, Pennsylvania (population: 600). Here is when using "on-demand widget creation" can help - only render the widgets once you really need to see them.
Applying this as a first attempt to improving our phone book experience, we would end up with something like this:
public void onModuleLoad() { VerticalPanel verticalPanel = new VerticalPanel(); initializeGMap(); final TabPanel directory = new TabPanel(); FlexTable listingAJ = new FlexTable(); ListingPlaceHolder listingKR = new ListingPlaceHolder(); ListingPlaceHolder listingSZ = new ListingPlaceHolder(); directory.addTabListener(new TabListener() { public boolean onBeforeTabSelected(SourcesTabEvents sender, int tabIndex) { Widget listing = directory.getWidget(tabIndex); if(listing instanceof ListingPlaceHolder) { FlexTable listingToLoad = new FlexTable(); loadListings(listingToLoad, headings[tabIndex]); directory.remove(listing); directory.insert(listingToLoad, headings[tabIndex], tabIndex); } return true; } public void onTabSelected(SourcesTabEvents sender, int tabIndex) { } }); loadListings(listingAJ, headings[0]); directory.add(listingAJ, headings[0]); directory.add(listingKR, headings[1]); directory.add(listingSZ, headings[2]); verticalPanel.add(directory); verticalPanel.setSize("500px", "500px"); RootPanel.get().add(verticalPanel); }
As shown in the code above, you can see that we're not instantiating the second and third tab listings just yet, and instead setting the tab content to an empty ListingPlaceHolder type. Further down in the code, you can see that we've added a new TabListener to the directory that is checking for when a given tab is selected and whether the selected tab has been instantiated yet. If not, it loads the listings for the tab and adds them to the directory tab panel.
In the GWT incubator project, you can find the LazyPanel - a panel which allows you to easily load widgets on demand according to your lazy load strategy.
Using the LazyPanel is simple. All you need to do is add the widget that you want to lazily load in the lazy panel, and then call setVisible(true) on the lazy panel to actually have the widget load on demand. It's worth mentioning that the LazyPanel is mainly intended for use with widgets like the TabPanel and the StackPanel, and is not ideal in all cases.
Applying this to the phone book example, the recrafted code might look something like this:
public void onModuleLoad() { VerticalPanel verticalPanel = new VerticalPanel(); initializeGMap(); TabPanel directory = new TabPanel(); LazyListingPanel listingAJ = new LazyListingPanel(headings[0]); LazyListingPanel listingKR = new LazyListingPanel(headings[1]); LazyListingPanel listingSZ = new LazyListingPanel(headings[2]); listingAJ.setVisible(true); directory.add(listingAJ, headings[0]); directory.add(listingKR, headings[1]); directory.add(listingSZ, headings[2]); verticalPanel.add(directory); verticalPanel.setSize("500px", "500px"); RootPanel.get().add(verticalPanel); } public class LazyListingPanel extends LazyPanel { String range; public LazyListingPanel(String range) { this.range = range; } @Override public FlexTable createWidget() { FlexTable listings = new FlexTable(); loadListings(listings, range); return listings; } private void loadListings(FlexTable listings, String range) { ... } }
And there you have it.
As mentioned just above, the LazyPanel isn't always the ideal choice for implementing an on-demand widget creation design. For example, one thing we mentioned about our phone book application is that each listing is clickable, and once clicked bring up an address popup. You could imagine that manually implementing the lazy load pattern for the address popup widget probably makes more sense than create LazyPanel instances for each address popup attached to each phone book listing.
Lazily loading your widgets has the obvious benefit of improving the load time performance of your application. It also allows you to control your application's memory usage as your user interacts with it, which can also be critically important since applications that tend to consume a lot of memory can potentially crash in the user's browser.
However, it is possible to overdo lazy loading and actually have it affect your application performance negatively. In fact, sometimes you want to do just the opposite and eagerly load parts of your application to improve performance.
After going through the simple example of on-demand widget creation and talking about its benefits, it may seem counter-intuitive to now say that sometimes it's better to eagerly load a part of the application. But there do indeed exist cases where it's better to eagerly load parts of your application to create a better user experience.
Take for example our phone book application. Let's say we now wanted to add different types of directories to our phone book - businesses, people and government institutions. If we were to apply the lazy load pattern the same way as we did for the personal listings from A-Z, we would be tempted to do the same here and only load the business listings once they were actually clicked on and asked for. Same for the government institution listings.
The average user probably spends most of their time looking up personal and business listings. For example, let's say a user is looking up his lawyer's number. He might first check the personal listing for "John Doe", and supposing he doesn't find it, switch to the business listings to see if the number is there.
Supposing the business listings are loaded on demand, the user might have to wait a quick but noticeable few milliseconds before the listings actually show up - time that might not have been spent waiting if the listings were already loaded. On the other hand, having the business listings load up in advance might slow down the initial application load time, not to mention incur some memory usage in addition to everything else that's already loaded.
So, lazy loading is great but takes some planning. One of the decision points we just mentioned is how much memory the components you need to load will take up and how they affect initial load time of either your application or one of its larger components.
Aside from profiling your application to plan a lazy load strategy, it's generally useful to learn and apply profiling techniques during development so you can quickly identify hot spots and fix them. After making your final CSS tweaks, updated RPC calls and integrating a third party library and realizing that your application is now running ten times slower, it's important to locate and eliminate the slowdowns.
Because GWT application profiling is such an important topic, it seems appropriate to devote an entire blog post to just that topic, and then relating it back to lazy load strategy planning later. Stay tuned for next week where we'll show you different tools you can use to profile your GWT application and help iron out the wrinkles.