Web Performance Blog, or How JobSort Works

Table of Contents

AVIF/WebP Formats (not PNG or JPEG)

WebP lossless images are 26% smaller in size compared to PNGs. WebP lossy images are 25-34% smaller than comparable JPEG images at equivalent SSIM quality index, as outlined on Google's Developers Portal.

WebP2 is in development by Google with the goal of achieving the same compression levels as AVIF but with faster encoding and decoding times.

brew install imagemagick

# Generates a random image.
convert -size 1000x1000 xc:gray +noise gaussian random.png

# Converts the PNG to different JPEG compression levels.
convert -quality  50 random.png jpeg-q050.jpg
convert -quality  60 random.png jpeg-q060.jpg
convert -quality  70 random.png jpeg-q070.jpg
convert -quality  80 random.png jpeg-q080.jpg
convert -quality  90 random.png jpeg-q090.jpg
convert -quality 100 random.png jpeg-q100.jpg

# Converts the PNG to different WebP compression levels.
convert -quality  50 random.png webp1-q050.webp
convert -quality  60 random.png webp1-q060.webp
convert -quality  70 random.png webp1-q070.webp
convert -quality  80 random.png webp1-q080.webp
convert -quality  90 random.png webp1-q090.webp
convert -quality 100 random.png webp1-q100.webp

# Converts the PNG to different AVIF compression levels.
convert -quality  50 random.png av1-q050.avif
convert -quality  60 random.png av1-q060.avif
convert -quality  70 random.png av1-q070.avif
convert -quality  80 random.png av1-q080.avif
convert -quality  90 random.png av1-q090.avif
convert -quality 100 random.png av1-q100.avif

# Lists sizes of all files.
ls -lnh .

1.8M random.png
162K jpeg-q050.jpg
201K jpeg-q060.jpg
256K jpeg-q070.jpg
332K jpeg-q080.jpg
459K jpeg-q090.jpg
798K jpeg-q100.jpg
237K webp1-q050.webp
260K webp1-q060.webp
290K webp1-q070.webp
346K webp1-q080.webp
462K webp1-q090.webp
781K webp1-q100.webp
224K av1-q050.avif
315K av1-q060.avif
375K av1-q070.avif
452K av1-q080.avif
633K av1-q090.avif
1.9M av1-q100.avif

Definitely avoid using PNGs or JPEGs. Use WebP or the better AVIF image format.

Brotli Compression (not Gzip)

JobSort uses Brotli compression. Brotli is a successor to Gzip and you can expect a 20% reduction in response size when using Brotli vs. Gzip. If your website still uses Gzip, try to prioritize turning on Brotli everywhere. It's a quick change (likely just a toggle on your CDN's dashboard) and can have a significant impact to those users who aren't on a good Internet connection.

Let's take an example and do a napkin calculation to assess just how much better Brotli is compared to Gzip.

brew install brotli gzip

# Generates random data for testing.
jot -r -c 100000 > random.txt

# Compresses using Gzip.
gzip -c -k -1 random.txt > gzip-q01.gz
gzip -c -k -2 random.txt > gzip-q02.gz
gzip -c -k -3 random.txt > gzip-q03.gz
gzip -c -k -4 random.txt > gzip-q04.gz
gzip -c -k -5 random.txt > gzip-q05.gz
gzip -c -k -6 random.txt > gzip-q06.gz
gzip -c -k -7 random.txt > gzip-q07.gz
gzip -c -k -8 random.txt > gzip-q08.gz
gzip -c -k -9 random.txt > gzip-q09.gz

# Compresses using Brotli.
brotli -k -q  0 -o brotli-q00.br random.txt
brotli -k -q  1 -o brotli-q01.br random.txt
brotli -k -q  2 -o brotli-q02.br random.txt
brotli -k -q  3 -o brotli-q03.br random.txt
brotli -k -q  4 -o brotli-q04.br random.txt
brotli -k -q  5 -o brotli-q05.br random.txt
brotli -k -q  6 -o brotli-q06.br random.txt
brotli -k -q  7 -o brotli-q07.br random.txt
brotli -k -q  8 -o brotli-q08.br random.txt
brotli -k -q  9 -o brotli-q09.br random.txt
brotli -k -q 10 -o brotli-q10.br random.txt
brotli -k -q 11 -o brotli-q11.br random.txt

# Lists sizes of all files.
ls -lnh .

195K random.txt
108K gzip-q01.gz
107K gzip-q02.gz
106K gzip-q03.gz
104K gzip-q04.gz
105K gzip-q05.gz
103K gzip-q06.gz
103K gzip-q07.gz
103K gzip-q08.gz
103K gzip-q09.gz
127K brotli-q00.br
106K brotli-q01.br
 98K brotli-q02.br
 99K brotli-q03.br
 99K brotli-q04.br
 98K brotli-q05.br
 98K brotli-q06.br
 98K brotli-q07.br
 98K brotli-q08.br
 98K brotli-q09.br
 87K brotli-q10.br
 87K brotli-q11.br

The uncompressed file is 195KB. The best Gzip compression outputs a 103KB file. The best Brotli compression outputs a 87KB file.

The browser send the br hint in the Accept-Encoding HTTP header if it can decode Brotli resources.

// Request
Accept-Encoding: gzip, deflate, br

// Response
Content-Encoding: br

Font Subsetting

To reduce the size of fonts, especially when you don't need all characters, you can subset a font by selecting just the ASCII characters range.

brew install fonttools

# Selects only the ASCII characters from the Inter Variable font.
pyftsubset packages/jobsort-site/public/fonts/inter-var-v319.woff2 --flavor=woff2 --output-file=packages/jobsort-site/public/fonts/inter-var-v319-ascii.woff2 --unicodes=0-ff

# Lists sizes of all files.
ls -lnh .

 49K inter-var-v319-ascii.woff2
317K inter-var-v319.woff2

Then use the unicode-range when defining the @font-face to inline the ASCII-only font using Webpack and then download the font with all the characters after page load. This avoids the flickering fonts as the page loads above the content.

HTTP/2 (not HTTP/1.1)

The browser will cap the number of concurrent requests to six per origin when using HTTP/1.1. That's why developers used to concatenate images in CSS sprites, to reduce the number of ongoing requests. This limitation does not apply for HTTP/2.

Intl API (not Luxon or Moment)

Luxon and Moment are popular date-time handling libraries, however they come with a huge cost in terms of kilobytes (in the hundreds) that you have to deliver to your users. There's a new Intl API, and specifically Intl.RelativeTimeFormat if you need to compute "x days ago". JobSort uses the Intl API to not depende on any date-time handling libraries and remove unnecessary bloat.

<link href="https://api.jobsort.com" rel="preconnect" />

Compared to the dns-prefetch relationship, preconnect does a DNS lookup, TCP handshake, and TLS negotiation.

<link
  as="fetch"
  crossorigin="anonymous"
  href="https://api.jobsort.com/db/results"
  rel="preload"
/>

The preload hint tells the browser to start preloading the resource with high-priority, before the actual fetch request is made. Make sure you cache the resource in the browser using the Cache-Control: max-age=14400 HTTP header so that when the actual fetch request is made, the contents is picked up from the cache.

<link
  as="fetch"
  crossorigin="anonymous"
  href="https://api.jobsort.com/search?q=software&offset=10"
  rel="prefetch"
/>

JobSort uses the prefetch relationship for infinite scroll. As you approach the bottom of the results page, JobSort tells the browser to start prefetching next page results with low-priority, so that by the time the user scrolls, the next page results are available and so, the scrolling experience is smooth.

Minify Images

SVGs

brew install svgo

wget https://upload.wikimedia.org/wikipedia/commons/a/a9/Amazon_logo.svg

svgo Amazon_logo.svg

Amazon_logo.svg:
Done in 18 ms!
5.4 KiB - 35.4% = 3.486 KiB

The svgo tool was able to minify the file by 35%; this is a material improvement. Try to minify all your SVGs using svgo.

Performance API

You can use the performance object should be preferred to subtracting Date objects because of its high precision. To cycle through all performance entries, if you're specifically interested in the markers, can be achieved easily with the following snippet. copy is a hook function available in the Chromium Developer Tools and copies the passed-in argument to the clipboard.

copy(performance.getEntries().map((element) => element.name));

Webpack Bundle Analyzer

If you're not already producing the bundle stats for your build, you should. Use the Webpack Bundle Analyzer to produce the interactive chart of all your dependencies and see which one eats up the most space. This is helpful to debug and see any surprizing and unexpected dependencies in the JavaScript bundles you end up delivering to the users over the network over and over again.

For full transparency, we make the bundle report available at /webpack.

Note that this is a static analysis tool that creates this report at build-time and doesn't necessarily mean that the page will download all these bundles.

Performance Tests

localStorage.setItem Test

const count = 1e5;
const t0 = performance.now();
for (let i = 0; i < count; ++i) {
  localStorage.setItem("random", Math.random());
}
console.log((performance.now() - t0) / count);

// 0.00073 ms

Turns out that localStorage.setItem is fairly fast and can be used to keep track of each keystroke on JobSort. JobSort saves each query you type in localStorage to show it again to you on the homepage when you visit the website again; it never leaves your computer.