React Storefront
|

Optimizing Performance

Measuring Performance Using Lighthouse

It is important to record performance measurements before and after making changes to the codebase.

One of the best ways to do this is by using Chrome's Lighthouse Audit Tool. It is important to note that running Lighthouse against local development will yield significantly lower performance scores than production due to the lack of caching in development.

Use incognito mode to prevent possible issues with Chrome extensions and other potential oddities while measuring.

Enable Caching for all Pages

The first and most important step to creating a fast site is ensuring that each page in the core shopping flow is cached:

  • Home
  • Category
  • Subcategory
  • Product

The routes for each of the above should have a cache handler like:

router.get(
  '/s/:id',
  cache({
    edge: { maxAgeSeconds: 60 * 60 * 24 }, // cache on the network edge for 24 hours
    client: true // cache on the client using the service worker
  }),
  fromClient({ page: 'Subcategory' }),
  fromServer('./subcategory/subcategory-handler')
)

You can verify that a page is cached if you see HIT in the x-cache response header:

x-cache: HIT, HIT

or

x-cache: MISS, HIT

The case above indiciates a cache miss in the L1 cache, but a hit in the L2 cache, which should be viewed as a hit overall.

If any page or request seems slow, see if the response was cached. If it wasn't, simply try again, it should be a cache hit if you've set up caching properly. If repeated requests result in a miss, caching isn't set up properly.

Proper caching setup is fundamental to delivering a fast site. The additional items below should only be followed once you have caching properly configured.

Delay Hydration Until Page Load

React hydration is a blocking event that tends to iterrupt largest image rendering. LIR time can be decreased by setting delayHydrationUntilPageLoad to true in launchClient. This delays hydration until the window's load event fires. For example:

// src/client.js

import App from './App'
import theme from './theme'
import model from './AppModel'
import router from './routes'
import launchClient from 'react-storefront/launchClient'
import errorReporter from './errorReporter'

launchClient({
  App,
  router,
  theme,
  model,
  errorReporter,
  delayHydrationUntilPageLoad: true // improves LIR time.
})

Image Optimization

An application's lighthouse score can often be significantly improved by optimizing how Images are loaded. On mobile devices, images should be downscaled to conserve bandwidth and served in webp format when supported by the browser. Moovweb's XDN and React Storefront make it easy to do this.

Image Component

To display individual images as React components, use React Storefront's Image React component. You can downscale the image using the quality prop, with takes a number from 1 to 100 corresponding to the percent the image should be downscaled. You can also use the lazy to delay loading the image until it is visible in the viewport. Here's an example:

import Image from 'react-storefront/Image'

export default Component({ image }) {
  return <Image src={image} quality={80} lazy />
}

Images within CMS content

React Storefront provides the following functions to help optimize images found in HTML stored in your CMS:

optimizeImages(options)

Transform images using Moovweb's CDN with the following options:

  • quality A number or string containing the number for the desired quality, on a scale from 1 (worst) to 100 (best).
  • width A number or string containing the number for the desired pixel width.
  • height A number or string containing the number for the desired pixel height.
  • format A string containing the desired file format. Options include jpg, png, and webp.
  • sourceAttributes An array of the attributes which contain the image source.
  • preventLayoutInstability Wrap the image in a container which fills the aspect ratio of the original image.

lazyLoadImages()

Delays the loading of each image until it is visible in the viewport. You must set CmsSlot's lazyLoadImages prop in order to use this feature.

Example Usage

import parse from 'react-storefront-extensions/html/parse'

export default somePageHandler(params, request) {
  const html = await fetchUpstreamHtml()
  const $ = parse(html)
  $('img').optimizeImages({ quality: 80 })
  $('img').lazyLoadImages()
  return {
    content: $('.content').html()
  }
}

Tip: There are situations where alternative attributes are used for image sources, such as data-src. You can specify those attributes in the options of optimizeImages using the sourceAttributes prop.

$('img').optimizeImages({ width: 500, sourceAttributes: ['src', 'data-src'] })

Lazy Loading Components

