
My top content in 2024
My most popular content in 2024 was all about the web - from performance to 11ty, hosting, and the Indie Web
Building a site that shows real-user web performance data by Shopify theme using HTTP Archive, the Chrome User Experience Report, BigQuery, Node, and 11ty
For two years at Shopify, I helped merchants and theme developers make their online stores and themes faster. I also applied learnings from those engagements to improve the Shopify platform.
I left in the summer of 2024, but I felt I had unfinished business. I wanted merchants and theme developers to have a better resource for understanding the real-world performance of Shopify themes. And, I knew publicly-available data for this existed in HTTP Archive and the Chrome User Experience Report, or CrUX for short.
Thus began my journey to query and present this data. In this blog post, I will cover how I built Theme Vitals, a site that shows real-user Core Web Vitals performance data by Shopify theme.
Contents:
If you prefer watching videos over reading text, I presented similar information at the 11ty Meetup:
I created a diagram in Figma to better outline how it all works together (open in full screen):
Shopify themes are pre-built design templates, or bundles of HTML, CSS, JavaScript, and Shopify’s own templating language, Liquid. Merchants can buy themes in the Shopify theme store, then customize and publish them as their online stores.
Themes contain the core frontend code (though they can also impact backend performance through how Liquid is used). Merchants can also install Shopify apps which are essentially third-party scripts and widgets. Both themes and apps can heavily impact web performance, including the Core Web Vitals.
Typically, how a theme is written sets your maximum achievable performance. Adding apps can further degrade your performance, especially Interaction to Next Paint (INP).
Site performance is important for both search engine rankings and user retention. While at Shopify, time and again I saw how online store bounce rates increased and conversion rates decreased for users with slow performance. Themes would usually be the biggest culprit followed by apps and tags (e.g., Google Tag Manager).
Unfortunately, the Shopify theme store does not yet contain data about web performance so both merchants and theme developers are left in the dark.
My goal was to provide Shopify merchants with insights into the real-world performance of these themes, enabling them to make informed decisions about which theme to choose based on their performance in the wild. I also wanted to provide theme developers with a way to identify their biggest performance bottlenecks so that they could be more effective at optimizing their themes.
Breaking down the problem into smaller steps, I needed to:
HTTP Archive and the Chrome User Experience Report (CrUX) are my data sources. Both have the ability to run custom queries using BigQuery.
HTTP Archive allowed me to achieve step 1. HTTP Archive runs a massive job once per month on the websites with the most traffic. This is currently the top ~16M URLs on mobile and ~13M on desktop. This job runs lab-based tests (a private instance of WebPageTest) on each website to collect detailed data including various performance metrics.
Recently, HTTP Archive added a custom metric that saves the global JavaScript Shopify
object when present. This object is present on all Shopify Liquid sites and contains information about the theme. If you open the Dev Tools console on a Shopify site and enter Shopify.theme
, here's an example output:
{
"name": "Loop Offset in Cart-UI",
"id": 128596803639,
"schema_name": "Palo Alto",
"schema_version": "6.0.1",
"theme_store_id": 777,
"role": "main",
"handle": "null",
"style": {
"id": null,
"handle": null
},
}
The name
is a custom name that the merchant can change. They often have multiple copies of the same underlying theme which they will switch out for promotions or seasonal changes. The most important property here is the theme_store_id
which tells us the theme came from the Shopify theme store and its ID. schema_name
is a recent addition which tells us the name of the theme.
Once I had the list of URLs and what theme they use, I was able to proceed to step 2...
CrUX provides real-world web performance data based on actual users visiting websites in Chrome. Unlike synthetic tools such as Lighthouse, CrUX data is based on real users and reflects how actual visitors experience websites. This is the same data used in:
Each month, CrUX publishes aggregated monthly data to their database on BigQuery.
To get the real-user performance of Shopify sites, I used SQL to join the URLs from HTTP Archive to the CrUX data set URLs in BigQuery. Then, I aggregated the data by theme within the same query. The full query is a bit gnarly, but you can see it here if you're curious: query.sql.
I exported the result of this query as a JSON file to use in my next step...
The output of the query gives us data for one month. However, Theme Vitals provides detailed data for the previous 6 months. Running queries in BigQuery can get quite expensive, so attempting to do a mega-query for the last 5 months would get prohibitively expensive as well as overly complex.
Thus, I wrote a Node.js script to process each month's data and munge it into one data file with both the aggregation calculations as well as the time series data for each theme.
For the visualizations, I wanted a charting solution that would ensure that the Theme Vitals loading speed and interactions stayed fast. This site is statically generated, so I wanted a static or server-side rendered option. Pre-rendered SVG charts were the perfect choice.
I used Apache eCharts in my Node script to generate SVG strings for each chart which are saved in the same data file by theme. Here's how a basic SVG string is generated:
// Server-side code
const echarts = require('echarts');
// In SSR mode the first container parameter is not required
let chart = echarts.init(null, null, {
renderer: 'svg', // must use SVG rendering mode
ssr: true, // enable SSR
width: 400, // need to specify height and width
height: 300
});
// use setOption as normal
chart.setOption({
//...
});
// Output a string
const svgStr = chart.renderToSVGString();
// If chart is no longer useful, consider disposing it to release memory.
chart.dispose();
chart = null;
I have some ideas for new features that would require more dynamic rendering on the client side, and eCharts still has that capability.
To generate the site, I used 11ty (a.k.a., Eleventy), my favorite static site generator. It's Node-based so I don't have to context-switch between JavaScript and other languages (e.g., Ruby). The concepts for how I structured the site may be applicable to other static site generators as well:
Global Data: I import the munged data from the previous step into global data objects in 11ty. I don't process and munge the data directly in 11ty because that would make my build time quite slow, and the data is only updated once per month. I create two global data objects:
themes
: all of the themes and their data and chartsmetadata
: metadata such as the date of the last run and aggregations across all themesAggregations Page: This page shows how all themes perform as a group, providing minimum, median, and maximum percent of themes passing each Core Web Vital metric. The template renders from metadata
directly like so:
{% for metric in metaData.aggregations.mobile %}
<tr>
<td>{{ metric.name }}</td>
<td>{{ metric.min }}%</td>
<td>{{ metric.median }}%</td>
<td>{{ metric.max }}%</td>
</tr>
{% endfor %}
themes
directly like so:<tbody id="tbody-mobile">
{% set sortedThemes = themes | sortThemes %}
{% for theme in sortedThemes %}
<tr
data-market-rank="{{ theme.summary.mobile.marketRank }}"
data-cwv-rank="{{ theme.summary.mobile.cwvRank }}"
data-alpha-rank="{{ loop.index }}">
<td><a href="/themes/{{ theme.slug }}/">{{ theme.name }}</a> {% if theme.sunset %}<span class="pill">vintage</span>{% endif %}</td>
<td>{{ theme.summary.mobile.marketRank }} <small>({{ theme.summary.mobile.marketSharePct }}%)</small></td>
<td>
themes
---
layout: base.njk
pagination:
data: themes
size: 1
alias: theme
permalink: "/themes/{{ theme.slug }}/"
eleventyComputed:
title: "{{ theme.name }} Performance Data | Theme Vitals"
description: "Explore real-user web performance for the Shopify theme {{ theme.name }} including the Core Web Vitals and other metrics, split by desktop and mobile sessions."
---
<section class="banner">
<div class="content flow">
<h1>{{ theme.name }}</h1>
There were several reasons I chose to use 11ty for this project:
Back to business... The monthly workflow is straightforward but involves a few manual steps each month:
main
branch is updated in Github, Github sends a webhook to my hosting provider which triggers a rebuild in production.This project has been very rewarding for me. I love building tools that help others. I'm hoping it will help Shopify merchants in their decision-making process. I'm also hoping it will pressure theme developers (just a light, friendly pressure) to make their themes more performant as well as give them the data they need to do that more effectively.
I'm not stopping here. I'm considering several new features such as:
Which one of those would be your preference? Let me know!
I make websites faster, smarter, and easier to grow.
If you want someone who’s creative and precise, deeply reliable, and not afraid to tell you what’s what to get you to the next level—I’m your partner.
My most popular content in 2024 was all about the web - from performance to 11ty, hosting, and the Indie Web
Use lite-youtube-embed in Eleventy for faster and more privacy-minded video
Set up responsive images in Eleventy using Cloudinary and Eleventy shortcodes
If you liked this article and think others should read it, please share it.
These are webmentions via the IndieWeb and webmention.io. Mention this post from your site: