Chapter 4: Simple Recipes – Jump Start Web Performance

Chapter 4: Simple Recipes

The tips provided in this chapter will require a little more effort, but the performance results on new and existing sites may be more dramatic. Some of the simplest but most effective database optimizations are tackled first before delving into images, media, fonts, CSS animations, and some controversial topics.

Optimize Your Database

Database access is often the biggest processing bottleneck on the server. Optimizing front-end performance may be futile if your database is struggling to cope with user demand.

There are a vast array of database types, but common performance solutions are described in the following sections.

Use a Query Analyzer

Most databases provide tools that describe how a query has been processed. These can identify missing indexes or other performance issues. Many SQL and NoSQL databases offer an EXPLAIN clause or option:

The output can be complex, verbose, and beyond the scope of this book. Consult the documentation and look for tools that can help understand the issues.

The database logs can usually be tuned to record long-running queries, and you may find open-source or commercial products to help optimize data.

Create Indexes

Many database performance issues will be solved with an index. An index works identically to those in a book: it allows a database to quickly jump to a record given a list of items defined in a specific order.

Consider a user table containing an ID (number), name, email, and hashed password. The ID is likely to be the primary key and the table is ordered by that value. A query for a specific ID is fast, because the database can use an efficient searching algorithm. For example, it can start at the middle record and, if its ID is higher, it knows the record must be in the first half of the table.

However, a login form requesting a user’s email address and password must query by that email. Those will be randomly ordered in the user table, so the database has to check every record until it locates a match. The larger the table, the slower the query. An index can define a list of emails in alphabetical order (or any order that’s practical). The searching algorithm can then use that index to locate a record by email, just as fast as searching by ID.

Indexes should therefore be considered on any field commonly used in search queries (typically WHERE or JOIN clauses in an SQL SELECT). It’s tempting to add indexes for every field, but the more you create, the more space is required, and the slower write operations become, as all indexes must be updated.

Simplify Queries

The less work the database has to do, the faster a result will be returned. Examine your codebase for complex or multiple dependent queries, especially those that are generated or contain sub-queries. It will usually be possible to make the search more efficient.

For example, consider a query that retrieves the top five selling books. In MySQL-compatible SQL:

SELECT title, author_id FROM book ORDER BY sales DESC LIMIT 5;

The results contain an author_id reference, so five further queries are made to fetch author names. For example:

SELECT firstname, lastname FROM author WHERE author_id = 14;
SELECT firstname, lastname FROM author WHERE author_id = 52;
SELECT firstname, lastname FROM author WHERE author_id = 50;
SELECT firstname, lastname FROM author WHERE author_id = 22;
SELECT firstname, lastname FROM author WHERE author_id = 20;

This is known as the N+1 problem: a large set of queries must be made for the parent records and each result.

A more efficient option would be to fetch all the authors in a single query:

SELECT firstname, lastname FROM author WHERE author_id IN (14,52,50,22,20);

Performance can be improved further with a single query that can be optimized by the database:

SELECT book.title, author.firstname, author.lastname
FROM book
LEFT JOIN author ON book.author_id = author.id
ORDER BY sales DESC LIMIT 5;

Create Additional Database Connections

Many web applications create a single database connection object that’s used for all queries and updates. Unfortunately, some databases queue all incoming requests from a single connection and process them in order. If one user runs a complex operation that takes 20 seconds to complete, every other user will have to wait at least 20 seconds for their operation to be processed.

Connection queuing issues will be more evident on continually running applications such those implemented in Node.js. PHP applications are usually served by a web server, which creates separate threads with new connection objects on every request, although pooling solutions may be in place.

To prevent database request queuing problems, consider these options:

  1. creating single-use connection objects for queries that could take time
  2. creating multiple connection objects for specific uses or which can be used in request order

However, be wary of creating too many in-memory connection objects, which could lead to stability issues.

Consider a Server or Memory Upgrade

Databases work more effectively when they have plenty of RAM. RAM allows the system to optimize frequently used queries and cache results in memory for fast access.

Alternatively, you could consider using either:

  1. a separate database server
  2. multiple servers that either share processing or shard data into smaller silos
  3. a third-party database provider that handles the hard work for you

Cache Results

It may not be necessary to perform queries every time a user requests a resource. Consider a statistical dashboard displaying various charts that are computationally expensive to create. The data could be fetched once from the database, cached in memory or a file, then returned on every subsequent request. The charts would be updated either when:

  1. data has changed
  2. a specific time has elapsed (such as ten minutes)
  3. a combination of factors is satisfied (such as when data has changed and it’s at least five minutes since the last calculation)

Solutions such as Redis and memcached are often used for caching purposes.

Use Background Processing

Consider a web application where a user can upload multiple images. These have metadata extracted, are resized, and have filters applied before data is stored in various tables.

Rather than doing all this work in the web application at the point the request is made, the server could return a result immediately and offload processing to one or more background tasks. The application will feel more responsive, even though the final results may take a short while to appear.

Use Alternative Data Systems

Examine alternative systems such as Elasticsearch, which provides faster, richer, and more appropriate search results than standard, full-text database queries. Background processes could populate Elasticsearch indexes, which are then used for search queries. While this now means you have two database systems to manage and optimize, it could reduce bottlenecks and improve functionality.

Remove or Optimize Social Media Buttons

Social media sharing buttons are regularly added to websites to improve engagement and publicize content on other platforms:

Those innocent buttons have a high cost: Facebook’s share button downloads 786KB of code (216KB gzipped). Twitter adds a further 151KB (52KB) and LinkedIn 182KB (55KB). Adding a few buttons considerably increases page weight, and processing a megabyte or two of JavaScript has a detrimental effect on performance—especially on mobile devices. That could be the start of your problems, for various reasons listed below.

  • The code is not sitting idle. Regardless of whether or not someone clicks a button, your visitors are being monitored across your site and others.
  • You may be liable for the use—or misuse—of personal data. The European Court of Justice ruled in 2019 that sites voluntarily sharing visitor information with a social network are considered joint data controllers.
  • Third-party JavaScript is a security risk (see the next section).
  • Supporting every social media platform is impossible. You’re likely to miss options, and some services don’t provide sharing facilities.
  • Site engagement can be reduced if your visitors are tempted to stay on the social network.

The risks are high, given just 0.2% of visitors use the buttons. (Sources: GOV.UK and Moovweb.)

If your site owners understand the hazards but still want to keep the buttons, there’s a couple of options for retaining sharing without adversely affecting performance, privacy, and security.

Any page can be shared on Facebook with a link like this:

https://www.facebook.com/sharer/sharer.php?u=${url}

Likewise for Twitter:

https://twitter.com/intent/tweet?url=${url}&text=${title}

And LinkedIn:

https://www.linkedin.com/shareArticle?mini=true&url=${url}&title=${title}

In these examples, ${url} is the page URL and ${title} is the title (perhaps the text contained in the page’s <title> tag).

Most social networks offer similar URL-based APIs. They’re lightweight and only activate when a user chooses to engage with the platform. You can implement these in standard <a> tags and, if necessary, intercept the click with JavaScript to open the link in a new window.

Use the Web Share API

Visitors can use their browser’s Share facility to post URLs to social media apps as well as email, messaging, Pocket, WhatsApp, and more.

The option is normally provided on mobile browsers, but it may not be obvious to users. Progressive Web Apps (see Chapter 5) can also hide the browser interface.

Fortunately, the Web Share API was introduced in Chrome 76 on Android, Safari 12.3 on iOS, and Safari 12.1 on macOS. The API hands information to the host operating system, which knows which apps support sharing.

The sharing UI can be shown in response to a user click. The following JavaScript checks whether the Web Share API is supported, then adds a button click handler that passes a ShareData object to navigator.share():

// is the Web Share API supported?
if ( navigator.share ) {

  // share button click handler
  document.getElementById('share').addEventListener('click', () => {

    // share page information
    navigator.share({
      url: 'https://example.com/',
      title: 'My example page',
      text: 'An example page implementing the Web Share API.'
    });

  });

}

The ShareData object contains:

  • url: the URL being shared (an empty string denotes the current page)
  • title: the document title (perhaps the page’s HTML <title> string)
  • text: arbitrary body text (perhaps the page’s description meta tag)

Unlike with share buttons, it’s possible to share a page #target such as an individual section or comment rather than the primary URL.

navigator.share() returns a Promise so .then() and .catch() blocks can be used if you need to perform other actions or react to failures.

Be Wary of Third-party Scripts

Analytics systems, advertising platforms, social media buttons, and custom widgets often require you to add a third-party <script> (from another domain). Those scripts may be huge or grow without you realizing.

Third-party scripts also run with the same site-wide rights and permissions as your own code. As well as hindering performance, they can track users, upload data elsewhere, change your content, redirect to other pages, trigger ecommerce transactions, auto-click advertisements, or perform any other malicious actions.

Your performance, privacy, and security is only as good as the weakest provider. Ensure third-party scripts:

  • are delivered over HTTPS to eliminate man-in-the-middle attacks
  • use <script crossorigin="anonymous"> to ensure there’s no exchange of user credentials via cookies or other technologies
  • set a <script> integrity attribute with a file hash to reject any script that’s been changed by the provider (refer to Subresource Integrity on MDN)

Ideally, move the script to your domain or remove it entirely.

Third-party Script Used to Target Site

British Airways was fined US$232 million in 2018 when 500,000 customers had their names, email addresses, and full credit card information stolen during website transactions. The attack originated from a third-party script that was modified to target BA, possibly without the knowledge or consent of its supplier.

Use Responsive Images

The <img> tag offers optional srcset and size attributes which are well-supported in most browsers (except IE). These allow specific images to be requested according to the size of the element and pixel density.

CSS Resolution

Modern smartphones offer screens with very high native resolutions, known as HiDPI or Retina displays. Each pixel is almost invisible to the naked eye, so the browser implements a CSS resolution such as 360x760px, where the native resolution could be 1440x3040px. The display density is therefore 4x, and a single CSS pixel will be using 4x4 (16) physical pixels.

Given a 100x100px space, it’s usually optimal to load a 100x100px image. However, the image quality can look comparatively poor on a 4x display density, so a 400x400px image could be preferable. The srcset attribute can define appropriate images in a standard <img> tag:

 <img width="100" height="100"
      alt="responsive image"
      src="img-100.jpg"
      srcset="img-100.jpg 1x,
              img-200.jpg 2x,
              img-300.jpg 3x,
              img-400.jpg 4x" />

The browser will select and download the most appropriate image for the display density. This ensures the best image quality without end users having to download unnecessarily large images on all devices.

The image referenced in the src attribute is used when the browser doesn’t support srcset.

image-set() and Media Queries

The CSS image-set() function offers similar options for background images, but support is currently limited.

An alternative that works in most browsers is the CSS resolution media query, although the code is more verbose.

Alternatively, you can target images based on the rendered width of the <img> element:

 <img alt="responsive image"
      src="small.jpg"
      srcset="small.jpg 400w
              large.jpg 800w" />

The w unit defines the image file’s actual width in pixels. Don’t use px as you would normally expect.

