Thoughts about front end best practices

I posted this as an answer to this question in Quora and I thought I would post it here and expand on it a little bit with things I thought about after I wrote the answer.

This is not an exhaustive list of performance best practices. It’s what I use and how I use them. You may have others and some of these may not apply to you. I’d love to hear what works for you… you can contact me via Twitter (https://twitter.com/elrond25).

The question:

What are the best practices for optimizing resources (JavaScript, CSS, images) used by an HTML page?

  • In general
    • Use Lighthouse (available in Chrome as part of the DevTools audits menu or as an extension)
      • The performance score is a good sign of how your app/site is doing
      • There are other audits you can run separately or at the same time
    • Always try to serve content via HTTP2.
      • HTTP2 solves a lot of the performance issues in older versions of HTTP. See this article for the nerdy details
      • If you want to take the time to test it, http2 push may also help increase your site’s performance. It is imperative that you test this because, if poorly implemented, you can wreck performance with push
    • Use a CDN like Akamai or Cloudflare to host and serve your static assets. Even basic services are good enough based on my experience
    • Consider using a service worker even if you’re not creating a PWA.
      • Service workers will improve performance on second and subsequent visits because of the browser will fetch content from your local computer
      • A service worker is the entry point to advanced features like web push notifications, background sync and background fetch among others
      • You can configure different caching strategies based on the needs of your site or app
      • If the browser doesn’t support service workers then it won’t get the performance boost and you will lose access to the advanced features but it will still display content for your users
    • Consider preloading resources
  • For your images
    • Use responsive images rather than create a single version of the image. Using a single small image means that it’ll look like crap in retina displays for desktop and higher-end mobile devices
    • Create responsive images as part of your build process. I use Gulp and gulp-responsive
    • Serve WebP images to browsers that support them.
    • You can incorporate WebP support in your responsive images
    • WebP is significantly smaller than JPG or PNG but not all browsers support the format
    • If you can’t or don’t want to use responsive images you can compress the images with Imagemin. I also do this as part of my build process with Gulp and gulp-imagemin
    • Use an image CDN like Cloudinary or Photon if you use WordPress to host your assets. They’ll do all the work for you
  • For your scripts and stylesheets
    • Consider minimizing your scripts. I use gulp and uglify-es
    • If you will use a lot of Javascript consider using a Bundler like Webpack or Rollup
    • I’m one of the few developers who doesn’t think you need to bundle all your assets (CSS, Javascript, and images) when building your site or app
    • Test if a bundler improves performance for the content you’re using it for before you decide to adopt it
    • Consider minimizing your stylesheets. I use sass/scss and normally create a compressed version either from CLI or using gulp-sass during the build process
    • I don’t concatenate them because I cache on the client using service workers and they work better as separate items
  • HTML
    • I don’t normally minimize my HTML until the size hits 75K or so. I’m old school and a lot of what I learned when I first started working on the web was by looking at other people’s code, duplicating it locally and then tweaking it to see what happened. I think it’s still useful to learn that way.
    • With the performance optimizations for scripts, images and stylesheets I think I’ve made up for not removing whitespace from my HTML content

Defensive Coding: Default Parameters

When working with Javascript we can add default values to our function parameters so they will work if we forget to pass them when declaring the function. In this post, we’ll discuss why we should and how to give default values to our function parameters.

Why give defaults to function parameters

The simplest reason, for me, is that I tend to forget to do so when using the function I just declared. Take the function below that, in theory, should just fetch the file at the given URL and display it inside the body of the page

async function getFile(url) {
  try{
    const response = await fetch(url);
    document.body.innerHTML = await response.text();
  }
  catch {
    console.log('There was an error retrieving the file');
  }
}

But what happens if we forget to give it the URL to fetch?

getFile()

In my experiments getFile() without a URL gives the expected 404 error. How can I prevent this?

I think the best way is to assign a default value to the URL parameter so, if we forget to give it a URL it will go somewhere useful and not error out. The code now looks like this:

async function getFile(url = 'https://my-site.com/') {
  try{
    const response = await fetch(url);
    document.body.innerHTML = await response.text();
  }
  catch {
    console.log('There was an error retrieving the file');
  }
}

Now, when we leave the URL blank, it will go to the root of my-site.com.

Now for the caveat:

The request will obey CORS and CORB restrictions so if you’re pointing people to third party sites they may not work

Starting a new Node Project

Most of the time, starting a Node project involves a lot of typing, copying and pasting and typing data into your repository. This post lists some ways to automate the process in the command line and via scripts.

Thanks to Phil Nash and Tienery Cyren for the information. 🙂

npx license mit uses the license package to download a license of your choice for the project, in this case, MIT

npx gitignore node uses the gitignore package to download the appropriate .gitignore file from GitHub

npx covgen uses covgen to generate the Contributor Covenant agreement and give your project a code of conduct.

npm init -y accepts all of the default options for npm init and creates a package.json file.

npx first became available with NPM 5 and it’s also available as a standalone package. It provides a way to run Node packages either from your local installation or from your global node repository, installing whatever packages it needs to run the command. This is awesome because it means you only need to install the packages you need like license or covgen once in the global scope rather than install them in each individual project.

Customizing the init file

Going back to npm init -y. Unless you’ve done it already it’ll produce a completely blank package.json file that you have to go edit later. Better than not having it or have to create the file by hand but it’s still a pain.

Until I read an article by Phil Nash I didn’t realize that you could customize the parameters npm init uses as defaults. They look like this:

npm set init.author.name "Your name"
npm set init.author.email "[email protected]"
npm set init.author.url "https://your-url.com"
npm set init.license "MIT"
npm set init.version "1.0.0"

Once the parameters are configured, they will be used whenever you run the npm init command, whether it’s automated or not.

We can take this a step further by creating a shell script to automate the steps down to one command. I created a repo-init.sh file and add the code below in it; then put the file somewhere in your shell’s path.

git init
npx license $(npm get init.license) -o "$(npm get init.author.name)" > LICENSE
npx gitignore node
npx covgen "$(npm get init.author.email)"
npm init -y
npx eslint --init
git add -A
git commit -m "Initial commit"

This assumes a few things:

  • You want to put things in a Git repository
  • You’ve filled out the defaults for init parameters
  • You want to use Code Covenant code of conduct
  • You want to use ESLint

So with this script, you have a one-liner to get your repository set up and ready to go. Some next steps may include additional tool configuration or populating package.json with other tools you normally use.

Improving Font Performance: Work to control font loading

Because of their size fonts tend to be some of the largest components of any web pages. According to the HTTP Archive, the sum of the transfer size of all fonts (eot, ttf, woff, woff2, or otf requested by the page is 98KB for Desktop and 83.4KB for mobile.

There are several CSS and Javascript techniques to help browsers control and speed up font display and how it swaps when the web font is loaded.

The idea is to load the page as quickly as possible using fallback fonts and then swap the web font in when it’s ready.

Use font-dispay

The font-display property of the @font-face rule allows the developer to better control how/when/if web fonts change the way the text looks. It is part of the CSS Fonts Module Level 4 specification and currently supported in most major desktop browsers (except Edge) and in Chrome for Android (see caniuse entry for more details).

Using the property, @font-face declarations now look like this:

@font-face {
  font-family: 'Open Sans';
  src:  url("opensans.woff2") format("woff2"),
        url("opensans.woff") format("woff");
  font-display: swap;
}

The font display timeline is based on a timer that begins the moment the user agent attempts to use a given downloaded font face. The timeline is divided into the three periods which dictate the rendering behavior of any element using the font face.

Font block period
If the font face is not loaded, any element attempting to use it must render an invisible fallback font face. If the font face successfully loads during this period, it is used normally.
Font swap period
If the font face is not loaded, any element attempting to use it must render a fallback font face. If the font face successfully loads during this period, it is used normally.
Font failure period
If the font face is not loaded, the user agent treats it as a failed load causing normal font fallback.

Using the timeline above, we can now understand the possible values for display-font.

auto
Whatever the user agent would normally do. This varies from browser to browser
block
Gives the font face a short block period and an infinite swap period.
swap
Gives the font face an extremely small block period and an infinite swap period.
fallback
Gives the font face an extremely small block period and a short swap period.
optional
Gives the font face an extremely small block period and no swap period.

I normally use swap as the value for font-display as it gives me a quick render of the page and the correct fonts once they have downloaded. As with many things in fonts, test it and make sure that it does what you want it to do, your mileage may vary.

Use Font Face Observer

Font Face Observer is a font loader that allows you to work with fonts from multiple origins using a promise-based interface. It doesn’t matter where your fonts come from: host them yourself, or use a web font service such as Google Fonts, Typekit, Fonts.com, and Webtype.

Font Face Observer doesn’t replace @font-face declarations. You still need to declare your fonts in your CSS and use font-display like so:

@font-face {
  font-family: 'NeueMontreal';
  src:  url('/fonts/NeueMontreal-Bold.woff2') format('woff2'),
        url('/fonts/NeueMontreal-Bold.woff') format('woff');
  font-display: swap;
}

@font-face {
  font-family: 'Fuji';
  src:  url('/fonts/Fuji-Light.woff2') format('woff2'),
        url('/fonts/Fuji-Light.woff') format('woff');
  font-weight: normal;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: 'Fuji';
  src:  url('/fonts/Fuji-Bold.woff2') format('woff2'),
        url('/fonts/Fuji-Bold.woff') format('woff');
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

Then the page you want to use the fonts needs to load the Font Face Observer script, either locally:

<script src="js/fontfaceobserver.js"></script>

Or from a CDN:

<script src="https://cdnjs.cloudflare.com/ajax/libs/fontfaceobserver/2.0.13/fontfaceobserver.standalone.js"></script>

Then we create the script that will run the loader. It takes the following steps:

  1. It creates new FontFaceObserver objects for each of the fonts that we want to load
  2. It adds a class to the root element (html) to indicate that the fonts are loading
  3. It uses Promise.all to load the fonts we defined in step 1
    • If all the fonts load successfully we add the fonts-loaded class to the root element
    • if any of the fonts fail to load then Promise.all will reject and the catch portion of the chain will add the fonts-failed class to the HTML element
//1
const NeueMontreal = new FontFaceObserver("NeueMontreal");
const Fuji = new FontFaceObserver("Fuji");
const FujiBold = new FontFaceObserver("Fuji", {
  weight: "700"
});

let html = document.documentElement;

// 2
html.classList.add("fonts-loading");

// 3
Promise.all([
    NeueMontreal.load(),
    Fuji.load(),
    FujiBold.load()
  ]).then(() => {
    // 4 success
    html.classList.remove("fonts-loading");
    html.classList.add("fonts-loaded");
    console.log("All fonts have loaded.");
  })
  .catch(() => {
    // 4 failure
    html.classList.remove("fonts-loading");
    html.classList.add("fonts-failed");
    console.log("One or more fonts failed to load");
  });

Each class (fonts-loaded and fonts-failed) should match classes in your CSS that use web fonts and fallbacks as appropriate. Using different classes means that you don’t have to wait for web font download to timeout.

Evaluate using the CSS font loading API

The CSS Font Loading Module Level 3 provides a programmatic way to handle font loading and handling of related events.

Even though the specification it’s at the candidate recommendation stage, it’s supported by most modern browsers (Edge is the exception) so I’m confident in suggesting you evaluate it.

The script runs the following tasks

  1. We define a logLoaded function to log successful font loads to the console
  2. For each font we want to process we:
    • Create a new FontFace object representing the font with the following attributes:
      • Name
      • URL
      • An optional style object representing the basic characteristics (style, weight, and stretch) of the font we’re loading
    • Add the font to the fonts stack
    • Log the successful result using the logLoaded function
  3. Using the ready() method as an example we make the element with class .content visible
//1
function logLoaded(fontFace) {
  console.log(fontFace.family, "loaded successfully.");
}

//2
// These rules replace CSS @font-face declarations.
const NeueMontrealFontFace = new FontFace(
  "NeueMontreal",
  "url(/fonts/NeueMontreal-Bold.woff2)"
);
document.fonts.add(NeueMontrealFontFace);
NeueMontrealFontFace.loaded.then(logLoaded);

const fujiFontFace = new FontFace("Fuji",
      "url(/fonts/Fuji-Light.woff2)", {
  style: "normal",
  weight: "400"
});
document.fonts.add(fujiFontFace);
fujiFontFace.loaded.then(logLoaded);

const fujiBoldFontFace = new FontFace("Fuji",
      "url(/fonts/Fuji-Bold.woff2)", {
  style: "normal",
  weight: "700"
});
document.fonts.add(fujiBoldFontFace);
fujiBoldFontFace.loaded.then(logLoaded);

//3
document.fonts.ready.then(function() {
  const content = document.getElementById("content");
  content.style.visibility = "visible";
});

Use variable fonts in browsers that support them

In order to use variable fonts on your operating system, you need to make sure that it is up to date. Linux OSes need the latest Freetype version, and macOS prior to 10.13 (High Sierra) will not work with variable fonts.

Variable fonts are an evolution of the OpenType font specification that enables multiple variations of a typeface to be incorporated into a single file, rather than having a separate font file for every width, weight, or style; reducing the number of requests and, potentially, the file sizes for the font assets by downloading a single file. The drawback is that it provides all the variations for the given font and downloading it means you get all the variations whether you plan on using them or not.

Subsetting fonts will reduce the number of characters but will not remove unused instances or any data other than glyphs.

To make these variable fonts with our current CSS we need to make some modifications. Using Roboto and its values as an example, the @font-face declaration looks like this:

@font-face: Roboto;
src:  url('/fonts/Roboto-min-VF.woff2') format('woff2'),
      url('/fonts/Roboto-min-VF.woff') format('woff');
font-weight:  250 900;
font-width: 75 100;
font-style: -12 0;

We can then use values within the defined boundaries in our style sheets.

.my-class {
  font-weight: 450;
  font-style: -12;
}

We will not cover details about Variable Fonts, if you want a deeper reference, check MDN’s Variable Fonts Guide.

However, working with Variable fonts poses the following question:

When are variable fonts not the best option for your site/app?

Say, for example, that you’re only using Roboto Regular and Bold in your application, and no Open Type features.

The variable font (compressed with WOFF2) is 978KB. Compressing individual weights of the font (regular and bold) using the same tool gives me a total of 135KB.

And even if you use the 4 basic font styles (regular, italic, bold and bold-italics), the WOFF2 fonts give you a combined weight of 270KB.

So, strictly from a performance point of view, variable fonts may not be your friend if you’re not using the full feature set of a font.

Improving Font Performance: Use Resource Hints

If you’re using a third party font service like Google Fonts or Typekit you should work on mitigating potential latency. Say you have a typical Google Font embed code in your &lt;head>. You could minimize the amount of time it takes to connect with that server using the preconnect resource hint.

These hints will not load the resource but will do a DNS lookup for the host, TCP handshake, and optional TLS negotiation, all before the resource is actually requested.

<link rel="preconnect" href="https://fonts.googleapis.com/" crossorigin="anonymous">
<link rel="preconnect" href="https://fonts.gstatic.com/" crossorigin="anonymous">

A more widely compatible alternative to preconnect is dns-prefetch. It won’t establish a connection to the server, but it will resolve the DNS for the specified host, which can still speed things up a bit:

<link rel="dns-prefetch" href="https://fonts.googleapis.com/">
<link rel="dns-prefetch" href="https://fonts.gstatic.com/">