Chapter 10. Image Consolidation (for Network and Cache Efficiencies)

If you’ve ever had to move from one home to another, you know that moving day is long and grueling. You quickly realize that you want to minimize the number of trips from your apartment to the moving truck. If you took one small box each trip, you’d spend more time going back and forth than actually loading the moving truck. Therefore, carrying more boxes in each load reduces the number of trips back up the stairs and brings that much-deserved beer that much closer. At the same time, there is a limit. Good luck trying to carry eight cartons of books in one load. An extra trip is better than a broken back.

This is the same challenge with loading images in a browser or app. In order to optimize the delivery we need to address either the number of requests or the payload per request. This is particularly true for small images. A useful technique is to consolidate images, thereby reducing requests and making each request more effective. This chapter explores how to achieve high performance for the smaller images using techniques like spriting, web fonts, and inlining.

The Problem

Just like in our analogy of moving household goods, the browser (and apps) have two particular problems:

Round-trip time

How long does it take from the time the request is sent to the time the response is received? Using our analogy, how long does it take for you to leave the truck, go up the stairs into your apartment, and come back with a load? Do you have to prop open the doors, or are they open already?

Making every trip count

How do we make sure each response contains the most data? Taking one trip to deliver a single carton containing a lampshade is not very efficient and delays completing the job.

TCP Connections and Parallel Requests

To understand the impact of the round-trip time, let’s start by examining what is happening at the TCP/IP layer. For reference, this section is particularly focused on the problems manifested in HTTP/1.1. The problems of congestion window scaling are specifically addressed in the HTTP/2 design.

To review, a typical Transmission Control Protocol (TCP) session starts with a handshake before sending and receiving data, as shown in Figure 10-1.

hpim 1001
Figure 10-1. The time delay for an HTTP or TLS handshake over TCP

The biggest challenge with TCP is latency. Internet service providers (ISPs) and cellular providers have been good at selling Internet based on bandwidth—how many megabytes per second your connection can send. The dirty little secret they don’t tell you is that you can have as much bandwidth as you like, but it will deliver inferior user experience if you have high latency.

Figure 10-1 shows the cost of merely establishing a TCP connection. In this illustration having just 50 ms of latency means that an unencrypted TCP connection takes 100 ms before the browser can send the first HTTP request (300 ms before the first HTTPS/TLS request). When we increase the latency to 75 ms, this problem inflates to 150 ms and 550 ms for HTTP and TLS, respectively.

To send a single small image (say 1,200 bytes), you would have the connection overhead + one packet for a request + one packet for a response. This means the total time on a 50 ms latency connection is 400 ms for just one packet of data—for just the small image.

Put another way, we are achieving 12.5% efficiency on our network connection. If we have to set up a new connection for each image, on a TLS connection only 12.5% of the total time is spent transmitting data (25% on an unencrypted connection).

Small Objects Impact the Connection Pool

Fortunately, HTTP/1.1 does provide for connection reuse with persistent connections. This way, the TCP connection is negotiated once per session and the socket is reused for multiple requests. (Of course, this assumes the server behaves properly and respects the Connection:keep-alive header.) Despite the persistent connection, small object delivery can impact the connection pool.

The fatal flaw with HTTP/1.1 is that each image request blocks and delays other resources from being loaded. Specifically, you are limited to one request and one response at a time. Any other requests queued on the network interface must wait for the HTTP response. For this reason multiple connections are usually opened in parallel to prevent head-of-line blocking. A browser (and operating system) imposes limits on the number of TCP connections. The usual limit imposed is around six connections per hostname. (Earlier versions of Android and iOS had lower global limits—as low as four.) Not just images are impacted by the connection limit; the limit affects all resources, including APIs, JavaScript, and CSS.

While a connection can be reused, it is still subject to congestion windows and TCP slow start. The situation is aggravated with small images because they won’t saturate the connection. Each request may be followed by a few packets of response data followed by another request packet. For example, if an image was only four packets (assume ~1,500 bytes per packet), the cost of latency to send and then receive data becomes quite high and the effective throughput will be low. We could be sending more data on the network, but we’re forced to pause and wait for the round trip for each new request.

Using our moving analogy, think of having a maximum of six movers. With small images you are only loading each worker up with one box per trip instead of many boxes per trip.

Not only are these small images blocking other requests, but they are penalized by the latency on the connection: more latency compounds the delay of page rendering.

To illustrate this, imagine a single page with 100 images of 3 KB each. The HTML is very simple: <img src="/hpi-3k.png?v=1" width="25" height="23">. Each image is the same, but marked with different version numbers to ensure cache busting. Notice that even in HTTP/1.1 and HTTP/2 the network connection is never saturated (see Figures 10-2 and 10-3).

hpim 1002
Figure 10-2. Many small images are not able to saturate the TCP connection (HTTP/1.1 unencrypted)—test with 100 3 KB images on a page
hpim 1003
Figure 10-3. Even with HTTP/2, many small images are not able to saturate the TCP connection—test with 100 3K images on a page

Efficient Use of the Connection

HTTP/2 does improve the situation by effectively increasing the number of parallel requests. You will incur the cost of a TLS handshake, but will be able to make many requests on a single connection without the penalty of head-of-line blocking. Requests and responses occur simultaneously, maximizing the connection throughput. Still, there is a finite data capacity. Using our analogy of moving, the doorway still restricts how many boxes can actually be transported from the house to the truck. If we are transmitting images ahead of critical content, we will still delay the experience of the waiting user.

Fortunately, the browser (and the protocol) can prioritize requests: images after XHR/AJAX, JavaScript, and CSS. This is an attempt to minimize the impact of delayed requests. Increasingly, however, these resources are loaded using JavaScript and other complex mechanisms, making it easier for the preloader/speculative parser to discover and queue images but less likely to discover critical JavaScript and XHR calls. Early CSS request and parsing will also quickly populate the request queue. The net result is that small images will block resources needed for user interaction.

Take, for example, Lottee.com, a South Korean online shopping mall (Figure 10-4). On the home page, the images in the network queue delay the loading of other critical CSS and JavaScript resources. Also note the use of the network bandwidth.

hpim 1004
Figure 10-4. Lotte.com request waterfall

Impact on Browser Cache: Metadata and Small Images

One last challenge with small images is the overhead of maintaining the images in the cache and sending them over the wire. While the actual bytes of the image might be small, there is always overhead metadata associated with the image that is sent along with the HTTP response in the form of HTTP headers and browser/device cache. This may seem to be a trivial issue, but when you compound it with many small images, it becomes a larger problem.

There are three areas where this metadata exists: the datacenter, transit, and client. We’ll ignore the cost of maintaining the images at the datacenter for now. In transit to the client, this metadata manifests itself as the HTTP response headers being sent to the client. Once received, this image must be stored and indexed on the user’s operating system for future reference.

Chrome uses block files to store images and other small content that is less than 16 KB (see Table 10-1). This reduces the overhead of sector waste on the filesystem. Each block file uses different sizes of blocks and is limited to ~64,000 entries each. A cached entry will include the hashed key, HTTP headers, rankings, and pointers to payload blocks. The payload is stored using all the same block sizes, which will likely yield at least one partially filled block.

Table 10-1. Chrome’s disk cache file organization with different cache block sizes for each file
File Block size

data_0

36 bytes

data_1

256 bytes

data_2

1 KB

data_3

4 KB

Let’s assume for a moment that the client has requested a 3.2 KB image and has 300 bytes of HTTP response headers. Not a big deal, right? This image will be indexed and stored in one of the cache block files; in this case, it will be data_2 and will require five blocks: one for the headers, four for the payload. Thus we have used ~512 bytes for the cache entry records along with an additional ~100 bytes for the rank and index plus 4 KB for the HTTP response. In total we have used 5.6 KB of storage for a 3.2 KB image. That 75% increase in file size (2.4 KB) is all overhead! Worse yet, the 64,000 entries in the data_1 block file are reduced by 4 just for a single cached file.

Modern browsers employ a fixed cache pressure to reduce IO overhead. While the use of data files optimizes the utilization of the cache for small files, the popularity of similar sized files can create cached entries to be dropped. The least-recently-used cache is a complex algorithm that takes many factors into account, including block utilization. The risk of having many small images on a page is that it will increase the probability of some or all of those images being evicted from the cache before a repeat visit from a user.

For example, if you consolidated 10 images into 1 consolidated file and request each subimage only one time, you would have effectively increased the cache popularity of the single consolidated image. As separate images they would have a cache hit of 1, whereas now the consolidated image has a hit of 10. Thus, the aggregated resource is less likely to be evicted compared to the many resources.

There is also the impact of IPC in the browser when making a request from the cache or the network. At a high level each tab in a browser has its own thread, but must communicate via IPC to the browser threads, which in turn dispatch multiple requests over the network or even fetch resources from the cache. This architecture allows isolation and parallel processing but at the cost of additional memory. IPC calls are not free and have synchronization overhead. The more we can reduce IPCs, the more efficiently the browser will behave.

Small Objects Observed

Surprisingly, a large portion of the images downloaded on the Web are small images. Of the top 1 million most popular images, 24% of all JPEGs requested by end users are less than 6 KB in size. Likewise, 80% of GIFs and 64% of PNGs are less than 6 KB. In aggregate, 44% of images requested by end users are below 6 KB, or approximately four packets wide (see Figure 10-5).

hpim 1005
Figure 10-5. Histogram of 1 million JPEGs, GIFs, and PNGs

As we have already discussed, images make up most of the bytes downloaded on a web page. Unfortunately, this byte volume also corresponds to an average of 54 images per page, according to httparchive.org. This is despite over a decade of web performance optimization education showing the necessity of optimizing for these small images.

Logographic Pages

Unfortunately, HTTP and HTML are biased in favor of English and the Latin-based languages. Logographic-based languages have many complexities—from character encoding to text flows. Many of the early browsers, proxies, and web servers had challenges differentiating ASCII and Unicode encodings like UTF-8. As a result, many websites to this day still depend on images for logographic words to ensure styling, formatting, and aesthetics are preserved across browsers. The result is much higher volume of images since much of the text content is embedded in images.

For example, compare the number of small images used on rakutan.co.jp to rakutan.co.uk (a popular online retailer in Japan), shown in Figure 10-6. The majority of this difference is to address the shortcomings of browser rendering discrepancies by using small images for words and text (see Figure 10-7).

hpim 1006
Figure 10-6. Compare the image bytes required for rakutan.co.jp (Japan) in contrast to rakutan.co.uk (UK)—nearly 6x the number of image bytes
hpim 1007
Figure 10-7. Most of the images contain Japanese text to solve layout and font issues

Raster Consolidation

Consolidating techniques focuses on maximizing I/O—whether network or cache—by using one data stream to represent multiple images. Raster and vector images have slightly different options (see “Raster Versus Vector” for more discussion).

CSS Spriting