small.jpg is used when the viewport is below 400px, but large.jpg is used on screens where the CSS or physical pixels exceed 400px.

This example is only practical when the image is the full width of the viewport. The sizes attribute defines the size of the image in relation to the viewport so the width can be calculated:

 <img alt="responsive image"
      src="small.jpg"
      srcset="small.jpg 200w
              large.jpg 400w"
      sizes="50vw" />

The image width is 50vw—half the viewport. small.jpg is used when the image width is 200px or less (the viewport is therefore less than 400px), but large.jpg is used when the image width is greater.

The sizes attribute can contain complex media queries and a final fallback size to determine the image width in multiple viewport dimensions. For example:

 <img alt="responsive image"
      sizes="(max-width: 299px) 100vw,
             (min-width: 300px) and (max-width: 799px) calc(100vw - 60px),
             50vw" />

The Bandwidth Cost of Larger Images

A 400x400px image could have a file size 16x greater than its 100x100px equivalent. It requires considerably more bandwidth, which could lead to a poor experience on a mobile network.

Presume smaller images are 20KB and the larger version is 200KB. Each page contains five images and 1,000 page views are made per day. The daily bandwidth saved by using smaller images is 900MB—or 330GB per year.

A compromise—perhaps 200x200px—could look reasonable without adversely affecting performance.

Define Responsive Image Aspect Ratios

Since the advent of responsive web design, developers have been advised not to set width and height attributes on <img> tags. The CSS then sets width: 100% or max-width: 100% to ensure the image is sized to the width of its container or the maximum dimensions of the image accordingly.

The technique has an unfortunate side-effect: when images start to load, the page must reflow to allocate space. You’ll often experience this on mobile devices, where the text you’re reading suddenly moves off-screen because an image suddenly appears further up the page.

An aspect ratio defines the relationship between the height and width, so it becomes possible to calculate the size when only one dimension is known. From Firefox 71 and Chrome 79, the browser parses <img> width and height attributes to calculate the aspect ratio. The appropriate space can then be reserved so reflows aren’t required:

<!-- image has a 4:3 aspect ratio -->
<img src="image.jpg" width="400" height="300" alt="my image" />

Choosing Height and Width

Any appropriate width or height can be used to set the aspect ratio, since it will be resized using CSS. For example, width="4" height="3". That said, it’s best to set a reasonable size to ensure the image is visible in very old browsers, or when CSS fails to load or is disabled.

The following CSS ensures the image uses the full width of its container and sets a height according to the aspect ratio:

img {
  width: 100%;
  height: auto; /* this is essential */
}

The browser will reserve appropriate space on the page so re-flows become unnecessary. Browsers that don’t calculate the aspect ratio won’t reserve any space, but there are no downsides. The image will remain responsive.

HTML and CSS Proposals for Defining Aspect Ratios

There are also proposals to define aspect ratios using an HTML intrinsicsize="400x300" attribute or a CSS aspect-ratio: 4/3 property. These would provide alternative options for avoiding reflows, so keep an eye on new browser releases!

Implement Art Direction

The HTML <picture> element is similar to <audio> and <video> in that it will request one of its child elements according to browser support and conditions. For example, it can be used to load a smaller WebP image or fall back to a standard JPG:

<picture>
  <source type="image/webp" srcset="image.webp" />
  <img src="image.jpg" alt="JPG image" />
</picture>

CDN and Server-side Solutions

Some image CDNs and server-side solutions can deliver the most optimum image based on the HTTP request, so just an <img> tag would be required.

The <picture> element can also be used for art direction. Different images are requested according to the dimensions and orientation of a device. Consider the following hero photograph:

The landscape image looks reasonable on a typical desktop monitor, but detail is lost on smaller devices held in portrait orientation. It would also become difficult to overlay text in the smaller space.

Using art direction, we can serve a more appropriate image showing the main subject with less background detail:

This looks better on a smartphone held in portrait orientation and, in this case, the file size is 65% smaller (59KB compared to 168KB):

The <source> items in a <picture> element can set media queries to determine which image is requested. For example, use landscape.jpg when the viewport width is greater than the height, or fall back to portrait.jpg otherwise:

<picture>
  <source srcset="landscape.jpg"
           media="(min-aspect-ratio:1/1)" />
  <img src="portrait.jpg" alt="portrait image" />
</picture>

Any number of <source> images can be defined with differing media queries. Each is processed in the specified order until a match is found. A default <img> should always be set as a fallback when no match is available, or for older browsers that don’t support <picture>.

Lazy Load Images and Iframes

The average web page requests almost 1MB of images. Half of all websites load significantly more! These images (and embedded <iframe> elements) download regardless of whether they’re viewed or not. A large off-screen image requires bandwidth and processing even when the user clicks a link at the top of the page and never scrolls down.

Load times, bandwidth, and device requirements can be reduced by lazy loading images and iframes when they’re scrolled into the viewport. Chrome 76 and above support native lazy loading with the new loading attribute:

<img src="image.png" loading="lazy" alt="lazy load" />
<iframe src="https://site.com/" loading="lazy"></iframe>

The following values can be set:

  • auto: the browser’s default behavior (identical to not using the attribute)
  • lazy: defer loading until the resource reaches a distance from the viewport
  • eager: load the resource immediately

The distance from the viewport can vary according to the type of resource, the network connection, and whether Lite mode/Save-Data is enabled. (Lite mode/Save-Data is covered later in this chapter.)

Native lazy loading is new, so non-Chrome and older browsers require JavaScript-based solutions such as progressive-image.js. These analyze scroll and resize events or use the Intersection Observer API to determine when an element is in view. As well as supporting more browsers, they can also implement attractive loading effects.

