Monday, December 26 2022

Case Study: How Miko.AI can leverage Edgio to improve their First Page Loads upto ~67%, acing Largest Contentful Paint.

Rishi Raj Jain
Rishi Raj Jain @rishi_raj_jain_

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.

Core Web Vitals of in.miko.ai

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.

Network Diagram

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.

Code

https://github.com/rishi-raj-jain/improve-in.miko.ai

Write a comment

Email will remain confidential.