The most common, and likely the most effective, way to reduce the number of small images is to utilize CSS sprites. Sprites are a robust technique that have a history stretching back to the early days of video games. A single image can contain multiple images that are sliced up and reused throughout the page. Better still: CSS sprites are supported by nearly all browsers.

Using CSS sprites accomplishes the following goals:

  • Multiple images combined into a single image

  • 1 HTTP request

  • 1 cache entry

  • Reduced file size for combined images

Consider the logos for the most popular browsers (see the following images). Including each icon as a separate file would result in the bytes downloaded and disk cache size (see “Impact on Browser Cache: Metadata and Small Images”) shown in Table 10-2.

Table 10-2. Small icons file byte size and size in cache
Logo Pixels Bytes Browser disk cache size

MS Edge

128×128

1.39 KB

3.6 KB

Chrome

128×128

3.34 KB

5.6 KB

Firefox

128×128

6.55 KB

9.6 KB

Safari

128×128

5.42 KB

9.6 KB

Total

16.7 KB

28.4 KB

hpim 10in01
hpim 10in02
hpim 10in03
hpim 10in04

In total, these icons occupy 16.7 KB. Combining the four images into one results in a single 12.8 KB image, requiring only one IPC and occupying 16.6 KB of cache disk (four blocks of data_3). Not only is this now an HTTP request, but it also reduces the cache footprint to 3.6 KB.

Creating CSS sprites

Creating and using CSS sprites is straightforward:

  1. Merge images into a single image.

  2. Create CSS styles that reference the appropriate sprite location.

  3. Add HTML markup placeholders for the images.

Merging images

You can use your favorite image editor, such as GIMP or Photoshop, to merge images. Create a canvas large enough to house all the sprites, copy and paste each image, lay out the images in a logical order, and save. You’ll likely save the resulting image as a PNG (see Chapter 3 for selecting the right format).

$ convert edge.png chrome.gif firefox.png safari.png
  -append PNG8:browsers-sprite.png

Change sprite direction with ImageMagick

Use ImageMagick to create a sprite with -append to append vertically or +append to append horizontally.

Creating CSS styles

Once you have the single image created, the next step is to create the appropriate CSS styles. CSS sprites use the background-image and background-position properties to move the image out of the viewable area. These attributes have existed since 1996 in CSS1 and have nearly ubiquitous browser support.

a.icon {
  display:inline-block;
  text-indent: -9999px;
}
.icon {
  background-image: url('/images/browsers-sprite.png[]');
  background-repeat: no-repeat;
  height: 128px;
  width: 128px;
}
.icon-facebook {
  background-position: 0px 0px;
}

.icon-twitter {
  background-position: 0px -128px;
}

.icon-linkedin {
  background-position: 0px -256px;
}

.icon-googleplus {
  background-position: 0px -384px;
}

A quick checklist for the styles:

  • Keep track of the relative position (-x, -y) of each sprite on the canvas.

  • Specify the width and height of the viewable sprite to avoid visual gaffes.

  • Use one style per sprite to avoid overlap.

  • Update all the relative positions if you change the sprite.

Adding HTML markup

For each location where you will use the sprite you will need a corresponding HTML element that supports background styling. You’ll have to use a blocking element, which in most cases means you’ll use a <div> or <span> instead of <img>. The HTML markup is usually the part that grates on most purists because it requires you to mix presentation with content.

<a class="icon icon-edge"
href="https://www.microsoft.com/en-ca/windows/microsoft-edge">
    Microsoft Edge
</a>
<a class="icon icon-chrome" href="https://www.google.com/chrome/">
    Chrome
</a>
<a class="icon icon-firefox" href="https://www.mozilla.org/en-US/firefox/new/">
    Firefox
</a>
<a class="icon icon-safari" href="http://www.apple.com/safari/">
    Safari
</a>

In this example we have made the social media links clickable while also making them accessible for anyone using a screen reader.

Automating to avoid image and link rot

Clearly, creating sprites by hand isn’t ideal. In fact, the biggest risk of manually creating sprites is image rot—images that are no longer being used but are still included in the sprite. The worst case is when the same image is included multiple times but at slightly different sizes.

If you are manually creating the sprite, then you will either need to revisit all the old references, or just blindly add a new image to the bottom of the existing sprite. The latter is the path of least resistance. Unfortunately, this will result in an ever-growing sprite canvas. Consider the pain and suffering of having to refactor all your CSS after your CEO discovers that the site is slow because of a 1 MB sprite (… not that this has actually happened to anyone I know).

Fortunately, there are many tools available to help automate the creation and referencing of sprites. Usually, the first approach is to do a global search and replace on HTML and CSS files. Don’t do that. It is painful and will be fraught with problems. You shouldn’t underestimate the creativity of your marketing team.

The better approach is to automate the creation of sprites and CSS styles. Clearly define the style-naming convention with your creative teams. Follow this up by removing all GIF/PNG/JPG files during your deployment process and monitor for broken links to find offenders.

Many frameworks now have automated mechanisms to create sprites. If you are starting from scratch, I suggest using Sprity. Sprity is very extensible and can plug into your existing styling frameworks (SCSS/Less) and build automation systems (Grunt/Gulp), but can also be plumbed into an existing deployment script.

For example, we can simplify our output with this command line to create both out/sprite.png and out/browsers.css files:

$ sprity out/ images/*.png -s browsers.css

The Sprity default creates styles prefixed with icon- which, fortuitously, matches our preceding example.

Drawbacks and shortcomings

While CSS sprites do provide broad browser support and are well understood, it isn’t all unicorns and rainbows. There are many rough edges in this technique.

Operationally:

  • Global sprites versus local sprites: should you create one global sprite but have many of the icons unused in a page, or have one sprite per page but have duplication?

  • Large sprites need to partitioned. Sprites shouldn’t be larger than 10 packets (~40 KB). Use partitioning schemes to manage growth.

  • Cache invalidation—any change will cause the sprite to be invalid and render downstream caches moot. You will certainly need to version your sprites and force the end user to download the new sprite, even if 90% of the icons haven’t changed.

  • You must be vigilant about ensuring that unsprited references to small images don’t creep into the system.

  • Chicken and egg: sprite first or style first? Sprites must be created first before creative teams can style a page and decide if the sprite is good enough. Iterating on an icon is burdensome.

Stylistically:

  • Images can’t be styled. You must manually implement CSS properties, like shadows, coloring, and underlining, by creating yet another image and sprite.

  • Different sizes and layouts also require different image sprite sets.

  • Animated PNG/GIF/WebP files can’t be included in a sprite (though arguably they are likely not small images).

  • Sprites mix presentation and content by injecting HTML.

Data URIs

Another technique that shares roots with CSS spriting is inlining images. This approach moves images not into a separate consolidated image, but into the referencing document, and encodes the binary into base64 text. In this way you can include the images in the HTML or CSS by using the data: prefix whenever a src attribute or property is used.

Inlining images with data URIs has benefits because it eliminates the need for yet another HTTP request and cache entry. The page becomes intrinsically consistent; there’s no need for versioning. What you sent is what was expected to be rendered.

The structure of a data URI is:

data:[<media type>][;charset=<character set>][;base64],<data>

For images you can ignore the ;charset attribute, but be sure to include the ;base64 attribute. For example, the 35-byte universal transparent 1×1 GIF is rendered as:

<img
  src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" />

You can use this in HTML and CSS like so:

<img
  src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYA
  AACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwA
  AAABJRU5ErkJggg==" alt="Red dot" />

<style>
  .dot {
    background: url('data:image/png;base64,
    iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4/
    /8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==')
  }
<style>

There are many tools available to generate the base64 output, including Grunt tasks like grunt-data-uri. You can also implement this yourself using the base64 command in Linux or OS X.

Considerations

As you would expect, there are caveats to consider when you are utilizing a data URI.

Increased size

The biggest objection to using data URIs is the bloat from base64-encoding the binary. Base64 will increase the raw byte size by ~35%. Fortunately, Gzip will reduce the contents between 3% and 37%. (Using Brotli you could get this down even further.) Overall most images will have no larger net size when transferred. (Though note that really small images can see some increases in size because of the headers required.)

Figures 10-8 and 10-9 again utilize the study of the top 1 million images, this time converted using Base64 and then Base64 with Gzip, respectively. The locus of the results shows a net decrease in byte size after inlining.

hpim 1008
Figure 10-8. Base64 only: image width versus % increase size from base64
hpim 1009
Figure 10-9. Base64 then Gzip: image width versus % increase size from base64/Gzip
Browser support

Unfortunately, data URIs are an advent of modern browsers. For those prior to IE8 you’ll need to have a non-inlined version of your CSS that references the images directly. IE8 also has an artificial limit of 32 K of encoded URIs. You can use the Internet Explorer conditional comment to add the correct CSS:

<!--[if lte IE 8]>
<style href="ie-noinlining.css />
<![endif]-->
Request blocking

The real problem with inlining is that the images have effectively moved up in priority and the transfer of the image is now blocking the transfer of other critical resources. As we have previously discussed, images are generally low priority, so by inlining them with data URIs we give the image an effective high priority because it is transferred at the time of the HTML or CSS.

Processing time

Further complicating the issue is that the decode process takes additional CPU and memory. One study by Peter McLachlan at Mobify found that “when measuring the performance of hundreds of thousands of mobile page views, loading images using a data URI is on average 6x slower than using a binary source link such as an <img> tag with an src attribute!”

While this is something that can be optimized over time in modern browsers, the use of data URIs can slow the loading and processing of the file. If you embedded all images in the HTML, resulting in an uncompressed doubling, it would impact the time to compute the DOM or calculate styles.

Caching and CSP

As with sprites, changes to images require caches to be invalidated. Unlike with spriting, though, the impact isn’t localized to a single image; it now requires the encapsulating CSS and HTML to be versioned or invalidated from the cache. If only an icon changes, then the entire page must be redownloaded.

Likewise, if your site employs a content security policy (CSP), the base64 or digest hash will need to be updated. Using inline images creates an ecosystem change.

Better: Deferred data URI stylesheet

If you are concerned about blocking the critical rendering path by inlining images in the HTML and CSS, another approach is to use an asynchronous CSS stylesheet.

  1. Replace CSS background properties to remove the url() reference. You can replace it with a solid color #ffffff or even with a 1×1 inline pixel so as not to minimize the stylesheet differences:

    .myclass {
      width: 123px;
      height: 456px;
      background: #ffffff no-repeat
    }
  2. Create a new CSS stylesheet (we will call it images.css) with just the CSS selector and real background properties that include url('data:images/ ...') with inline source for the actual content:

    .myclass {
      background-image: url('data:image/gif;base64, ... ')
    }
  3. Defer the loading of images.css with the following JavaScript (courtesy of Scott Jehl’s loadCSS.js (https://github.com/filamentgroup/loadCSS):

<script>
  // include loadCSS here...
(function(w){
	"use strict";
	var loadCSS = function( href, media ){
		var doc = w.document;
		var ss = doc.createElement( "link" );
		var refs = ( doc.body || doc.getElementsByTagName( "head"
		             )[ 0 ] ).childNodes;
		var ref = refs[ refs.length - 1];

		var sheets = doc.styleSheets;
		ss.rel = "stylesheet";
		ss.href = href;
		// temporarily set media to something inapplicable to ensure
		// it'll fetch without blocking render
		ss.media = "only x";

		var onloadcssdefined = function( cb ){
			var resolvedHref = ss.href;
			var i = sheets.length;
			while( i-- ){
				if( sheets[ i ].href === resolvedHref ){
					return cb();
				}
			}
			setTimeout(function() {
				onloadcssdefined( cb );
			});
		};

		// once loaded, set link's media back to `all` so that the
		// stylesheet applies once it loads
		ss.onloadcssdefined = onloadcssdefined;
		onloadcssdefined(function() {
			ss.media ="all";
		});
		return ss;
	};
	// commonjs
	if( typeof module !== "undefined" ){
		module.exports = loadCSS;
	}
	else {
		w.loadCSS = loadCSS;
	}
}( typeof global !== "undefined" ? global : this ));

  // load a file
  loadCSS( "/images.css" );
</script>

<noscript><link href="/images.css" rel="stylesheet"></noscript>

<!--[if lte IE 8]>
<style href="ie-noinlining-images.css />
<![endif]-->

The net result will be a combined CSS with just the inlined images. This combined CSS is loaded asynchronously (don’t forget to include the legacy fallback for IE 5–8). All of the inlined images will have two distinct benefits:

  • Total bytes are reduced via Gzip to 1–3% less than even the original total bytes.

  • Images will avoid being manipulated by intermediate proxies that can recompress images and distort images for mobile users (we will discuss this in Chapter 13).

Tools

There are many tools options to help you create inline images with different approaches.

  • You can use automated frontend optimization services, such as PageSpeed for Apache, NginX, and IIS. Many content delivery networks (CDNs) also include this service as part of their value-add delivery.

  • Build tools into your development workflow. Compass can automate the creation in your SCSS/SASS stylesheets. Grunt tasks, like grunt-data-uri, can also examine existing CSS and transform the content automatically ahead of deployment to production.

  • Roll your own tools. You can use the base64 command on most Linux systems or use the base64() equivalent function in most languages.

Vector Image Consolidation

Using raster graphics for icons and layout styling support is not ideal. This is especially true for logographic and non-Latin-based content (hiragana, katakana, kanji, zhōngwén, hangul, etc.) and also can be problematic for responsive layouts. Either you are sending down very large raster images and forcing the client to resize down, or doing the opposite and scaling up small images. Both are undesirable from a performance and aesthetics perspective. It gets worse if you are trying to align CSS styling with these bitmap images. A better solution is to implement these small images in vector format to allow clean scaling on all resolution of displays.

Icon Fonts

Vector images can be merged into a custom web font. This approach replaces literal characters with a custom icon or graphic.

There are many downsides to this approach, and it should be used only in a few situations, particularly:

FOIT

Flash of Invisible Text (Chrome/Safari/Firefox). Text styled by web fonts is hidden until the font is loaded.

FOUT

Flash of Unstyled Text (Internet Explorer/Edge). Text is presented unstyled initially, then changed after the custom font is loaded.

Proxy browsers

Many browsers, particularly those on low-powered mobile devices, don’t support custom web fonts.

Accessibility

Visually impaired and dyslexic users often override default fonts. Using custom fonts will make your website look like gibberish.

However, web fonts can make sense in some situations:

  • Accents or enhancements to existing text or icons (there are already many icons presented in Unicode, including emojis)

  • Ligatures where words are replaced with enhanced text

  • Logographic content where standard

  • Native app web views where you can limit which platforms utilize the web fonts

Overview

There are two approaches to utilizing web fonts:

  • Single-character replacement: Create the new HTML entity &#broccoli; with the image seen in Figure 10-10.

    images/hpim_1010.png
    Figure 10-10. Image to create new HTML entity

    These images can be referenced by decimal position or defined colloquial name. Existing characters can also be replaced.

  • Typographic ligatures: this is essentially the same as a single-character replacement but has some additional usability benefits. Instead of a single-character replacement, you can do multiple-character replacement to replace a whole word with an icon. For example, the word love can be replaced with the ♥ character. In this way, “I love broccoli” will be rendered “I ♥ broccoli.”

Additionally, icon fonts can be styled with CSS just like any other text. This includes color adjustments, shading, shape, rotation, and even font styles like bold and italic. Adding CSS styles to font icons provides you with flexibility and eliminates the need to regenerate from source when applying subtle aesthetic changes.

Creating and using icon web fonts

Assembling an icon font is fairly straightforward. You can assemble a new icon font by using existing web fonts or, using SVG images as source, by defining character mapping and converting to the various web font formats. The trickier part is ensuring cross-browser support, fallback, and accessibility.

Fortunately, you don’t have to build your icon web font from scratch. There are many font libraries ready for use, many of which can be reassembled into purpose-built web fonts. IcoMoo, SymbolSet, Font Squirrel, and Pictos are just some of the many sites that can assemble, create, and host icon fonts. (We’ll discuss hosting and performance shortly.)

If you’re using images for Asian characters, this is the best place to start to build a logographic typeface.

There are many tools for type designers and typographic experts to create custom web fonts. This includes FontLab Studio, FontForge, and many others. However, for custom icon web fonts this may involve a lot more complication and is not necessarily scalable for use with your creative teams.

There are also a number of tools that can help you automate the process of creating web fonts and avoid the manual design process. Typically these tools start with SVG images, transform them into the custom font, and provide the appropriate character mapping. The typical workflow starts with SVG images, converts to an SVG font, and then converts that font to the other web font formats, such as TTF, EOT, WOFF, and WOFF2. Alternatively, there are also Grunt and Gulp tasks (such as grunt-webfont or gulp-iconfont) that wrap up these individual steps into a single task, making it easier to automate the process.

Web Fonts are Monochromatic

It is important to remember that web fonts are monochromatic. Color detail represented in SVGs will be lost when embedded in a font.

To demonstrate this workflow we will use the following libraries:

  1. SVG images >> SVG font

  2. SVG font >> TTF font

  3. TTF font >> EOT font

  4. TTF font >> WOFF

  5. TTF font >> WOFF2

There are a number of other libraries that are also useful for this process that we won’t explore, specifically:

  • SVG Optimizer, which reduces the redundant information and helps collapse the code paths.

  • TTFAutoHint, which can help improve rendering of fonts, particularly in Windows, for maximum readability.

Using the same browser logos we used when creating the CSS sprite we can combine them into a web font. This time we will start with SVG representations. Our folder /images contains the following SVGs, as shown in Figure 10-11.

  • images/safari.svg

  • images/firefox.svg

  • images/u0065-edge.svg

  • images/u0063,u0063u0068u0072u006fu006du0065-chrome.svg

hpim 1011
Figure 10-11. SVG browser icons

Invoking the conversion to create the SVG font (fonts/browsers.svg) is fairly straightforward. This will create the root font we will use to convert to the other web font formats. It is also the step where the character mapping, ligature creation, and colloquial glyph naming occurs. In this example, the filename will also provide hints for character mapping for the Edge and Chrome logo. The letter e will be replaced with the Edge logo or the ligature chrome will be replaced with the Chrome logo.

$ svgicons2svgfont --fontname=browsersfont -s uEA01 
  -o fonts/browsers.svg images/*.svg

Likewise, converting to TTF, EOT, WOFF, and WOFF2 can be accomplished thusly:

$ svg2ttf fonts/browsers.svg fonts/browsers.ttf
$ ttf2eot fonts/browsers.ttf fonts/browsers.eot
$ ttf2woff fonts/browsers.ttf fonts/browsers.woff
$ ttf2woff2 fonts/browsers.ttf fonts/browsers.woff2

Utilizing the newly created web font is now as easy as adding the font declaration and associated HTML:

<span class="icon icon-safari"></span>
<span class="icon icon-firefox"></span>
<span class="icon icon-edge"></span>
<span class="icon icon-chrome">chrome</span>
@font-face {
  font-family: 'socialmediafont';
  src: url('browsers.eot'); /* IE9 Compat Modes */
  src: url('browsers.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
       url('browsers.woff2') format('woff2'), /* Super Modern Browsers */
       url('browsers.woff') format('woff'), /* Pretty Modern Browsers */
       url('browsers.ttf')  format('truetype'), /* Safari, Android, iOS */
       url('browsers.svg#socialmediafont') format('svg'); /* Legacy iOS */
}

.icon {
    font-family: 'browsersfont' !important;

    font-feature-settings: "liga"; /* enable ligatures */
}

.icon-safari:before {
    content: "ea01";
}
.icon-firefox:before {
    content: "ea02";
}
.icon-edge:before {
    content: "65";
}
.icon-chrome:before {
    content: "63";
}

Since we have created an icon font with only the icons, only a limited number of characters can be rendered. Any additional text that is caught in the CSS style that is not defined may render oddly with different browsers. For example, if you have the text edge, only the letter e will display and the following characters may have empty boxes. Be careful to scope icon fonts appropriately.

In this example we have enabled ligatures using the CSS property. However, a more comprehensive style would include:

.icon {
    font-family: 'browsersfont' !important;

    /* Ligature support */
    letter-spacing: 0;
    -webkit-font-feature-settings: "liga";
    -moz-font-feature-settings: "liga=1";
    -moz-font-feature-settings: "liga";
    -ms-font-feature-settings: "liga" 1;
    -o-font-feature-settings: "liga";
    font-feature-settings: "liga";
}

As previously mentioned, using Gulp or Grunt tasks can simplify these steps and combine them into a single action. Both tasks will also generate the necessary CSS and mapping to further reduce rendering errors.

Compatibility

Unfortunately, web font support across the browser spectrum is very fragmented (see Figure 10-13). There isn’t a single universal format that is supported by all browsers. While modern browsers have rallied around WOFF and WOFF2, older browsers support a myriad of formats, including EOT, TTF, and SVG. Worse yet, most proxy browsers, including Opera Mini, do not support any web fonts, so fallback is always important.