Play Audio and Video on Demand

Auto-playing media saps bandwidth, degrades performance, and is unlikely to be appreciated by users. Modern browsers will also block or silence auto-playing by default.

In most cases, it’s preferable to show a thumbnail image—perhaps with a play icon overlay—which the user can click to start the media. Both the <video> and <audio> elements support this feature with the following attributes:

  • autoplay="false" to stop auto-playing
  • preload="none" to prevent media preloading or preload="metadata" to fetch meta data such as the video duration
  • poster="image.jpg" to show a thumbnail image
  • controls="true" to enable native playback controls

Here’s an example:

<video controls="true"
       autoplay="false"
        preload="metadata"
         poster="videothumb.jpg">
  <source src="video.mp4" type="video/mp4">
  <source src="video.webm" type="video/webm">
</video>

Alternatively, a JavaScript solution could be implemented that replaces a (lazy loaded) <img> with appropriate <video> or <audio> elements when clicked. The solution could also work for third-party video providers such as YouTube and Vimeo, which provide custom video players.

Replace Images with CSS3 Effects

The days of slicing and dicing images in a graphic package to create custom fonts, rounded corners, shadows, linear gradients, and transparency effects have long gone. CSS3 options such as web fonts, border-radius, text-shadow, box-shadow, color gradients, and opacity are quicker to implement, easier to change, and require far fewer bytes than images.

An element, image, or background image can be manipulated using CSS3 effects rather than having to create multiple variations. For example:

  • The clip-path and mask properties can partially or fully hide parts of an image or element to create non-rectangular shapes.
  • The shape-outside, shape-margin, and shape-image-threshold properties can be used to define non-rectangular text flows around or within an element.
  • The transform property can rotate, scale, and skew an element.
  • The filter property offers possibilities such as blurring, brightness, contrast, hue rotation, inversion, saturation, grayscale, sepia, opacity, and shadows.
  • Both background-blend-mode and mix-blend-mode control how backgrounds and images blend with each other in a similar way to Photoshop layers. Options include normal, multiply, screen, overlay, darken, lighten, color-dodge, color-burn, hard-light, soft-light, difference, exclusion, hue, saturation, color, and luminosity.

CSS3 Effects Can Be Costly

CSS shadows, gradients, and filters may be costly during browser repaints. Use the effects sparingly and test their impact on scrolling and animation performance.

Use SVGs Effectively

Scalable Vector Graphics define points, lines, and shapes as vectors in XML. Unlike bitmaps, SVG images can be scaled to any dimensions without increasing the file size or losing quality. This makes them ideal for logos, charts, and diagrams.

It’s possible to create and manipulate SVGs manually, on the server, or in client-side JavaScript. However, more complex images will require a graphics package such as Adobe Illustrator, Affinity Designer, Inkscape, or SVG edit, followed by an optimization clean-up in svgo or SVGOMG.

There are three primary ways to add an SVG to a web page. Choose the most appropriate option for each graphic you’re using.

1. Add SVGs Using an <img> Tag

The SVG acts like any normal image: it can be cached and reused on other pages.

For security reasons, browsers will disable embedded scripts, links, and other types of interactivity. Some browsers won’t apply style sheet rules defined in a separate CSS file.

The lesser-used <object>, <embed>, and <iframe> elements can circumvent these restrictions, but the browser treats the image as another document, so performance could be affected.

2. Add SVGs as CSS Background Images

An SVG can be referenced as a URL in a background image:

.mysvgbackground {
  background-image: url('image.svg');
}

It can also be embedded inline:

.mysvgbackground {
  background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 600"><circle cx="400" cy="300" r="50" stroke-width="5" stroke="#f00" fill="#ff0" /></svg>');
}

Like <img>, the browser will block embedded scripts, links, and other SVG interactions, but backgrounds can be useful for regularly used icons.

Inline Data for Larger Images

Inline data should be avoided for larger images, especially when regular changes will invalidate the whole style sheet cached in the browser.

3. Embed SVGs into the Page

An SVG can be embedded directly into the HTML:

<body>
  <svg class="mysvg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 600">
    <circle cx="400" cy="300" r="50" />
  <svg>
</body>

The SVG nodes become part of the DOM and can be styled or animated directly using CSS:

circle {
  stroke-width: 1em;
}

.mysvg {
  stroke-width: 5px;
  stroke: #f00;
  fill: #ff0;
}

This reduces SVG code weight by reusing CSS styles, and it offers additional flexibility such as alternative colors, hover effects, animation of specific elements, and so on.

Unfortunately, the SVG must be embedded into every page where it’s required. This will increase HTML weight, so embedding is generally best for small or infrequently used SVGs.

Consider Image Sprites

Often-used images can be packaged into a single sprite file so individual items can be accessed in CSS. This is an old optimization technique, but it continues to offer advantages:

  1. A single HTTP request is required for many images (although this is less beneficial with HTTP/2).
  2. A single image will normally result in a smaller overall file size than the total weight of the individual images.
  3. All referenced images appear instantly after the sprite has loaded.

The following image defines five 64x64px icons in a single 320x64px 24-bit PNG:

Background position offsets are then defined in CSS:

.sprite {
  width: 64px;
  padding: 64px 0 10px 0;
  text-align: center;
  background: url("browser-sprite.png") 0 0 no-repeat;
}

.sprite.edge    { background-position: -64px  0; }
.sprite.firefox { background-position: -128px 0; }
.sprite.opera   { background-position: -192px 0; }
.sprite.safari  { background-position: -256px 0; }

Individual images can then be referenced in HTML using class names:

