The success of any mobile web application relies on two factors: design and performance. For mobile app design, we must have a consistent look and feel across all platforms. For better performance, we must have offline capabilities, animations on the UI, and backend services that retrieve and send data via RESTful or WebSocket endpoints. To put it simply, your app is constrained by two ever-changing speeds: the speed of the device CPU/GPU and the speed of the Internet. The UI is handled by device hardware, such as the GPU, when doing native-like animations and transitions through CSS, and your backend services are limited to the current Internet connection speed of the mobile device.
In this chapter, we’ll discuss how to design, create, and tune your mobile web app to be better looking and more performant. The chapter starts with a brief explanation of how apps should look for mobile devices then jumps into a low-level explanation of hardware-accelerated CSS and how to debug it. From there, you’ll learn what it takes to build an offline mobile application and how to bring all the code together into one application to create a native-like mobile web app that is capable of handling intermittent Internet connections. Lastly, you’ll examine today’s most popular mobile frameworks to get an understanding of when or if you should add a community-supported framework to your project.
The “native versus Mobile Web” debate isn’t about which programming model will win. It’s about what we can build until HTML5-like technologies catch up. We have three choices:
Pure native approaches, which are clearly winning today in terms of overall application responsiveness
Hybrid approaches and frameworks, which try to bridge the gap of HTML5 and native
True, bleeding edge, mobile web frameworks, which are trying to conquer the native feel with markup, JavaScript, and CSS
Couple a fast and responsive mobile web app with your existing enterprise infrastructure, and let the games begin. Web standards are quickly closing the gap on missing native features, and device makers are catching up on implementing them. As of Android 3.1, for example, you can capture photos and videos due to the Media Capture API specification.
The W3C is a busy place these days, and developers are moving specifications and better use cases forward. Projects like jQuery are calling on the open source community to participate in these specifications and to submit their ideas for a better Web.
It only makes sense that mobile developers are leaning in favor of writing once, and running their app anywhere. Write once, run anywhere, or WORA, received a lot of fanfare after Sun’s JVM started to emerge in the enterprise. With HTML5, WORA basically means you can use standard JavaScript and CSS to access all of the device features that a native application can (the device GPS, camera, accelerometer, etc.). This approach has given new life to browsers and a language (HTML) that was once only used to serve up documents—not apps.
To truly achieve that native look and feel, not only does your app need to respond quickly, but it must also look good. These days, the big secret to getting your native app listed in an App Store Top 10 list is to have a good-looking design. That’s all it takes. If you have a killer data-driven application using all the latest device bells and whistles, it will not make it very far without a good clean design.
Overall, the Web has its own look and feel, and everyone knows that. There isn’t a default look that will make all your users happy, however, so the burden is on you and your design team to create an attractive user experience.
iOS definitely has its own Mobile Web look and feel that mimics its native apps, but what about Android, Windows Mobile, Kindle, and all the other devices? Even if you could get your web app to respond like a native application, how do you conquer making it look like one? Because you are most concerned with only the three or four leading platforms, you could create three native skins for your target platforms and a default web look and feel for all the others.
Theresa Neil does a great job of explaining UI patterns for native apps in Mobile Design Pattern Gallery (O’Reilly). Her website, (shown in Figure 3-1), is a great resource for trending patterns in mobile design.
Spinning refreshes, choppy page transitions, and periodic delays in tap events are just a few of the headaches you face when attempting to create a mobile web app that behaves like a native one. Developers are trying to get as close to native as they possibly can, but are often derailed by hacks, resets, and rigid frameworks.
Normally, GPUs handle detailed 3D modeling or CAD
diagrams, but for mobile web apps, we want our primitive drawings
(div
s, backgrounds, text with drop
shadows, images, etc.) to appear smooth and animate smoothly via the
GPU. The unfortunate thing is that most frontend developers are
dishing this animation process off to a third-party framework without
being concerned about the semantics, but should these core CSS3
features be masked? Consider a few reasons why caring about hardware
acceleration is important:
If you go around compositing every element in the DOM just for the sake of hardware acceleration, the next person who works on your code may chase you down and beat you severely.
Obviously, when hardware kicks in, so does the battery. Developers are forced to take the wide array of device constraints into consideration while writing mobile web apps. This will be even more prevalent as browser makers start to enable access to more and more device hardware. Luckily, we will soon have an API for checking the status of the device battery.
You will encounter glitchy behavior when applying hardware acceleration to parts of the page that were already accelerated. So knowing if you have overlapping acceleration is very important.
To make user interaction smooth and as close to native
as possible, you must make the browser work for you. Ideally, you want
the mobile device CPU to set up the initial animation, and then have
the GPU responsible for only compositing different layers during the
animation process. This is what translate3d
,
scale3d
, and
translateZ
do: they
give the animated elements their own layer, thus allowing the device
to render everything together smoothly.
CSS features can come at a cost on low-end devices. When using
CSS gradient
, box-shadow
,
borders
, and
background-repeat
,
you are using the device GPU to paint your images on the fly. CSS can
be very powerful for rendering a nice user interface, but you should
avoid doing this type of work in software when it can be prebaked in
images. This means you should use sprites so the device downloads only
a single image and embed data URIs in your CSS files for smaller
images.
A few animations that don’t require repaints are:
CSS selector performance can cripple older mobile browsers. Using selectors like:
div
[
style
*=
'foo'
]
will severely reduce performance on iOS devices up to version 4.3.x.
Take a look at three of the most common user-interaction approaches when developing a mobile web app: slide, flip, and rotation effects. First, we’ll dissect the slide, flip, and rotation transitions and how they’re accelerated. Notice how each animation requires only three or four lines of CSS and JavaScript. The examples don’t use any additional frameworks, only DOM and vendor prefixed APIs.
You can view this code in action at http://html5e.org/example. The demo is built for a mobile device, so fire up an emulator, use your phone or tablet, or reduce the size of your browser window to 1024px or less.
The most common of the three approaches, sliding page transitions, mimics the native feel of mobile applications. The slide transition is invoked to bring a new content area into the view port.
For the slide effect, first you declare your markup:
<div
id=
"home-page"
class=
"page"
>
<h1>
Home Page</h1>
</div>
<div
id=
"products-page"
class=
"page stage-right"
>
<h1>
Products Page</h1>
</div>
<div
id=
"about-page"
class=
"page stage-left"
>
<h1>
About Page</h1>
</div>
Notice that the pages are staged left and right. You could place them in any direction, but this is most common.
We now add animation plus hardware acceleration with just a few
lines of CSS. The actual animation happens when we swap classes on the
page div
elements.
.page
{
position
:
absolute
;
width
:
100
%
;
height
:
100
%
;
/*activate the GPU for compositing each page */
-
webkit
-
transform
:
translate3d
(
0
,
0
,
0
);
}
Although translate3d(0,0,0)
is
known as the silver bullet approach for WebKit, other browser engines
like fennec (Mobile Firefox) and Opera Mobile do not support, or are
just implementing, translate3d
as of
this writing. They do support 2D transformations, which cut out the
Z-axis, so to support these browsers, you need to change:
transale3d
(
X
,
Y
,
Z
);
//
or
translateX
(
X
),
translateY
(
Y
),
translateZ
(
Z
);
to:
translate
(
X
,
Y
);
The one downside to 2D transforms is that, unlike 3D transforms, they are not GPU accelerated.
Hardware acceleration tricks do not provide any speed improvement under Android Froyo 2.2 and beyond. All composition is done within the software.
When the user clicks a navigation element, we execute the following JavaScript to swap the classes. We’re not using any third-party frameworks yet, this is straight up JavaScript!
function
slideTo
(
id
)
{
//1.) the page we are bringing into focus dictates how
// the current page will exit. So let's see what classes
// our incoming page is using.
//We know it will have stage[right|left|etc...]
var
classes
=
getElement
(
id
).
className
.
split
(
' '
);
//2.) decide if the incoming page is assigned to right or left
// (-1 if no match)
var
stageType
=
classes
.
indexOf
(
'stage-left'
);
//3.) on initial page load focusPage is null, so we need
// to set the default page which we're currently seeing.
if
(
FOCUS_PAGE
==
null
)
{
// use home page
FOCUS_PAGE
=
getElement
(
'home-page'
);
}
//4.) decide how this focused page should exit.
if
(
stageType
>
0
)
{
FOCUS_PAGE
.
className
=
'page transition stage-right'
;
}
else
{
FOCUS_PAGE
.
className
=
'page transition stage-left'
;
}
//5. refresh/set the global variable
FOCUS_PAGE
=
getElement
(
id
);
//6. Bring in the new page.
FOCUS_PAGE
.
className
=
'page transition stage-center'
;
}
stage-left
or stage-right
becomes stage-center
and forces the page to slide into
the center view port. We are completely depending on CSS3 to do the
heavy lifting.
.stage-left
{
left
:
100
%
;
}
.stage-right
{
left
:
100
%
;
}
.stage-center
{
top
:
0
;
left
:
0
;
}
By controlling the animations through swapping the stage
classes in JavaScript, we are decoupling
the CSS implementations from JavaScript. We could, however, control all
the presentation logic within JavaScript by using:
FOCUS_PAGE
.
style
.
transform
=
"translate(X,Y)"
;
Each browser vendor may be using a specific vendor prefix for the transform capabilities. One quick way of checking to see what your target browser supports is to use:
var
getTransformProperty
=
function
(
node
)
{
var
properties
=
[
'transform'
,
'WebkitTransform'
,
'msTransform'
,
'MozTransform'
,
'OTransform'
];
var
p
;
while
(
p
=
properties
.
shift
())
{
if
(
typeof
node
.
style
[
p
]
!=
'undefined'
)
{
document
.
querySelector
(
"#log"
).
innerHTML
+=
p
+
"<br/>"
;
}
}
return
false
;
};
This slide effect has been tested on Mobile Safari, Android, Mobile Firefox (Figure 3-2), and Opera Mobile (Figure 3-3).You can also see the source code that supports all the aforementioned browsers.
On mobile devices, flipping is characterized by actually swiping the page away. Here you can use some simple JavaScript to handle the event on iOS and Android (WebKit-based) devices. Here is an example of flipping in action and the source at github.
When dealing with touch events and transitions, the first thing
you’ll want is to get a handle on the current position of the element.
Thanks to the CSSMatrix interface, which is implemented by WebKit only
at the time of this writing, you can get an instance of WebKitCSSMatrix
by passing the current
transform’s computed style.
function
pageMove
(
event
)
{
// get position after transform
var
curTransform
=
new
WebKitCSSMatrix
(
window
.
getComputedStyle
(
page
).
webkitTransform
);
var
pagePosition
=
curTransform
.
m41
;
}
Because we are using a CSS3 ease-out transition for the page flip,
the usual element.offsetLeft
will not
work.
For more information on WebKitCSSMatrix
, go to Apple's
developer page.
Next we want to figure out which direction the user is flipping and set a threshold for an event (page navigation) to take place.
if
(
pagePosition
>=
0
)
{
//moving current page to the right
//so means we're flipping backwards
if
((
pagePosition
>
pageFlipThreshold
)
||
(
swipeTime
<
swipeThreshold
))
{
//user wants to go backward
slideDirection
=
'right'
;
}
else
{
slideDirection
=
null
;
}
}
else
{
//current page is sliding to the left
if
((
swipeTime
<
swipeThreshold
)
||
(
pagePosition
<
pageFlipThreshold
))
{
//user wants to go forward
slideDirection
=
'left'
;
}
else
{
slideDirection
=
null
;
}
}
You’ll also notice that we are measuring the swipeTime
in milliseconds as well. This allows
the navigation event to fire if the user quickly swipes the screen to
turn a page.
To position the page and make the animations look native while a finger is touching the screen, we use CSS3 transitions after each event firing.
function
positionPage
(
end
)
{
page
.
style
.
webkitTransform
=
'translate3d('
+
currentPos
+
'px, 0, 0)'
;
if
(
end
)
{
page
.
style
.
WebkitTransition
=
'all .4s ease-out'
;
//page.style.WebkitTransition = 'all .4s cubic-bezier(0,.58,.58,1)'
}
else
{
page
.
style
.
WebkitTransition
=
'all .2s ease-out'
;
}
For this example, ease-out
did the
trick, but for your own projects, play around with cubic-bezier
to give
the best native feel to your transitions.
Finally, to make the navigation happen, we must call the
previously defined slideTo()
methods used
in the last example.
track
.
ontouchend
=
function
(
event
)
{
pageMove
(
event
);
if
(
slideDirection
==
'left'
)
{
slideTo
(
'products-page'
);
}
else
if
(
slideDirection
==
'right'
)
{
slideTo
(
'home-page'
);
}
}
Next, take a look at the rotate animation being used in
this demo. At any time, you can rotate the page you’re currently viewing
180 degrees to reveal the reverse side by tapping on the Contact menu
option. Again, this only takes a few lines of CSS and some JavaScript to
assign a transition class onclick
.
The rotate transition isn’t rendered correctly on most versions of Android, because it lacks 3D CSS transform capabilities. Unfortunately, instead of ignoring the flip, Android makes the page “cartwheel” away by rotating instead of flipping. I recommend using this transition sparingly until support improves.
Here's the full source, but here’s the markup (basic concept of front and back):
<div
id=
"front"
class=
"normal"
>
...</div>
<div
id=
"back"
class=
"flipped"
>
<div
id=
"contact-page"
class=
"page"
>
<h1>
Contact Page</h1>
</div>
</div>
The JavaScript you need is:
function
flip
(
id
)
{
// get a handle on the flippable region
var
front
=
getElement
(
'front'
);
var
back
=
getElement
(
'back'
);
// again, just a simple way to see what the state is
var
classes
=
front
.
className
.
split
(
' '
);
var
flipped
=
classes
.
indexOf
(
'flipped'
);
if
(
flipped
>=
0
)
{
// already flipped, so return to original
front
.
className
=
'normal'
;
back
.
className
=
'flipped'
;
FLIPPED
=
false
;
}
else
{
// do the flip
front
.
className
=
'flipped'
;
back
.
className
=
'normal'
;
FLIPPED
=
true
;
}
}
Finally, here is the relevant CSS:
#back
,
#front
{
position
:
absolute
;
width
:
100
%
;
height
:
100
%
;
-
webkit
-
backface
-
visibility
:
hidden
;
-
webkit
-
transition
-
duration
:
.5s
;
-
webkit
-
transform
-
style
:
preserve
-3
d
;
-
moz
-
backface
-
visibility
:
hidden
;
-
moz
-
transform
-
style
:
preserve
-3
d
;
-
moz
-
transition
-
duration
:
.5s
;
}
.normal
{
-
webkit
-
transform
:
rotateY
(
0
deg
);
-
moz
-
transform
:
rotateY
(
0
deg
);
}
.flipped
{
-
webkit
-
user
-
select
:
element
;
-
webkit
-
transform
:
rotateY
(
180
deg
);
-
moz
-
transform
:
rotateY
(
180
deg
);
}
With the code of the basic transitions covered, take a look at the mechanics of how the transitions run on the device and are composited. Here are a few tips to remember when using accelerated compositing:
Reduce the quantity of layers
Keep layers as small as possible
Update layers infrequently
Tailor layer compositing to your purpose
Use trial and error; testing is important
To begin debugging, fire up a couple of WebKit-based browsers and your IDE of choice.
First, start Safari from the command line to make use of some debugging environment variables. I use a Mac, so the example commands might differ from those for your OS. Open the Terminal, and type the following (or just skip Safari and use the Chrome settings in next section):
$> export CA_COLOR_OPAQUE=1 $> export CA_LOG_MEMORY_USAGE=1 $> /Applications/Safari.app/Contents/MacOS/Safari
These lines start Safari with a couple of debugging helpers.
CA_COLOR_OPAQUE
shows you which
elements are actually composited or accelerated, while CA_LOG_MEMORY_USAGE
shows you how much
memory you are using when sending drawing operations to the backing
store. This tells you exactly how much strain you are putting on the
mobile device and possibly give hints to how your GPU usage might be
draining the target device’s battery.
You may also start Safari after running the following command, which gives you a full Debug menu with all available options, as shown in Figure 3-4:
defaults write com.apple.Safari IncludeInternalDebugMenu 1
Now fire up Chrome to see some good frames per second (FPS) information and borders around the composited layers:
Open the Google Chrome web browser.
In the URL bar, type about:flags
.
Scroll down a few items, and click Enable for the FPS counter as shown in Figure 3-5.
Do not enable the “GPU compositing on all pages” option. The FPS counter appears in the left corner only if the browser detects compositing in your markup—and that is what you want in this case.
If you view this page in your souped-up version of Chrome, you will see the red FPS counter in the top-left corner, as shown in Figure 3-6.
This is how you know hardware acceleration is turned on. It also gives you an idea of how the animation runs and whether you have any leaks (continuous running animations that should be stopped).
Another way to visualize the hardware acceleration is to open
the same page in Safari with the environment variables mentioned
above. Every accelerated DOM element will have a red tint to it. This
shows you exactly what is being composited by each layer, or
accelerated div
element. Notice in
Figure 3-7, the white
navigation is not red because it is not accelerated.
A similar setting for Chrome is also available in the about:flags tab: Click Enable for “Composited render layer borders.”
Another great way to see an example of composited layers is to
view the WebKit falling leaves demo (http://www.webkit.org/blog-files/leaves/) while
CA_COLOR_OPAQUE=1
is applied. Figure 3-8 shows the results.
Finally, to truly understand the graphics hardware performance of an application, look at how memory is being consumed. Here you can see that the app is pushing 1.38MB of drawing instructions to the CoreAnimation buffers on Mac OS. The CoreAnimation memory buffers are shared between OpenGL ES and the GPU to create the final pixels you see on the screen (Figure 3-9).
When you simply resize or maximize the browser window, you can see the memory expand as well (Figure 3-10).
Using the previous debugging techniques gives you an idea of how memory is being consumed on your mobile device only if you resize the browser to the correct dimensions. When debugging or testing for iPhone environments, for example, resize to 480 by 320 pixels.
This section illustrated how hardware acceleration works and what it takes to debug memory issues or other hardware accelerated glitches. It’s one thing to read about it, but to actually see the GPU memory buffers working visually really brings things into perspective.
Now it’s time to take your page and resource caching to the next level. Much like the approach that jQuery Mobile and similar frameworks use, you can prefetch and cache your pages with concurrent AJAX calls. A few core mobile web challenges highlight the reasons why following this approach makes sense:
Prefetching your pages allows users to take the app offline and also eliminates waiting between navigation actions. Of course, you don’t want to choke the device’s bandwidth when the device comes online, so you need to use this feature sparingly.
You want a concurrent or asynchronous approach when
fetching and caching pages. Because it’s well supported among
devices, you also need to use localStorage
, which unfortunately isn’t
asynchronous.
Using innerHTML()
to insert the AJAX
response into the DOM is dangerous, and it could be unreliable according to http://martinkou.blogspot.com/2011/05/alternative-workaround-for-mobile.html.
Instead, I recommend a reliable mechanism for AJAX response insertion
and handling concurrent calls (http://community.jboss.org/people/wesleyhales/blog/2011/08/28/fixing-ajax-on-mobile-devices). You also can leverage some new features of HTML5 for parsing the xhr.responseText
.
You can build on the code from the slide, flip, and rotate demos by adding some secondary pages and linking to them. You can then parse the links and create transitions on the fly.
<div
id=
"home-page"
class=
"page"
>
<h1>
Home Page</h1>
<a
href=
"demo2/home-detail.html"
class=
"fetch"
>
Find out more about the home page!</a>
</div>
As you can see, this snippet leverages semantic markup with a link
to another page. The child page follows the same node/class structure as
its parent. You could take this a step further and use the data-*
attribute for page
nodes, and the like.
Here is the detail page (child) located in a separate HTML file (/demo2/home-detail.html), which will be loaded, cached, and set up for transition on app load.
<div
id=
"home-page-detail"
class=
"page"
>
<h1>
Home Page Details</h1>
<p>
Here are the details.</p>
</div>
Now take a look at the JavaScript. For simplicity’s sake, I’m leaving any helpers or optimizations out of the code. The code is looping through a specified array of DOM nodes to dig out links to fetch and cache. For the complete source, see https://github.com/html5e/slidfast/blob/master/slidfast.js#L264.
var
fetchAndCache
=
function
()
{
// iterate through all nodes in this DOM to
//find all mobile pages we care about
var
pages
=
document
.
getElementsByClassName
(
'page'
);
for
(
var
i
=
0
;
i
<
pages
.
length
;
i
++
)
{
// find all links
var
pageLinks
=
pages
[
i
].
getElementsByTagName
(
'a'
);
for
(
var
j
=
0
;
j
<
pageLinks
.
length
;
j
++
)
{
var
link
=
pageLinks
[
j
];
if
(
link
.
hasAttribute
(
'href'
)
&&
//'#' in the href tells us that this page is
//already loaded in the DOM - and
// that it links to a mobile transition/page
!
(
/[\#]/g
).
test
(
link
.
href
)
&&
//check for an explicit class name setting to fetch this link
(
link
.
className
.
indexOf
(
'fetch'
)
>=
0
))
{
//fetch each url concurrently
var
ai
=
new
ajax
(
link
,
function
(
text
,
url
){
//insert the new mobile page into the DOM
insertPages
(
text
,
url
);
});
ai
.
doGet
();
}
}
}
};
The use of the AJAX object ensures proper asynchronous post-processing. In this example, you see the basic use of caching on each request and of providing the cached objects when the server returns anything but a successful (200) response.
function
processRequest
()
{
if
(
req
.
readyState
==
4
)
{
if
(
req
.
status
==
200
)
{
if
(
supports_local_storage
())
{
localStorage
[
url
]
=
req
.
responseText
;
}
if
(
callback
)
callback
(
req
.
responseText
,
url
);
}
else
{
// There is an error of some kind, use our
//cached copy (if available).
if
(
!!
localStorage
[
url
])
{
// We have some data cached, return that to the callback.
callback
(
localStorage
[
url
],
url
);
return
;
}
}
}
}
Unfortunately, because localStorage
uses UTF-16 for character encoding,
each single byte is stored as 2 bytes, bringing our storage limit from 5MB
to 2.6MB total. Fetching and caching these pages/markup outside of the
application cache scope allows you to take advantage of all the storage
space provided by the device.
With the recent advances in the iframe
element with
HTML5, you now have a simple and effective way to parse the responseText
you get back from an AJAX call.
There are plenty of 3,000-line JavaScript parsers and regular expressions
that remove script tags and so on. But why not let the browser do what it
does best? The next example writes the responseText
into a temporary hidden iframe
. This uses the HTML5 sandbox
attribute, which disables scripts and
offers many security features. (See complete source.)
To quote the HTML5 spec: “The sandbox
attribute,
when specified, enables a set of extra restrictions on any content
hosted by the iframe
. Its value must
be an unordered set of unique space-separated tokens that are ASCII
case-insensitive. The allowed values are allow-forms
, allow-same-origin
, allow-scripts
, and allow-top-navigation
. When the attribute is
set, the content is treated as being from a unique origin, forms and
scripts are disabled, links are prevented from targeting other browsing
contexts, and plug-ins are disabled. To limit the damage that can be
caused by hostile HTML content, it should be served using the text/html-sandboxed
MIME type.”
var
getFrame
=
function
()
{
var
frame
=
document
.
getElementById
(
"temp-frame"
);
if
(
!
frame
)
{
// create frame
frame
=
document
.
createElement
(
"iframe"
);
frame
.
setAttribute
(
"id"
,
"temp-frame"
);
frame
.
setAttribute
(
"name"
,
"temp-frame"
);
frame
.
setAttribute
(
"seamless"
,
""
);
frame
.
setAttribute
(
"sandbox"
,
"allow-same-origin"
);
frame
.
style
.
display
=
'none'
;
document
.
documentElement
.
appendChild
(
frame
);
}
// load a page
return
frame
.
contentDocument
;
};
var
insertPages
=
function
(
text
,
originalLink
)
{
var
frame
=
getFrame
();
//write the ajax response text to the frame and let
//the browser do the work
frame
.
write
(
text
);
//now we have a DOM to work with
var
incomingPages
=
frame
.
getElementsByClassName
(
'page'
);
var
pageCount
=
incomingPages
.
length
;
for
(
var
i
=
0
;
i
<
pageCount
;
i
++
)
{
//the new page will always be at index 0 because
//the last one just got popped off the stack with
//appendChild (below)
var
newPage
=
incomingPages
[
0
];
//stage the new pages to the left by default
newPage
.
className
=
'page stage-left'
;
//find out where to insert
var
location
=
newPage
.
parentNode
.
id
==
'back'
?
'back'
:
'front'
;
try
{
// mobile safari will not allow nodes to be transferred from one
// DOM to another so we must use adoptNode()
document
.
getElementById
(
location
).
appendChild
(
document
.
adoptNode
(
newPage
));
}
catch
(
e
)
{
// todo graceful degradation?
}
}
};
The target browser (Mobile Safari) correctly refuses to implicitly
move a node from one document to another. An error is raised if the new
child node was created in a different document. So this example uses
adoptNode
, and all is
well.
So why iframe
? Why not
just use innerHTML
? Even though
innerHTML
is now part of the HTML5
spec, it is a dangerous practice to insert the response from a server
(evil or good) into an unchecked area. innerHTML
has also been noted to fail
intermittently on iOS (just do a Google search on “ios innerhtml” to see
the latest results) so it’s best to have a good workaround when the time
comes.
Figure 3-11 shows
the latest performance test from http://jsperf.com/ajax-response-handling-innerhtml-vs-sandboxed-iframe.
It shows that this sandboxed iframe
approach is just as fast, if not faster than innerHTML
on many of today’s top mobile
browsers. Keep in mind the measurement is operations per second, so higher
scores are better.
Now that we have the ability to buffer (or predictive cache) the example web app, we must provide the proper connection detection features to make the app smarter. This is where mobile app development gets extremely sensitive to online/offline modes and connection speed. Enter the Network Information API. With it, you can set up an extremely smart mobile web app.
When would this be useful? Suppose someone on a high-speed train is using your app to interact with the Web. As the train rushes along, the network may very well go away at various moments, and various locales may support different transmission speeds (HSPA or 3G might be available in some urban areas, while remote areas might support much slower 2G technologies only). Not only does the following code address connection scenarios like this, it also:
Provides offline access through applicationCache
Detects if bookmarked and offline
Detects when switching from offline to online and vice versa
Detects slow connections and fetches content based on network type
Again, all of these features require very little code. The first step is detect the events and loading scenarios (see https://github.com/html5e/slidfast/blob/master/slidfast.js#L536):
window
.
addEventListener
(
'load'
,
function
(
e
)
{
if
(
navigator
.
onLine
)
{
// new page load
processOnline
();
}
else
{
// the app is probably already cached and (maybe) bookmarked...
processOffline
();
}
},
false
);
window
.
addEventListener
(
"offline"
,
function
(
e
)
{
// we just lost our connection and entered offline mode,
// disable external link
processOffline
(
e
.
type
);
},
false
);
window
.
addEventListener
(
"online"
,
function
(
e
)
{
// just came back online, enable links
processOnline
(
e
.
type
);
},
false
);
In the EventListener
statements
above, we must tell the code if it is being called from an event or an
actual page request or refresh. The main reason is because the body onload
event won’t
be fired when switching between the online and offline modes.
The simple check for an online
or onload
event below
resets disabled links when switching from offline to online. For a more
sophisticated app, you could also insert logic that would resume fetching
content or handle the UX for intermittent connections.
function
processOnline
(
eventType
)
{
setupApp
();
checkAppCache
();
// reset our once disabled offline links
if
(
eventType
)
{
for
(
var
i
=
0
;
i
<
disabledLinks
.
length
;
i
++
)
{
disabledLinks
[
i
].
onclick
=
null
;
}
}
}
For the processOffline()
function, you could manipulate your app for offline mode and try to
recover any transactions that were going on behind the scenes. The code
below crawls the DOM for all of the external links and disables them,
trapping users in our offline app—forever!
function
processOffline
()
{
setupApp
();
// disable external links until we come back
// setting the bounds of app
disabledLinks
=
getUnconvertedLinks
(
document
);
// helper for onlcick below
var
onclickHelper
=
function
(
e
)
{
return
function
(
f
)
{
alert
(
'This app is currently offline and cannot access the hotness'
);
return
false
;
}
};
for
(
var
i
=
0
;
i
<
disabledLinks
.
length
;
i
++
)
{
if
(
disabledLinks
[
i
].
onclick
==
null
)
{
//alert user we're not online
disabledLinks
[
i
].
onclick
=
onclickHelper
(
disabledLinks
[
i
].
href
);
}
}
}
Okay, suppress your evil genius laugh, and let’s get on to the good stuff. Now that the app knows what connected state it’s in, we can also check the type of connection when it’s online and adjust accordingly with the code below. In the comments, I listed typical North American providers’ download and latencies for each connection.
function
setupApp
(){
// create a custom object if navigator.connection isn't available
var
connection
=
navigator
.
connection
||
{
'type'
:
'0'
};
if
(
connection
.
type
==
2
||
connection
.
type
==
1
)
{
//wifi/ethernet
//Coffee Wifi latency: ~75ms-200ms
//Home Wifi latency: ~25-35ms
//Coffee Wifi DL speed: ~550kbps-650kbps
//Home Wifi DL speed: ~1000kbps-2000kbps
fetchAndCache
(
true
);
}
else
if
(
connection
.
type
==
3
)
{
//edge
//ATT Edge latency: ~400-600ms
//ATT Edge DL speed: ~2-10kbps
fetchAndCache
(
false
);
}
else
if
(
connection
.
type
==
2
)
{
//3g
//ATT 3G latency: ~400ms
//Verizon 3G latency: ~150-250ms
//ATT 3G DL speed: ~60-100kbps
//Verizon 3G DL speed: ~20-70kbps
fetchAndCache
(
false
);
}
else
{
//unknown
fetchAndCache
(
true
);
}
}
There are numerous adjustments you could make to the fetchAndCache
process, but the example code
simply tells it to fetch the resources asynchronous (true) or synchronous
(false) for a given connection. To see how this works in practice,
consider the edge (synchronous) request timeline shown in Figure 3-12 and the WiFi (asynchronous)
request timeline shown in Figure 3-13.
The example code allows for at least some method of user experience adjustment based on slow or fast connections, but it is by no means an end-all-be-all solution. Another improvement would be to throw up a loading modal when a link is clicked (on slow connections) while the app still may be fetching that link’s page in the background. Your overall goal is to cut down on latencies while leveraging the full capabilities of the user’s connection with the latest and greatest HTML5 has to offer. You can view the network detection demo at http://html5e.org/example.
It seems like there’s a new JavaScript-based mobile framework popping up every day. You can literally spend days (or months) comparing frameworks and whipping up multiple proofs-of-concept (POCs), only to find out that you may not want or need a framework at all.
In the majority of situations, whether converting an existing app or starting from scratch, you’re better off writing your own CSS and DOM interactions. The harder you lean on a framework, the harder your app will fall when problems arise. Knowing the basics and how to fix those problems under the hood are essential. The DOM is the underlying infrastructure and API for all web apps. No matter how much you like or dislike the API, if you desire a mobile web app that screams at blazing fast speeds and gets “close to the metal,” you must understand how to work with it.
One commonly used programming model for the Mobile Web is called
single page. This means you put your entire markup
into a single HTML page, often enclosed by a <div>
or some other sensible block
element, as in this sample single-page web app structure:
<!DOCTYPE html>
<html
lang=
"en"
dir=
"ltr"
>
<body>
<div
id=
"home-page"
>
...page content</div>
<div
id=
"contact-page"
>
...page content</div>
</body>
</html>
Why put everything in one page? Primarily, it buys you native-like transitions and fewer initial HTTP requests. You must use AJAX and CSS3 transitions to emulate the feel of a native application and load data dynamically. This single-page approach also promotes including all your resources, such as JavaScript and CSS, within the file. Again, this reduces additional HTTP requests to get the best performance possible from your mobile application.
With an understanding of the basics, consider a few mobile-focused JavaScript frameworks that try to take care of the heavy lifting on the UI. Most of today’s JavaScript frameworks have a specific browser or platform they’re targeting. Some are WebKit-only and others try to span all device browsers. There may be features you need, and ones you don’t. So it’s up to you to decide when to bring any framework into your current or existing project.
Some mobile frameworks extend or build on older, bloated, desktop-browser frameworks. Be careful that whichever framework you choose does not check for older IE6 bugs or platforms that you aren’t targeting. This bloat may seem minimal to some, but as you will see in the next chapter, every byte you can shave off the initial load time will greatly enhance the user experience.
When evaluating mobile JavaScript frameworks, look for:
Optimization for touch screen devices; make sure the framework uses CSS3 transitions to handle animations
Cross-platform consistency across all the major platform (Grade A and B) browsers
Use (or wrapping) of the latest HTML5 and CSS3 standards
Strong open source community behind the framework
Finally, investigate the programming model uses and ask yourself: does my project require a dynamically generated UI through JavaScript, or do I want to declare my markup beforehand in the single-page approach?
The framework smackdown in the following sections provides an overview of the three main approaches to mobile web apps development: single page, no page structure, and 100% JavaScript-driven.
As previously mentioned, the single-page approach forces you to put as much markup and resources as possible into a single HTML file. In the end, this limits HTTP requests for a better performing app. The leaders here are jQuery Mobile and jQTouch.
jQuery Mobile (http://jquerymobile.com; demo at http://jquerymobile.com/test) is strictly tied to the release schedule of the core jQuery library. Known for its AJAX-based navigation system and themeable ThemeRoller designs, the framework is produced by the core jQuery project. It also has an attractive set of widgets, but unfortunately, they’re all decorated with CSS background gradients, text shadows, rounded corners, and drop shadows. As you’ll see in the coming chapters, heavy use of CSS decorations in mobile web apps can slow the browser to a crawl.
jQuery Mobile is the most popular mobile web framework out there today. Taking into account its over 10,000 followers on Twitter and more than 6,000 watchers on github (Figure 3-14), you can easily see the power piggy-backing on an existing project’s success (in this case, core jQuery) to catapult a project into the mainstream. The real power and strength of this project comes from its community. Table 3-1 gives a high-level snapshot of the jQuery Mobile project.
Table 3-1. jQuery Mobile
Platform support | Android, bada, BlackBerry, iOS, MeeGo, Symbian, webOS, and Windows Phone (others are graded at different levels of 49 support) |
License | Dual license MIT or GPL 2 |
Programming model | CSS and JavaScript:
declarative on the DOM itself; markup with CSS and |
Wrapped or polyfilled HTML5 APIs | None |
To set up the page, use the code:
<!DOCTYPE html>
<html>
<head>
<title>
My Page</title>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1"
>
<link
rel=
"stylesheet"
href=
"/jquery.mobile-1.0.min.css"
/>
<script
type=
"text/javascript"
src=
"/jquery-1.6.4.min.js"
></script>
<script
type=
"text/javascript"
src=
" /jquery.mobile-1.0.min.js"
></script>
</head>
<body>
<div
data-role=
"page"
>
<div
data-role=
"header"
>
<h1>
My Title</h1>
</div>
<!-- /header -->
<div
data-role=
"content"
>
<p>
Hello world</p>
</div>
<!-- /content -->
</div>
<!-- /page -->
</body>
</html>
To set up a component such as the one shown in Figure 3-15, use:
<ul
data-role=
"listview"
data-inset=
"true"
data-filter=
"true"
>
<li><a
href=
"#"
>
Acura</a></li>
<li><a
href=
"#"
>
Audi</a></li>
<li><a
href=
"#"
>
BMW</a></li>
<li><a
href=
"#"
>
Cadillac</a></li>
<li><a
href=
"#"
>
Ferrari</a></li>
</ul>
jQTouch is a Zepto/jQuery plug-in and a good, simple framework to get started with quickly. It offers a basic set of widgets and animations but lacks support for multiple platforms. The framework also suffers from slow, flickering animations and delayed tap events. Supporting only iOS and Android, jQTouch is the second most popular framework on the interwebs with more than 9,000 Twitter followers and a nice following on github (Figure 3-16). However, the commit history in github looks a little sparse, with six-month gaps at times. Table 3-2 outlines its features. (Check out the jQTouch demo.)
Table 3-2. jQTouch
Platform support | Android and iOS only |
License | MIT |
Programming model | Heavy CSS, light JavaScript; uses CSS classes for detecting the appropriate animations and interactions; extensions supported |
Wrapped or polyfilled HTML5 APIs | None |
To set up the page, use the code:
<html>
<head>
<Title>
My App</title>
</head>
<body>
<div
id=
"home"
>
<div
class=
"toolbar"
>
<H1>
Hello World</h1>
</div>
<ul
class=
"edgetoedge"
>
<li
class=
"arrow"
><a
href=
"#item1"
>
Item 1</a></li>
</ul>
</div>
</body>
</html>
To set up the component shown in Figure 3-17, use:
<ul
class=
"edgetoedge"
>
<li
class=
"arrow"
><a
id=
"0"
href=
"#date"
>
Today</a></li>
<li
class=
"arrow"
><a
id=
"1"
href=
"#date"
>
Yesterday</a></li>
<li
class=
"arrow"
><a
id=
"2"
href=
"#date"
>
2 Days Ago</a></li>
<li
class=
"arrow"
><a
id=
"3"
href=
"#date"
>
3 Days Ago</a></li>
<li
class=
"arrow"
><a
id=
"4"
href=
"#date"
>
4 Days Ago</a></li>
<li
class=
"arrow"
><a
id=
"5"
href=
"#date"
>
5 Days Ago</a></li>
</ul>
In the no-page-structure approach, the markup is flexible and lightweight. Unlike the single-page approach, markup here is not tied to a specific DOM structure. Your best option for using this method is xui.
Born from the PhoneGap framework, xui (http://xuijs.com) does not try and dictate a page structure or widget paradigm. Instead, xui handles events, animations, transforms, and AJAX. It prides itself in being lightweight with the ability to add plug-ins for needed features.
xui is specifically tailored for DOM manipulation in a mobile environment. This is an important factor when dealing with existing desktop browser based frameworks like jQuery. With xui, you get a 10kb JavaScript file that gives you a very useful programming model. Brian Leroux is the author of xui and is well known within the HTML5/Open Web community. One notable thing about this project is the all-star list of contributors to the code: Rebecca Murphey, Remy Sharp, Fil Maj, Alex Sexton, Joe McCann, and many others. So, point being, sometimes it helps to judge a project’s value by who the contributors and founders are opposed to how many followers it has (Figure 3-18). Table 3-3 outlines its stats.
If you prefer to create your user interface programmatically, without touching much markup, then the 100% JavaScript-driven approach may be your best option. Out of this approach, Sencha Touch, Wink Toolkit, and The-M-Project are three of the top projects.
An HTML/CSS3/JavaScript framework, Sencha Touch offers a variety of native-style widgets, flexible theming via SASS/Compass, data-feature-like models, stores, and proxies. Enhanced touch events and a strong data model give this framework a bit of an enterprise edge without a ton of coverage across devices (see Table 3-4). Although not in a github repository, Sencha Touch currently has around 800 followers on Twitter. (See the Sencha Touch demo.)
If you choose Sencha Touch, be aware it is a specific way of life for mobile developers. Much like GWT or JSF, you are tied to a specific development model for creating user interfaces. In jQTouch or jQuery Mobile, you write specially structured HTML. When it loads, the library reconfigures the page and turns your regular links into AJAX-based animated ones. With Sencha, you basically don’t write HTML at all, but instead, you build your UI and app with JavaScript, so be prepared for a learning curve.
Table 3-4. Sencha Touch
Platform support | Android, iOS, and BlackBerry (from Sencha 1.1) |
License | GPLv3, Limited Touch Commercial License |
Programming model | Very little HTML; relies on writing, subclassing, and instantiating JavaScript objects |
Wrapped or polyfilled HTML5 APIs | Geolocation, Web Storage |
For your page setup, use the code:
<!DOCTYPE html>
<html>
<head>
<meta
http-equiv=
"Content-Type"
content=
"text/html; charset=utf-8"
>
<meta
name=
"viewport"
content=
"width=device-width; initial-scale=1.0;
maximum-scale=1.0; minimum-scale=1.0; user-scalable=0;"
/>
<link
rel=
"stylesheet"
href=
"/sencha-touch.css"
type=
"text/css"
>
<title>
List</title>
<script
type=
"text/javascript"
src=
"/sencha-touch.js"
></script>
</head>
<body></body>
</html>
JavaScript handles setup of your component, as well as the entire app (Figure 3-19):
Ext
.
setup
({
tabletStartupScreen
:
'tablet_startup.png'
,
phoneStartupScreen
:
'phone_startup.png'
,
icon
:
'icon.png'
,
glossOnIcon
:
false
,
onReady
:
function
()
{
Ext
.
regModel
(
'Contact'
,
{
fields
:
[
'firstName'
,
'lastName'
]
});
var
groupingBase
=
{
itemTpl
:
'<div class="contact2"><strong>{firstName}</strong>
{lastName}</div>'
,
selModel
:
{
mode
:
'SINGLE'
,
allowDeselect
:
true
},
grouped
:
true
,
indexBar
:
false
,
onItemDisclosure
:
{
scope
:
'test'
,
handler
:
function
(
record
,
btn
,
index
)
{
alert
(
'Disclose more info for '
+
record
.
get
(
'firstName'
));
}
},
store
:
new
Ext
.
data
.
Store
({
model
:
'Contact'
,
sorters
:
'firstName'
,
getGroupString
:
function
(
record
)
{
return
record
.
get
(
'firstName'
)[
0
];
},
data
:
[
{
firstName
:
'Hello'
,
lastName
:
'World'
},
]
})
};
if
(
!
Ext
.
is
.
Phone
)
{
new
Ext
.
List
(
Ext
.
apply
(
groupingBase
,
{
floating
:
true
,
width
:
350
,
height
:
370
,
centered
:
true
,
modal
:
true
,
hideOnMaskTap
:
false
})).
show
();
}
else
{
new
Ext
.
List
(
Ext
.
apply
(
groupingBase
,
{
fullscreen
:
true
}));
}
}
})
The Wink Toolkit project started in early 2009 at Orange Labs (France Telecom R&D). Since June 2010, Wink has been a project of the Dojo foundation. Wink’s core offers all the basic functionalities a mobile developer would need from touch event handling to DOM manipulation objects and CSS transforms utilities (Table 3-5). Additionally, it offers a wide range of UI components. Currently, its online following is low, as evidenced in Figure 3-20. (See their demo).
The coolest thing about Wink is their vast set of 2D and 3D components, and the ability to manipulate components with gestures. For example, with Wink’s Cover Flow component, the user can use two fingers to alter the perspective (Figure 3-21).
Table 3-5. Wink Toolkit
Platform support | iOS, Android, BlackBerry, and Bada |
License | Simplified BSD License |
Programming model | JavaScript helpers to add standard mobile browser support; UI is created inside of JavaScript snippets |
Wrapped or polyfilled HTML5 APIs | Accelerometer, Geolocation, Web Storage |
The HTML for page setup is:
<html>
<head>
<link
rel=
"stylesheet"
href=
"wink.css"
type=
"text/css"
>
<link
rel=
"stylesheet"
href=
"wink.default.css"
type=
"text/css"
>
...<script
type=
"text/javascript"
src=
"wink.min.js"
></script>
...</head>
<body
onload=
"init()"
>
<div
class=
"w_box w_header w_bg_dark"
>
<span
id=
"title"
>
accordion</span>
<input
type=
"button"
value=
"home"
class=
"w_button w_radius w_bg_light w_right"
onclick=
"window.location='..?theme='+theme"
/>
</div>
<div
class=
"w_bloc"
>
click on the accordion section below to display the content.</div>
<div
id=
"output"
style=
"width: 95%; margin: auto"
>
</div>
</body>
</html>
To set up the component shown in Figure 3-22, use:
var
accordion
,
section1
,
section2
,
section3
;
init
=
function
()
{
accordion
=
new
wink
.
ui
.
layout
.
Accordion
();
section1
=
accordion
.
addSection
(
'Section1'
,
'Hello World'
);
section2
=
accordion
.
addSection
(
'section2'
,
'...'
);
section3
=
accordion
.
addSection
(
'section3'
,
'...'
);
$
(
'output'
).
appendChild
(
accordion
.
getDomNode
());
}
deleteSection
=
function
()
{
accordion
.
deleteSection
(
section2
);
The-M-Project (their demo ) is built on top of jQuery and jQuery Mobile. It uses concepts and parts from SproutCore and bases its persistence handling on persistence.js. Figure 3-23 gives a snapshot of its community following.
Because The-M-Project UI looks exactly like jQuery Mobile, it’s hard to tell at first glance what the big difference is. The project, however, is much more than a shiny UI framework. It has four core development concepts: MVC, Content Binding, Dynamic Value Computing, and Event Handling. So unlike the UI-focused Wink Toolkit, The-M-Project puts most of its focus on the programming model, as you can see in Table 3-6.
Table 3-6. The-M-Project
Platform support | iOS, Android, WebOS, BlackBerry, Windows Phone |
License | GPLv2 and MIT |
Programming model | Relies heavily on MVC pattern; creates view components through JavaScript and addresses data binding |
Wrapped or polyfilled HTML5 APIs | Web Storage (DataProvider for local and remote storage persistence) |
A bit of JavaScript handles page setup:
PageSwitchDemo
.
Page1
=
M
.
PageView
.
design
({
childViews
:
'header content'
,
header
:
M
.
ToolbarView
.
design
({
value
:
'Page 1'
}),
content
:
M
.
ScrollView
.
design
({
childViews
:
'button'
,
button
:
M
.
ButtonView
.
design
({
value
:
'Goto Page 2'
,
events
:
{
tap
:
{
target
:
PageSwitchDemo
.
ApplicationController
,
action
:
'gotoPage2'
}
}
})
})
});
To create the component shown in Figure 3-24, use:
M
.
SelectionListView
.
design
({
childViews
:
'item1 item2 item3 item4'
,
/* renders a selection view like radio buttons */
selectionMode
:
M
.
SINGLE_SELECTION
,
item1
:
M
.
SelectionListItemView
.
design
({
value
:
'item1'
,
label
:
'Item 1'
,
isSelected
:
YES
}),
item2
:
M
.
SelectionListItemView
.
design
({
value
:
'item2'
,
label
:
'Item 2'
}),
item3
:
M
.
SelectionListItemView
.
design
({
value
:
'item3'
,
label
:
'Item 3'
}),
item4
:
M
.
SelectionListItemView
.
design
value
:
'item4'
,
label
:
'Item 4'
})
});
Of course, many other frameworks—SproutCore, Jo, Zepto, LungoJS, the list goes on—are available. All of these frameworks contain useful features and building blocks for everyday programming of mobile web apps. Some even try to create a wrapper or proxy for spec-driven features like Web Storage. But, it seems they all have a gaping hole in terms of the needs of enterprise developers and a consistent architecture across device browsers.
In the world of desktop-based web development, we have many tools at our disposal for debugging. Firebug and Chrome’s developer tools are a few that help us get the job done faster. For mobile, the situation is much different, and we must remotely debug through third-party tools. Luckily, projects like weinre, Adobe Shadow, and Opera’s Remote Debugging tools try to give developers the same debugging experience as desktop environments.
Like FireBug for FireFox and Web Inspector for WebKit-based browsers, weinre (http://people.apache.org/~pmuellr/weinre) is a debugger for web pages. Its difference is that it is designed to work remotely and, in particular, to allow you debug web pages on a mobile device, such as a phone. If you’ve used Safari’s Web Inspector or Chrome’s Developer Tools, weinre will be very familiar (Figure 3-25).
For debug clients, weinre supports:
weinre Mac application (Mac OS X 10.6 64-bit)
Google Chrome 8.x
Apple Safari 5.x
For debug targets, weinre supports:
Android 2.2 Browser application
Android 2.2 with PhoneGap 0.9.2
iOS 4.2.x Mobile Safari application
BlackBerry v6.x simulator
webOS 2.x (unspecified version)
Shown in Figure 3-26, Adobe Shadow is an inspection and preview tool that streamlines the preview process for Android and iOS mobile devices. After installing Shadow on your computer, you’ll be able to wirelessly pair your devices, have them browse in sync with your computer, and perform remote inspection and debugging so you can see HTML/CSS/JavaScript changes instantly on your device. Some of Shadow’s features include the ability to:
Wirelessly pair your iOS devices to your computer
Synchronously browse with your computer
Target a device for debugging and select an element in the DOM
Make changes to your HTML markup
Tweak your CSS rules
See changes instantly on your device
Using the remote debugging functionality of Opera Dragonfly, you can analyze and debug pages running in the Opera Mobile Emulator (see Figure 3-27). With Dragonfly, you can debug in separate instances of the Opera browser, as well as other Opera Presto-powered user agents. It doesn’t matter if these are located on the same machine or on another device such as a mobile phone or television. When put into Remote Debugging mode, Opera Dragonfly will listen for a connection to the IP address and port specified. The separate instance of the Opera browser can connect over a network and pass debugging information across the connection. Opera Dragonfly can then interact with the web pages and applications on the remote instance, just as if it were running locally.