hpim 1013
Figure 10-13. Browser support for @font-face with web fonts from CanIUse.com (2016)

Safari Support for SVG Fonts

While the SVG font container is supported by Safari, modern versions also support WOFF. Only early versions of Safari supported SVG Web fonts. It is relatively safe to omit SVG in your CSS declaration.

Each browser also loads fonts differently, resulting in a variety of rendering experiences for users. Of particular note is the dreaded Flash of Unstyled Text (FOUT). For example, Internet Explorer will display the text in an alternate font until the web font is available. This is OK if you have appropriate fallback characters, but it will display empty boxes if you’re using PUA character mapping.

In contrast, Safari will hide the text until the custom font is available and display it only after the font is loaded. Finally, Chrome and Firefox will wait up to 3 seconds and use the fallback font, repainting after the font is available. This Flash of Invisible Text (FOIT) is probably worse from a user experience perspective—especially if the user is on a poor network connection.

Most browsers also load fonts asynchronously with the exception of Internet Explorer. The result is that the icon images can be displayed later and prolong the FOUT period while the fonts are loaded. For smaller icon fonts, inlining the font with a data URI can be more efficient. Unfortunately, because of the multiple font formats, you will also need to inline the different font files even if they aren’t being used.

To work around this, you can use adaptive delivery for your CSS and detect, server side, the browser and version and deliver a specific CSS file with the appropriate inlined font file. (For more details, see Chapter 13.)

While the hoops to generate font files for vector images might seem arduous, the real benefit is bringing accessibility for your website and images, as well as a convenient encapsulation to bring vector images to legacy browsers.

Web font pros and cons

While icon fonts are a convenient and durable mechanism to consolidate small vector images, there are many drawbacks. Most notable is the outright lack of support by some browsers—specifically, the lack of support by proxy browsers like Opera Mini. There are also various CSS and rendering nuances in different browsers and operating systems that need to be accounted for and tested. This includes CSS tricks like including !important to avoid browser extension issues and explicitly enabling font smoothing using -webkit-font-smoothing: antialiased and -moz-osx-font-smoothing: grayscale—not to mention issues of alignment, spacing, and churning.

On the other hand, web icon fonts can be good for text, specifically to augment existing text (using ligatures) or logographic content.

SVG Sprites

While SVGs are text and highly compressible, they are not immune to the challenges of small image delivery. In fact, SVGs have nearly the same kind of file size distribution—the majority being less than a single packet wide.

If you have vector images (in SVG) you aren’t limited to web fonts to consolidate. You can create SVG sprites just as you would GIF/PNG sprites. As with raster sprites, you would arrange your icons on a canvas in a grid. Most vector image editors, from Adobe Illustrator to PixelImator, make this a quick task.

For Convenience, Set SVG viewBox Equal to viewport Dimensions

When using SVG for sprites, setting the viewport and viewBox to different values can have odd results. Remember the viewport is the viewable size (i.e., how large your monitor is), and the viewBox is the portion of the SVG canvas that should be stretched or shrunk to fit the viewport. For simplicity it is best to set the viewBox and viewport to the same dimensions.

For example, for our browser icons we might have an SVG sprite such as:

<svg version="1.1" xmlns="http://www.w3.org/2000/svg"
    xmlns:xlink="http://www.w3.org/1999/xlink"
    width="800" height="1080"
    viewBox="0 0 800 1080" >
    <g>
    	<path d="..." />
    	<!-- graphics arranged in rows and columns -->
    </g>
</svg>

Then, as usual you can reference each icon via CSS background:

.icon-safari {
  width: 20px;
  height: 20px;
  background-image: url('images/chrome.svg');
  background-repeat: no-repeat;
  background-position: -110px -630px;
  position: absolute;
}

This approach makes it easy for your creative team since it is a very familiar process. Better yet, this approach works in all browsers that support SVG—reaching back to IE9. Unfortunately, like raster image CSS sprites, the sprite must be manually maintained, and deprecated icon usage is nearly impossible to track. Without careful change management processes it is difficult to remove old icons for fear of creating a negative user experience.

There are other drawbacks. Using SVG has the appeal of custom styling using CSS and even animations. However, if you are using an SVG in a background-image you lose this flexibility. If you don’t intend to style the SVG differently on the page, use :onhover, or apply any other customization of the image, then you could stop here. But we could do better.

SVG fragment identifier links

Often it’s easier to use a common, colloquial name, instead of remembering the coordinates on the canvas. Since SVG is XML, most elements can be marked with a fragment identifier or id attribute. For example, we could define our icon in a <symbol> (which won’t be visible until it is displayed):

<svg>
    <symbol id="chrome-logo"> <!-- ... --> </symbol>
</svg>

You can use fragment identifiers in SVG in many ways. Just as in HTML you can apply specific CSS styling to different nodes by referencing the id. You can also use it as a template for repeat use: you can reference the id in a use block multiple times (for example, drawing leaves on a tree). The identifier link can reference whole other files or a definition in the same file. You name the identifier at the end of the URL after the hash symbol, just as you would with HTML fragment identifiers:

<svg viewBox="0 0 100 200">
    <defs>
        <g id="firefox-logo"> <!-- ... --> </g>
    </defs>

    <use xlink:href="#firefox-logo"></use>
    <use xlink:href="images/browsers.svg#edge-logo"></use>
</svg>

In this example we place two SVG images on our canvas: one internally referenced symbol and another external. For completeness you can see how we reference both a symbol and a group (<g>). The group is wrapped in a defs block to ensure that it doesn’t display until referenced. Hiding the fragment isn’t required; it is convenient. We could always reference the first use of a template. However, it is better practice to define your templates separately. Doing so also solves a particular bug in some browsers (noteably Safari) where references must be defined before they’re used.

Note

Using symbol has the advantage of being able to define the template’s viewBox and preserveaspectratio. It is also more clearly identified as a template rather than just another grouping layer.

For SVG spriting, we can use the fragment identifier to reference a specific image in a single consolidated SVG. This way we can ignore the location on the canvas:

