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 in.miko.ai! by caching their home page (/
) and product display page (/store/miko3
). I aimed for at least 50% boost in the performance.
Currently, https://in.miko.ai 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, in.miko.ai). 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.
— shoppingFlowRouteHandler.ts
: Using this to abstract the process in a single template to fetch the upstream response from the origin/backend server
— 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, in.miko.ai as my origin. The idea is to proxy everything including assets and pages using the backend.
// File: improve-in.miko.ai/edgio.config.js
module.exports = {
routes: './src/routes.ts',
connector: './node_modules',
backends: {
// Proxying origin
origin: {
domainOrIp: 'in.miko.ai',
hostHeader: 'in.miko.ai',
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-in.miko.ai/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:\/\/miko\.ai\//gi, '/')
updateResponseHeader('location', /https:\/\/in\.miko\.ai\//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-in.miko.ai/src/routes.ts
import { Router } from '@edgio/core/router'
import shoppingFlowRouteHandler from './shoppingFlowRouteHandler'
export default new Router()
.match('/', shoppingFlowRouteHandler)
// cache assets for long period
.match('/miko-logo.svg', ({ cache, proxy }) => {
cache({
edge: {
maxAgeSeconds: 60 * 60 * 24 * 365,
},
})
proxy('origin')
})
.match('/homepage/miko-robo-mobile.webp', ({ cache, proxy }) => {
cache({
edge: {
maxAgeSeconds: 60 * 60 * 24 * 365,
},
})
proxy('origin')
})
.match('/_nuxt/:path*', ({ cache, proxy }) => {
cache({
edge: {
maxAgeSeconds: 60 * 60 * 24 * 365,
},
})
proxy('origin')
})
.match('/fonts/:path*', ({ cache, proxy }) => {
cache({
edge: {
maxAgeSeconds: 60 * 60 * 24 * 365,
},
})
proxy('origin')
})
// cache pages with stale-while-revalidate on the edge!
.match('/:suffix', shoppingFlowRouteHandler)
.match('/store/:suffix', shoppingFlowRouteHandler)
.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-in.miko.ai/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) {
const $ = cheerio.load(response.body)
response.body = $.html()
.replace(/\=\"\/\//g, '="https://')
.replace(/https?:\/\/in\.miko\.ai\//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 67% improvement on first page loads!
Home First Load, Improvement ~67%
Home First Load, LCP: in.miko.ai: 2.064s, Optimised with Edgio: 0.684s, Improvement: ~67%)
PDP First Load, Improvement ~39%
(PDP First Load, LCP: in.miko.ai: 1.976s, Optimised with Edgio: 1.143s, Improvement: ~39%)
Discussion
My implementation focuses on caching the page with stale-while-revaliate (but on the edge!), which effectively the improved LCP under 5 minutes. I do that with Edgio Applications’s Caching of more than just assets. No front-end optimisation required (for Miko!). Definitely, this approach is not set in stone, but seems like in.miko.ai can deliver a way better online shopping experience by utilising caching.