<div class="sprite chrome">Chrome</div>
<div class="sprite edge">Edge</div>
<div class="sprite firefox">Firefox</div>
<div class="sprite opera">Opera</div>
<div class="sprite safari">Safari</div>

The result:

Image sprites can be generated in a graphics package, using tools such as SpriteCow or Instant Sprite, or in your build process.

Consider OS Fonts

It’s possible to add dozens of fonts to a page … but that doesn’t mean you should!

  1. Designers recommend using fonts sparingly, with one or two typefaces per document.
  2. A custom font typically requires a few hundred kilobytes of data. The more you add, the larger the page weight, and the worse the performance.
  3. The days of every site using standard OS fonts are over. Perhaps Helvetica, Times New Roman, or Comic Sans would look good on your site?!

Using an OS font provides a noticeable performance boost; there’s no download delay or flash of unstyled or invisible text.

Each platform supplies different default fonts, but fallbacks can be specified as well as the generic font family names of serif, sans-serif, monospace, cursive, fantasy, and system-ui. For example:

body {
  font-family: Arial, Helvetica, sans-serif;
}

Web apps may also feel more native if they use a standard system font. The following stack implemented on GitHub.com targets system fonts available across all popular platforms:

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, 
  Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}

Similar variations are used by Medium.com and the WordPress administration panels:

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, U
  buntu, Cantarell, "Helvetica Neue", sans-serif;
}

Alternatively, the CSS @font-face local() function can be used to locate a font on the user’s system first, but load from a URL when it can’t be found:

@font-face {
  font-family: MyHelvetica;
  src: local("Helvetica Neue"),
       local("HelveticaNeue"),
       url("/fonts/Helvetica-webfont.woff2") format("woff2"),
       url("/fonts/Helvetica-webfont.woff") format("woff");
}

An OS font should be your first choice if it closely matches branding requirements.

Many designers will be horrified by the suggestion of using OS fonts, so web font use is inevitable. The most popular option is to use a repository that serves fonts from a CDN. Popular options include:

Where possible, load fonts using a <link> in your HTML <head>. For example:

<link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet">

This downloads the font in parallel with other fonts and style sheets.

A CSS @import method may be offered by the repository, but this blocks processing of the style sheet until the font has been downloaded and parsed.

Limit Font Styles and Text

Only request the fonts, weights, and styles you require—and definitely remove any fonts you aren’t using!

Here’s an example of two Google Font URLs:

  • https://fonts.googleapis.com/css?family=Inconsolata:500,700
  • https://fonts.googleapis.com/css?family=Roboto:bolditalic

Both fonts can be contained in a single URL:

  • https://fonts.googleapis.com/css?family=Inconsolata:500,700|Roboto:bolditalic

In some cases, you may only need specific characters—perhaps for a regularly used title or logo. The text “Hello” requires just four characters from a specific font:

Finally, you could benefit from hosting the fonts locally or using more popular fonts that have a higher chance of being pre-cached in the user’s browser.

Use a Good Font-loading Strategy

A web font can take several seconds to download. The browser will choose one of two options:

  1. Show a flash of unstyled text (FOUT). The first available font fallback is used immediately. It’s replaced by the web font once it’s loaded. This process is used by IE, Edge 18 and below, and older editions of Firefox and Opera.
  2. Show a flash of invisible text (FOIT). No text is displayed until the web font has loaded. This process is used in all modern browsers, which typically wait three seconds before reverting to a fallback.

Either option can be jarring and affect perceived performance.

The CSS font-display property allows you to define the font-handling process. The options are:

  • auto: the browser’s default behavior (usually FOIT).
  • block: effectively FOIT. The text may be invisible for up to three seconds. There’s no font swap, but text can’t be read immediately.
  • swap: effectively FOUT. The first fallback is used until the web font is available. Text can be read immediately, but the font swap effect may be jarring if not managed effectively.
  • fallback: a compromise between FOIT and FOUT. Text is invisible for a short period (typically 100ms) then the first fallback is used until the web font is available. Text is readable as the page loads, but the font swap can still be problematic.
  • optional: the same as fallback, except no font swapping occurs. The web font will only be used if it’s available within the initial period. The first page view is likely to show a fallback font while the web font is downloaded and cached. Subsequent page views will use the web font.

Similar Web and OS Fonts

optional could be a reasonable choice if the web and OS fallback fonts are similar, but if that’s the case, using an OS font throughout would offer better performance!

Example CSS:

@font-face {
  font-family: 'mytypeface';
  src: url('mytypeface-webfont.woff2') format('woff2'),
       url('mytypeface-webfont.woff') format('woff');
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}

Google Fonts also provides a display URL query string parameter. For example:

Settings for Specific Text Types

Different text blocks could use different font-display settings. For example, body text could use swap (FOUT) so it can be read immediately, while menus and heading text use block (FOIT).

A pragmatic compromise could be considered, which uses a fallback font with similar weights, line heights, and spacing to the web font. font-display: swap (FOUT) can then be used, but the replacement effect is less noticeable.

A tool such as Font Style Matcher can be used to find suitable fallback parameters.

Consider Variable Fonts

OpenType 1.8 introduced variable fonts, and they’re supported in most browsers (except IE). Rather than creating multiple files for each variation of the same typeface, a font is defined with minimum and maximum vector limits along an axis.

Any weight between the two extremes can be interpolated. A single variable font can therefore be used instead of several variations in order to reduce page weight and improve performance.

Open-source and commercial variable fonts can be found at sites including:

These can then be loaded using @font-face with a woff2-variations format and the allowable ranges. For example:

@font-face {
  font-family: 'VariableFont';
  src: 'variablefont.woff2' format('woff2-variations');
  font-weight: 200 800;
  font-stretch: 75% 125%;
  font-style: oblique 0deg 20deg;
}

Browser support for variable fonts can be tested using @supports with font-variation-settings:

body {
  font-family: sans-serif;
}

@supports (font-variation-settings: 'wght' 500) {

  body {
    font-family: 'VariableFont';
  }

}

Aspects of the typeface can then be adjusted in CSS, including the weight (typically 0 to 1000):

font-weight: 500;
/* or */
font-variation-settings: 'wght' 500;

Also width—or stretch—can be adjusted to produce condensed and extended variations (100% is normally the default, with lower values creating narrower fonts and higher values creating wider fonts):

font-stretch: 80%;
/* or */
font-variation-settings: 'wdth' 80;

Whether or not italics are required can also be set (either on or off, since italics are often defined as a different character set):

font-style: italic;
/* or */
font-variation-settings: 'ital' 1;

Also slant—or oblique—can be adjusted, which modifies the axis in a different way from italic (typically between 0 and 20 degrees):

font-style: oblique 10deg;
/* or */
font-variation-settings: 'slnt' 10;

The shorthand font-variation-settings property allows multiple font aspects to be set:

font-variation-settings: 'wght' 300, 'wdth' 100, 'slnt' 0;

OS Fonts as Variable Font Fallback

It’s possible to download a single variable font but retain multiple fonts for older browsers. Unfortunately, modern browsers will download every font specified, which negates any performance benefit. It’s therefore preferable to use an OS font as the fallback.

Use Modern CSS3 Layouts

For many years, it’s been necessary to use CSS floats to lay out pages. The technique was always a hack and required considerable code, along with endless margin/padding tweaking to make the layout work. Even then, floats break at smaller screen sizes unless media queries are used.

Floats are no longer necessary:

  • Flexbox should be used for one-dimensional layouts, which (can) wrap to the next row according to the widths of each block. It’s ideal for menus, image galleries, cards, etc. Flexbox is supported by most browsers including IE10+.
  • Grid is for two-dimensional layouts with explicit rows and columns. It’s ideal for page layouts. Grid is supported by most browsers, although IE10/11 use an older version of the standard.

Both options are simpler to develop, use far less code, can adapt to any screen size, can remove the need for media queries, and render faster than floats because the browser can natively determine an optimum layout.

Fallbacks for Older Browsers

It’s possible to use float-based fallbacks for older browsers. However, it’s often better to use a simpler, single-column layout rather than trying to emulate what you achieved using Flexbox or Grid. Pixel perfection is futile!

Remove Unused CSS

The smaller your style sheet, the quicker it will download, the sooner it will parse, and the faster your page will become.

We all start with good intentions, but CSS can bloat over time as the number of features increases. It’s easier to retain old, unnecessary code than remove it and risk breaking something. Those using a CSS framework such as Bootstrap may find they’re only using a fraction of the facilities.

CSS removal recommendations:

  1. Organize CSS into smaller files (partials) with clear responsibilities (which can be concatenated into a single file at build time). It’s easier to remove a carousel widget if the CSS is clearly defined in widgets/_carousel.css.
  2. Consider naming methodologies such as BEM to aid the development of discrete components.
  3. Avoid deeply nested Sass/pre-processor declarations. The expanded code can become unexpectedly large.
  4. Avoid using !important to override the cascade.
  5. Avoid inline styles in HTML.

Chrome’s Coverage panel helps locate unused CSS and JavaScript code. Select Coverage from the DevTools More tools sub-menu, then hit the record button and browse your application. Click any file to open its source. Unused code is highlighted in red in the line number gutter.

Coverage for Single Pages Only

Chrome doesn’t remember used/unused code as you navigate to new pages! The Coverage panel is only practical for single-page applications.

The following tools provide options to analyze HTML and CSS usage either at build time or by crawling URLs so that redundant code can be identified. Note that some configuration will be required to ensure styles triggered by JavaScript and user interactions are whitelisted.

Alternatively, a visual regression system such as Percy could be used to compare old and new screenshots.

Those preferring a manual—and considerably more hardcore—process could add an invisible background image to suspicious selectors. For example:

/* check usage */
.amiused1 {
  color: #abc;
  background-image: url(/used.png?.amiused1/);
}

#another .suspect {
  color: #123;
  background-image: url(/used.png?#another-.suspect/);
}

Either selector can be removed if no reference to their background image appears in server logs over a reasonable usage period.

Be Wary of Expensive CSS Properties

Not all CSS properties are created equally. Those that take longer to paint than others include:

This does’t mean you shouldn’t use them, but be wary of applying expensive effects to hundreds of elements, as it will affect rendering and scrolling performance.

Keeping Selectors Simple

Try to simplify CSS selectors where possible. CSS performance improvements may be negligible, but simpler selectors are easier to maintain, reduce page weight, and have a better chance of working in older browsers.

Embrace CSS3 Animations

Native CSS3 transitions and animations will always be faster and require less code than JavaScript-powered equivalents. It shouldn’t be necessary to add a library or framework for typical fade, show, hide, and move effects. Very old browsers may not support the properties, but CSS degrades gracefully, and users will rarely know they’re missing anything.

JavaScript animations should only be considered when fine-grained control is required—such as for HTML5 games, interactive charts, <canvas> manipulation, and so on.

Avoid Animating Expensive Properties

Once the browser has parsed the HTML document and styles, it renders elements in three stages:

  1. Layout: the calculation of how much space an element requires and how it affects elements around it
  2. Paint: the filling of pixels with color
  3. Composite: the drawing of layers in the correct order when they overlap

Animating the dimensions or position of an element can cause the whole page to re-layout on every frame. Performance can therefore be improved if an animation only affects the compositing stage. The most efficient animations only use:

  1. opacity and/or
  2. transform to translate (move), scale, skew, or rotate an element (the original space the element used is not altered so the layout is not affected)

Browsers often use the hardware-accelerated GPU to render these effects in their own layer. If neither property is ideal for your animation, consider taking the element out of the page flow with position: absolute; or similar to avoid complex layout changes.

Indicate Which Elements Will Animate

The will-change property allows CSS authors to indicate how an element will be animated so the browser can make performance optimizations in advance—for example, to declare that an element will have a transform applied:

.myelement {
  will-change: transform;
}

Any number of comma-separated properties can be defined. However:

  • Only use will-change as a last resort to fix animation issues. It should not be used to anticipate performance problems.
  • Don’t apply it to too many elements.
  • Give it sufficient time to work. Don’t begin animations immediately.

Use CSS Containment

CSS Containment is a new (experimental) feature that indicates when an element’s subtree is independent from the rest of the page. This can improve rendering performance during animations or when elements are added, modified, or removed from the DOM. The new CSS contain property accepts one or more of the following values in a space-separated list:

  • none: containment is not applied.
  • layout: the internal layout of the element is isolated from the rest of the page. Its content cannot have any effect on ancestor elements.
  • paint: children of the element will not be displayed outside its boundary. Any overflows will not be visible (similar to overflow: hidden;).
  • size: the size of the element can be determined without checking its children. The dimensions are independent of the content.
  • style: counters and quotes cannot appear outside the element. (This value may be dropped from the specification.)

Two special values are also available:

  • strict: all containment rules except style are applied. This is equivalent to contain: layout paint size;.
  • content: all containment rules except size and style are applied. This is equivalent to contain: layout paint;.

Imagine you have a page with an unordered <ul> list containing one thousand child <li> list elements. If you change the contents of a single item that has contain: strict; applied, the browser won’t attempt to recalculate the size or position of that item, others in the list, or any other elements on the page.

Check the Save-Data Header

The Save-Data field is an HTTP request header indicating that reduced data usage is preferred. It’s named Lite mode in Chrome and can be enabled or disabled by the user.

When enabled, the Save-Data header is sent with every browser request. For example:

GET /image.jpg HTTP/1.0
Host: example.com
Save-Data: on

A server can respond accordingly when Save-Data is detected. For example, it can respond by:

  • reducing the volume of HTML content—such as returning 100 rows of table data rather than 500
  • providing low-resolution versions of an image even when high-resolution options are requested
  • removing non-essential JavaScript such as trackers or advertising scripts

To ensure the minimal content is not cached and reused after the user disables Save-Data, the server should set the following header in the HTTP response:

Vary: Accept-Encoding, Save-Data

The Save-Data header can also be detected using client-side JavaScript:

if ('connection' in navigator && navigator.connection.saveData) {
  // Save-Data enabled
}

An optimum solution could presume data-saving by default, but add a full-data class to the HTML element when the header is not enabled:

if ('connection' in navigator && !navigator.connection.saveData) {
  document.documentElement.classList.add('full-data');
}

CSS and JavaScript components could then react accordingly. For example:

header {
  background-image: url("low-res-hero.jpg");
}

.full-data header {
  background-image: url("high-res-hero.jpg");
}

Adopt Progressive Web App Technologies

Progressive web apps (PWAs) can enhance performance by caching essential files locally. They’re usually more responsive than standard web apps and can even be faster than native apps.

PWAs comprise a mixture of technologies that make web apps function like native mobile apps and overcome the constraints imposed by web-only and native-only solutions:

  1. The app requires a single codebase developed with open, standard W3C web technologies.
  2. Users can discover and install a PWA from the Web. There’s no need to abide with app store rules or fees.
  3. PWAs can work offline and update automatically.

Most tutorials describe how to build a native-looking, single-page, mobile-like app. However, any site can benefit from PWA technologies and be working within a few hours. There are three essential requirements …

1. Enable HTTPS

PWAs require an HTTPS connection, although Chrome, for example, permits an HTTP localhost or 127.x.x.x address during testing.

2. Create a Web App Manifest

The web app manifest provides information about your application, such as the name, description, and images. These are used by the OS to configure home screen icons, splash pages, and viewport settings.

The manifest is a JSON text file in the root of your app. It must be served with a Content-Type: application/manifest+json or Content-Type: application/json HTTP header:

{
  "lang"              : "en-US",
  "dir"               : "ltr",
  "name"              : "Standard Name",
  "short_name"        : "Short Name",
  "description"       : "A description of the site/app",
  "scope"             : "/",
  "start_url"         : "/",
  "display"           : "minimal-ui",
  "theme_color"       : "#fff",
  "background_color"  : "#fff",
  "icons": [
    {
      "src"           : "https://site.com/icon-076.png",
      "sizes"         : "76x76",
      "type"          : "image/png"
    },
    {
      "src"           : "https://site.com/icon-192.png",
      "sizes"         : "192x192",
      "type"          : "image/png"
    },
    {
      "src"           : "https://site.com/icon-512.png",
      "sizes"         : "512x512",
      "type"          : "image/png"
    }
  ]
}

A list of manifest properties can be found on MDN, or you can use the Generate Web Manifest tool.

A link to the manifest file is required in the <head> of all your pages:

<link rel="manifest" href="/app.webmanifest">

3. Create a Service Worker

Service workers are programmable proxies that can intercept and respond to network requests. They’re a single JavaScript file that resides in the application root.

Your page JavaScript must check for service worker support and register the file:

if ('serviceWorker' in navigator) {

  // register service worker
  navigator.serviceWorker.register('/service-worker.js');

}

service-worker.js then triggers and reacts to events, including:

  • install when the app is first run. This can be used to cache regularly used files.
  • fetch when a network request is made. This can return a cached file or make further network requests.
const
  staticCacheName = 'cache-v1';
  filesToCache = [
    '/',
    'style/main.css',
    'js/main.js',
    'images/hero.jpg'
  ];

// install event: cache regularly used files
self.addEventListener('install', event => {

  event.waitUntil(
    caches.open(staticCacheName)
    .then(cache => {
      return cache.addAll(filesToCache);
    })
  );

});

// fetch event: serve files from cache or network
self.addEventListener('fetch', event => {

  event.respondWith(
    caches.match(event.request)
    .then(response => {
      if (response) {
        return response; // from cache
      }
      return fetch(event.request); // from network
    })
    .catch(error => {})
  );

});

This example doesn’t update cached files or attempt to cache further requests, but it illustrates the basics of progressive web apps. Further PWA tutorials can be found at:

Power Down Inactive Tabs

Although we’re mostly concerned with page performance when a user interacts with our site, we should also be responsible when the tab is inactive.

Browsers normally throttle events such as requestAnimationFrame, intervals, and timeouts on inactive tabs, but we can take this further to auto-pause and resume games, animations, video playback, Ajax polling, WebSocket handling, background loading, notifications, and so on. The less work an inactive tab does, the longer the smartphone battery will last, and the more likely the user can return to your site!

The Page Visibility API can be used to detect whether or not a tab is active and trigger an event when visibility changes. The following code adds a tab-active class to the <html> element when the tab is being viewed:

console.log('tab is', isTabActive() ? 'active' : 'not active');

document.addEventListener('visibilitychange', isTabActive);

function isTabActive() {

  if (document.visibilityState === 'visible') {
    // tab is active
    document.documentElement.classList.add('tab-active');
    return true;
  }
  else {
    // tab is inactive
    document.documentElement.classList.remove('tab-active');
    return false;
  }

});

CSS could then be used to start or stop animations. For example:

.myelement {
  animation: something 3s linear 1s infinite alternate;
  animation-play-state: paused;
}

.tab-active .myelement {
  animation-play-state: running;
}

Other Throttling Techniques

Similar throttling techniques could be used with these tools:

  • The NetworkInformation API, to determine when connection speeds could affect performance. The API is experimental, has limited support, and may not be accurate.
  • The Battery Status API, to detect when device power falls below a specific threshold. While this may be implemented in some browsers, the API was dropped as a web standard owing to privacy concerns. An individual could be identified and tracked by their fairly unique battery status.
  • The Ambient Light API, to determine whether a device is being used in strong or dim lighting and modify the theme accordingly—such as increased contrast in strong sunlight and dimmer colors in darker situations. The API is experimental and has limited support.

Consider Inlining Critical CSS

Google page analysis tools often make a suggestion to “inline critical CSS” or “reduce render-blocking style sheets”. Loading a CSS file blocks rendering, so performance can be improved by:

  1. Extracting the styles used to render elements above the fold. Tools such as critical and criticalCSS can help.
  2. Inlining those styles in a <style> element in the HTML <head>.
  3. Loading the main CSS file asynchronously using JavaScript at the bottom of the page, or perhaps once the DOM is ready.

The technique noticeably improves performance—even on a fast connection—and will boost audit scores. It could benefit progressive web or single-page apps with consistent interfaces, but may be more difficult to manage on other sites:

  • The “fold” is different on every device.
  • Many sites have a variety of page layouts. Each could require different critical CSS, so a build tool becomes essential.
  • Critical CSS tools can struggle with specific frameworks, HTML generated by client-side code, or dynamic, event-driven changes.
  • The technique mostly benefits the user’s first page load. CSS is cached for subsequent pages so additional inlined styles will increase page weight.

Provide Accelerated Mobile Pages (AMP)

The AMP project was announced in October 2015. A collaboration between Google and more than 30 news publishers aimed to improve mobile web performance. AMP is an open-source web component framework which claims that “you can easily create user-first websites, stories, emails, and ads.”

AMP requires you to publish existing or original content as an AMP HTML page. AMP HTML is a subset of HTML5, providing a limited set of web components, styles, images, videos, and advertisements. Features and styling are purposely restricted, and you can’t add custom JavaScript. Most AMP pages are served from Google’s AMP cache—a proxy-based CDN that assigns a Google-specific URL to the page. This ensures optimal delivery using Google’s global network.

AMP is fast, so it’s mentioned in this book. However, while the project may have started with noble aims, AMP has been criticized for serving Google more than publishers and users, for reasons such as this:

  • AMP is not necessarily faster or more efficient than your own optimized website.
  • Unless you go AMP-only, you must duplicate existing content pages.
  • Alternative URLs may be confusing to users.
  • Google gains control of your content, visitors, and data.
  • AMP could be considered a closed alternative to the open web.

Google wants the Web to be faster, yet AMP pages receive preferential treatment in mobile search results even when the original site is more efficient.

Ultimately, the decision is yours. There are WordPress plugins and CDNs such as Cloudflare that can automatically create AMP pages from your content but, for many sites, AMP will require further development effort. Those pages may receive additional publicity, but whether it’s you or Google who benefits is another matter.

AMP development guides:

AMP criticisms:

Feeling Full Yet?

We may have lost weight, but the only way to guarantee long-term benefits is to change our development attitude! The next chapter provides life-changing diets.