<img src="images/browsers.svg#firefox-logo" />

It would be tempting to wrap all of our SVGs in <symbol> elements and add the id attribute. Boom. Done. Unfortunately, we would have two problems:

  • <symbol> and <defs> aren’t visible. Externally referencing them in your HTML or CSS would likewise draw nothing since the canvas is empty.

  • Browser support for referencing fragment identifiers inside an SVG is spotty—but we can work around these issues.

Fragment identifiers and viewBox

To use SVG sprites, we need to provide instruction on how to draw the vector on a canvas. Adding a viewBox attribute provides this detail. Just as we need to consider the viewBox in relation to the viewport when we display the entire SVG, we also need to specify how much of the fragment is displayed so that it can be stretched appropriately inside the referencing HTML node.

You can define the viewBox a few ways:

  • Add viewBox in the URL as you would a fragment identifier: browsers.svg#svgView(viewBox(0, 0, 256, 256)). Unfortunately, while Firefox, Internet Explorer, and Edge get it right, Chrome (until 40) and Safari have problems with this approach. It is also only slightly better than using the traditional CSS approach because you need to maintain the coordinate references.

  • Use an inline SVG block with a reference to the fragment identifier as follows:

    <svg viewBox="0 0 100 200">
      <use xlink:href="images/browsers.svg#safari-logo"></use>
    </svg>

    This is better but it is odd to require an SVG in order to reference an SVG sprite.

  • Define a <view> in the SVG and use that reference. As we mentioned, <g> does not support viewBox and <symbol> is hidden, but a <view> can merge use cases and expose a fragment identifier:

    <svg>
        <view viewBox="0 0 100 200" id="firefox-logo">
            <!-- ... -->
        </view>
    </svg>

    Now referencing the fragment in your HTML will behave as you expect and you’ll be able to style not only the HTML container, but also the SVG elements inside. The only remaining challenge is browser support. Again, not all browsers are created equally and using an <img> with a reference to the SVG + fragment identifier poses problems for Safari. We can more universally get around this by using an <object> tag instead:

    <object data="images/browsers.svg#safari-logo" type="image/svg+xml"/>

Using this approach will allow you to use both fragment identifiers and consolidate SVGs to all browsers that support SVG. We still need to support older browsers by using raster sprites as a fallback.

Automating SVG consolidation and fallback

Just as with raster sprites, we can automate the creation of SVG sprites to avoid image rot and duplication. There are several libraries that can be used with Grunt and Gulp wrappers. For example, Joschi Kuphal’s svg-sprite works well:

$ svg-sprite --view -D out/ images/*.svg

This will generate a consolidated SVG as we would expect with a <view> wrapper and a fragment identifier using the filename:

<svg>
    <view viewBox="0 0 100 200" id="browsers-firefox-logo">
        ...
    </view>
</svg>

You can also use this tool if you want to generate an SVG that uses conventional CSS spriting. This will produce a stylesheet with the coordinates on the consolidated SVG:

$ svg-sprite -css --ccss -D out/ images/*.svg

Legacy support is nearly not an issue. However, there are still many users trapped on devices and browsers with IE <9, Android <5, or iOS <7. You can support them in a few ways:

  • If you use the CSS style spriting, you can use device detection and return different stylesheets based on the browser support. (Unfortunately, you can’t use detection examining the Accepts: header.) In this way you would serve /sprites.css to almost all browsers, with the exception that you use a raster-sprited view in /sprites-raster.css. This would require generating raster images and spriting them as well. Wrapper tools like Iconizr can make this easy.

  • If you are using <object>, add a fallback to CSS spriting and use a <div> tag inside:

    <object data="images/browsers.svg#safari-logo" type="image/svg+xml">
        <div style="no-svg icon-safari-logo"/>
    </object>
  • Do nothing; let the browser show or hide the output. This isn’t a terrible solution because these legacy browsers are usually running low-powered hardware. Displaying nothing will improve the experience without forcing more overhead.

Summary

Consolidating small graphics, icons, and images will improve the user experience. There are different techniques that can be employed whether the sources are raster or vector based. Spriting is the most common technique for both because it typically uses lossless formats for raster images and is fairly well understood by most web developers. The same approach can be used for SVGs but requires consideration as to what features are needed and to browser support. Other techniques, such as inlining with data URIs, can also be useful but forgo the ability for the sprite to be cached if any of the surrounding HTML/CSS is modified between code releases. Finally, web fonts can be used, but because of the many shortcomings in ecosystem support, it is generally advisable to keep their usage targeted to specific use cases.

A few considerations for content that is eligible for consolidation:

  • Any file < 1,500 bytes (1 packet).

  • Four or more like files whose total bytes <24 KB for raster or <40 KB for vector (~16 packets).

  • Consolidated images shouldn’t exceed 48 KB (raster) or 80 KB (vector).

  • Group candidates based on probability to change. Each change will cause the client’s cache to be invalid.

To help select the right consolidation technique, try the flow diagram in Figure 10-14.

images/hpim_1013.png
Figure 10-14. Flow diagram for selecting correct consolidation technique

It is easy to focus all of our attention on the large images that dominate the user’s field of view: the hero image, the product images, the latest social media posts that get most of our attention. Yet, the presentation of our websites and apps is just as dependent on the subtle details, the small images. We can improve the performance of these small images primarily through reducing the number of requests, and reducing the overall size of the requests. The odd nuance of high performance small images is that if we do them right, no one will notice. However, if we do them incorrectly, everyone will notice.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset