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.
The first and most important step to creating a fast site is ensuring that each page in the core shopping flow is cached:
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.
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.
})
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.
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 />
}
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.
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'] })
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>
)
}
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.
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.
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 />
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.
Here are some things you can check when a particular page transition feels slow:
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
}
})
)
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:
@inject
decorator instead. Here's an example of a good timings chart: