With Edgio Applications, nearly every website can boost their front-end performance. Easiest way to ace Largest Contentful Paint (part of Core Web Vitals)? Combine your frontend optimisations with the powerful caching offered by Edgio.
Introduction
A good user experience starts with delivering content as fast as possible. With the backbone of Edgio Applications CDN, Edgio Applications promises fast content delivery. In this study, I set out to improve www.nginx.com! by caching their home page (/) and product listing page (/solutions/). I aimed for at least 50% boost in the performance.
Currently, https://www.nginx.com is failing Core Web Vitals badly.
Disclaimer
I’m a Technical Customer Success Manager at Edgio Applications, but this is purely my ideation & work.
Leveraging Edgio Applications
The starting point to ship speed with Edgio Applications is at their WebApp CDN guide. Overall, Edgio Applications will become the main CDN for the site/origin server (here, www.nginx.com). Once a response is received from the origin, it gets cached over Edgio Applications's globally distributed edge network. In browser, then those cached pages, assets, api’s, etc. can be prefetched.
Show me the code!
I started with installing the Edgio CLI, by the following command:
# Using NPM
npm i -g @edgio/cli
# Using Yarn
yarn global add @edgio/cli
Creating a new project with Edgio Applications, is then just a matter of command:
edgio init
Irrespective of the initial configuration by the CLI, I alter my project structure to look like below.
Project Structure
— edgio.config.js
: Controls how your apps run on Edgio Applications. Also, here we define the origin/backend server.
— src/routes.ts
: Using this file to define what shall be cached, and for how long.
— src/shoppingFlowRouteHandler.ts
: Using this to abstract the process in a single template to fetch the upstream response from the origin/backend server
— src/transform.ts
: Once the response is proxied, exploiting transformResponse function by Edgio Applications to inject Edgio specific JS to leverage prefetching, and make some front-end optimisations.
Now let’s do walk over on each of the files mentioned above.
edgio.config.js
In this file, I’ve defined one backend, www.nginx.com as my origin. The idea is to proxy everything including assets and pages using the backend.
// File: improve-www.nginx.com/edgio.config.js
module.exports = {
routes: './src/routes.ts',
connector: './node_modules',
backends: {
origin: {
domainOrIp: 'www.nginx.com',
hostHeader: 'www.nginx.com',
disableCheckCert: true,
},
},
}
shoppingFlowRouteHandler.ts
So assume that if a user comes looking for the homepage, then according to the configuration in routes.ts
, it calls the RouteHandler defined in this file, and then fetches the upstream on the same route. Then, the response headers of set-cookie
& content-security-policy
header are removed. Finally, the response is transformed as per the transformResponse function as defined in transform.ts
.
// File: improve-www.nginx.com/src/shoppingFlowRouteHandler.ts
import transformResponse from './transform'
import { RouteHandler } from '@edgio/core/router/Router'
const handler: RouteHandler = async ({ cache, removeUpstreamResponseHeader, updateResponseHeader, proxy }) => {
cache({
edge: {
maxAgeSeconds: 60,
staleWhileRevalidateSeconds: 60 * 60 * 24 * 365,
},
})
removeUpstreamResponseHeader('set-cookie')
removeUpstreamResponseHeader('cache-control')
updateResponseHeader('location', /https:\/\/nginx\.com\//gi, '/')
updateResponseHeader('location', /https:\/\/www\.nginx\.com\//gi, '/')
proxy('origin', { transformResponse })
}
export default handler
routes.ts
I define all the possible paths to be cached (with the help of configurations defined in cache.ts
), rest sent to the origin (the fallback).
An example of defining a route
Assume that on a website, an asset that has a relative url /l0-prodstatic/images/image/1.png
is being fetched. The route below will consider the variable :path*
to be images/image/1.png
. Then Edgio Applications would fetch the :path*
relative to the origin server as defined in the assets
keys of backends in edgio.config.js
. Once done, Edgio Applications would remove the set-cookie
header, update the cache-contol
response header and apply the cache timings.
.match('/l0-prodstatic/:path*', ({ cache, removeUpstreamResponseHeader, proxy, setResponseHeader }) => {
setResponseHeader('cache-control', 'public, max-age=86400')
removeUpstreamResponseHeader('set-cookie')
cache(CACHE_ASSETS)
proxy('origin', { path: ':path*' })
})
The whole file
// File: improve-www.nginx.com/src/routes.ts
import { Router } from '@edgio/core/router'
import shoppingFlowRouteHandler from './shoppingFlowRouteHandler'
export default new Router()
.match('/', shoppingFlowRouteHandler)
.match('/:suffix', shoppingFlowRouteHandler)
.match('/wp-includes/:path*', ({ cache, proxy, removeUpstreamResponseHeader }) => {
removeUpstreamResponseHeader('set-cookie')
cache({
edge: {
maxAgeSeconds: 60 * 60 * 24 * 365,
},
})
proxy('origin')
})
.match('/wp-content/:path*', ({ cache, proxy, removeUpstreamResponseHeader }) => {
removeUpstreamResponseHeader('set-cookie')
cache({
edge: {
maxAgeSeconds: 60 * 60 * 24 * 365,
},
})
proxy('origin')
})
.match(
'/:path*/:file.:ext(js|mjs|css|png|ico|svg|jpg|jpeg|gif|ttf|woff|otf)',
({ cache, proxy, removeUpstreamResponseHeader, setResponseHeader }) => {
setResponseHeader('cache-control', 'public, max-age=86400')
removeUpstreamResponseHeader('set-cookie')
cache({
edge: {
maxAgeSeconds: 60 * 60 * 24 * 365,
},
})
proxy('origin')
}
)
.fallback(({ proxy }) => {
proxy('origin')
})
transform.ts
The transformResponse function called by the shoppingFlowRouteHandler.ts
is used to modify the HTML response before sending it to the users. If there’s a response body, I assumed it had valid HTML, and then parsed it into a cheerio object. Then comes the part of front-end optimisation on the fly. Think of the approach in the implementation as serving pages optimised for performance, and then hydrating the page as required with JS.
// File: improve-www.nginx.com/src/transform.ts
import * as cheerio from 'cheerio'
import Request from '@edgio/core/router/Request'
import Response from '@edgio/core/router/Response'
export default function transformResponse(response: Response, request: Request) {
if (response.body) {
// Load the HTML into cheerio
const $ = cheerio.load(response.body)
// Only optimise for / and /ltl/
if (request.path == '/' || request.path.includes('/solutions')) {
// Remove dns-prefetch(es) on the page
$('[rel="dns-prefetch"]').remove()
let bodyScripts = $('body script')
// As the page's style and look
// isn't affected by JavaScript
// Move the scripts away from head to the
// end of the body
$('head script').each((_, ele) => {
let temp = $(ele)
$(ele).remove()
$('body').append(temp)
})
// As we moved away the head scripts
// We'd also need to move the body scripts
// after them so that they can load the
// required libraries first
bodyScripts.each((_, ele) => {
let temp = $(ele)
$(ele).remove()
$('body').append(temp)
})
// Load google fonts without them
// blocking the rendering completion
$('head link[href*="googleapis"]').each((i, el) => {
let temp = $(el)
temp.attr('media', 'print')
temp.attr('onload', "this.media='all'")
})
// As the page's style isn't affected
// by their font awesome css,
// defer the style's load
$('head link[href*="font-awesome.min.css"]').each((i, el) => {
let temp = $(el)
temp.attr('media', 'print')
temp.attr('onload', "this.media='all'")
})
}
response.body = $.html()
// Replace =" with ="https://
.replace(/\=\"\/\//g, '="https://')
// Replace all https://www.nginx.com with /
.replace(/https?:\/\/www\.nginx\.com\//g, '/')
// Replace all https://nginx.com with /
.replace(/https?:\/\/nginx\.com\//g, '/')
}
}
We’re done! Let’s deploy fearlessly now.
Emulating the production experience, locally
With Edgio Applications’s CLI, it is possible to emulate edge rules locally as if the code went live. Here’s how I did it:
edgio build && edgio run --production
Deploy
Deploying from CLI can be done as mentioned in Edgio Applications Docs
edgio deploy
Results
If you’ve come this far (awesome!) you probably want to know the result. Tbh, I was surprised with the results too. Upto 56% improvement on first page loads & 80% improvement on navigation!
A quick look at TTFB of both the sites around the world
SpeedVitals of rishi-raj-jain-nginx-default.layer0-limelight.link
Home First Load, Improvement ~50%
Home First Load, LCP: www.nginx.com: 4.203s, Optimised with Edgio: 2.107s, Improvement: ~50%)
PLP First Load, Improvement ~50%
(PLP First Load, LCP: www.nginx.com: 4.338s, Optimised with Edgio: 2.170s, Improvement: ~50%)
Discussion
My implementation aggressively focuses on optimising what I set out for testing: First Page Loads & LCP. I do that with Edgio Applications’s Caching of more than just assets and browser prefetching. I also did some optimisation in the frontend to prioritise the assets and page load. Definitely, this approach is not set in stone, but seems like www.nginx.com can deliver a way better online shopping experience by utilising caching and prefetching.