Components displayed below the fold that are resource intensive, such as third-party JavaScript widgets, should be loaded only when the user scrolls them into view. You can lazy-load any component by wrapping it in React Storefront's Lazy component:

It is important to set an explicit height for lazy components to prevent layout instability when the component is loaded. This can be done using style or className.

import Typography from '@material-ui/core/Typography'
import Lazy from 'react-storefront/Lazy'

export default function Product({ product }) {
  return (
    {/* Things above the fold... */}

    <Typography variant="h1">{product.name}</Typography>

    {/* Things below the fold... */}

    <Lazy key={product.url} style={{ height: 500 }}>
      <ExpensiveComponents/>
    </Lazy>
  )
}

Implement Skeletons and Ensure Layout Stability

One of the most important transitions that impacts perceived site speed occurs when the user navigates from a product listing (a.k.a. Subcategory) to a product page. Ideally, as much of the content above the fold on product pages should be rendered in the skeleton, so that the transition appears instant. This commonly includes breadcrumbs, product name, price, and main product image. Most importantly:

  • The main product image in the product skeleton should be the same URL as the thumbnail that the user clicked on in the product listing. This automatically happens if you use <ProductLink> in the product listing and ImageSwitcher from react-storefront/Skeleton in the product skeleton. The React Storefront starter app uses these by default.

  • The elements above the main product image in the product skeleton should always match the height of the equivalent elements on the product page so that the main product image doesn't move when the product page loads. This will ensure that the "largest image render" metric is as fast as possible.

Check AJAX response sizes.

When requesting data for a server side AJAX handler, make sure to reduce the size of the response as much as possible. The response size should be in the 10-30k range. Definitely not 100k+. Only respond with data the client will specifically need.

Use Preconnect

Make sure any domains which resources will be requested from use the preconnect hint. Preconnect allows the browser to setup early connections before an HTTP request is actually sent to the server.

<link href="https://cdn.domain.com" rel="preconnect" crossorigin />

Analytics Optimization

When using any analytics targets, it is important to use the delayUntilInteractive prop in the AnalyticsProvider:

// src/App.js

import AnalyticsProvider from 'react-storefront/AnalyticsProvider'

export default class App extends Component {
  render() {
    return (
      <AnalyticsProvider targets={analyticsTargets} delayUntilInteractive>
        {/* ... */}
      </AnalyticsProvider>
    )
  }
}

This allows the browser to focus on fetching the required resources first, then after the page becomes interactive, the targets are loaded.

Debugging Slow Page Transitions

Here are some things you can check when a particular page transition feels slow:

  • Was the json data fetch a cache hit at the edge? Look at the X-Moov-T response header in the Chrome Developer Tools Network tab. If the response came from the cache you will see vc=hit or oc=hit in the value. Otherwise, it was a cache miss. Sometimes the first few requests to a page will result in one or more cache misses. After a few requests you should consistently get cache hits. If you're not, the route likely isn't cached. Check your router definition (usually in routes.js) and ensure that the route in question has a cache handler with an edge config and a maxAgeSeconds property. for example:
// src/routes.js

new Router().get(
  '/p/:id',
  cache({
    edge: {
      maxAgeSeconds: 60 * 60 * 24 // 24 hours
    }
  })
)
  • Is the json being cached on the client by the service worker? Look in Application -> Cache Storage -> runtime-json-*. You should see the json fetch response listed there. If not, check that the cache handler for the route has client: true:
new Router().get(
  '/p/:id',
  cache({
    edge: {
      maxAgeSeconds: 60 * 60 * 24 // 24 hours
    },
    client: true // <~ this makes the service worker cache the result on client
  })
)
  • Is there anything excessive going on during rendering? Use the Performance tab in Chrome developer tools to analyze the rendering process:

    • Click the record button
    • Click the link to start the transition
    • After the transition is complete, stop the recording and look at the timings chart. Are there any operations that take more than 100ms? If so, there's likely some room for improvement. Make sure only those components that are expected to change are rerendering. Ensure that each react component accesses only what it needs from the models in the app state tree to minimize rendering. Don't pass model data down the component tree. Use the @inject decorator instead. Here's an example of a good timings chart:

Google Dev Tools Performance Tab