Sample: a Shopify speed report
John Singh ·
This is a sample, not a client’s report. It covers the “before” page of my own speed demo: a mock product page for a made-up desk brand, which I built with six common problems. This sample covers the demo’s product page only.
It’s laid out as a full report: what was measured, the headline numbers, each cause ranked by how much time it adds with the check that will show it’s fixed, and what’s already fine. Every number can be checked: both of Google’s reports are public, and the demo’s before and after pages are live for anyone to measure again.
What was measured, and how
- Page
- The demo’s product page, before.html, on a phone. Google’s reports were run at 2:49 a.m. Eastern time on 22 September 2026. Minutes later I added one line of CSS to the before and after pages, which sets the two “In the room” photos in a 3:2 frame, and neither page has changed since. It doesn’t change what either page downloads, but it’s why Google’s report shows the second of those photos at 364 × 364, where the page today shows it at 364 × 243.
- Real shoppers
- Google has no data from real visitors for this page (its report says “No Data”), so everything here comes from lab tests. When Google has that data for your store, the report starts there.
- Google’s test
- PageSpeed Insights, mobile, run on 22 September 2026: Lighthouse 13.5.0, emulating a Moto G Power on a slow 4G connection. The public report.
- My lab runs
- 5 October 2026, on my Mac, with Playwright driving Chromium 148: a fresh browser with an empty cache for every run, a 412 × 823 phone screen at 1.75× pixel density, and the settings Lighthouse uses when it slows a real browser to slow 4G, applied through the Chrome DevTools Protocol: 562.5 ms added to every request, 1,474.56 kbit/s down, 675 kbit/s up, and the processor slowed 4×. Three runs of each page; every lab figure is the median of the three. To count one jump that happens on some loads and not others (cause 3), I loaded the live before page 18 more times, 13 of them without the phone emulation.
- Read from
- The browser’s own timings (first paint, largest paint, layout shifts and long tasks), and its network log for page weight and for the requests that hold up the first paint. Sizes in the tables and evidence are in KiB, as Google’s report shows them (1 KiB is 1,024 bytes).
- Time per cause
- Six copies of the page, each with only one cause fixed, the way the after page fixes it, measured the same way alongside the unchanged page, both served from my Mac. The difference between them is the time that cause adds. Nothing on the live page was changed. On your store I have no access, so the test browser rewrites the page as it arrives instead, one cause at a time, while everything else loads from your store as normal. Tried that way on the live demo page, it gave the same order, with every time within 0.05 s of the ones below.
The headline numbers
In plain English: text appears after two to three seconds, but the main product photo takes about 25 seconds to finish drawing. The photos come to more than 4 MB, all downloading at once over the same slow connection, and the main photo, the biggest at 2.3 MB, finishes last. Fixing that one photo accounts for most of the difference.
| Measure | My lab | |
|---|---|---|
| Main photo on screenLargest Contentful Paint · good is 2.5 s or less | 24.3 s | 25.6 s |
| First text on screenFirst Contentful Paint · good is 1.8 s or less | 2.8 s | 2.1 s |
| How much the page jumpsCumulative Layout Shift · good is 0.1 or less | 0.007 | 0.008 |
| Time frozen after first paintTotal Blocking Time · good is 200 ms or less | 0 ms | 0 ms |
| How fast the screen fillsSpeed Index · good is 3.4 s or less | 4.5 s | — |
| Page weighteverything downloaded · 13 requests in my runs | 4,487 KiB | 4,484 KiB |
| Performance scoreout of 100 · good is 90 or more | 68 | — |
Google: 22 September 2026. My lab: 5 October 2026, median of three runs. Two sets of three runs that morning differed by up to 0.07 s, so a re-run can round 0.1 s either way. Google estimates its timings by simulating the slow phone and connection from one fast load; my runs slow the browser down for real, so the two don’t match to the decimal, but they agree on what’s slow. My runs don’t compute Google’s score or Speed Index. Neither layout-shift figure includes a jump that happens on about half of loads, which cause 3 explains: on a load where it’s counted, the score is 0.31, in Google’s poor range.
Causes, ranked by time added
Each time is the median for the unchanged page minus the median for a copy with only that cause fixed. The causes overlap, mostly because they share one slow connection, so their times don’t add up to the total: fixing all six together is what the after page shows. The copies were served from my Mac, so the unchanged page’s figures here (25.5 s and 2.14 s) differ a little from the live page’s above.
-
1The main photo is a full-size 2.3 MB JPEG
+23.2 s
Main photo on screen: 25.5 s for the unchanged page, 2.3 s with only this fixed.
- What it is
- The product’s main photo is a 2400 × 2400 px JPEG, shown 364 px wide on a phone. It downloads at the same time as every other photo on the page, with nothing marking it as the one that matters, so on a slow connection it’s the last to finish. The thumbnails under it use the same full-size files.
- Evidence
- Google’s “Improve image delivery” puts it at 2,263.7 KiB, with 2,242.1 KiB to save, and “LCP request discovery” flags that it isn’t marked high priority. In my runs it started downloading at 0.6 s and finished at 25.6 s. Until then it draws in from the top, a strip at a time; the moment it’s complete is what Google’s measure counts.
- The written check
- The main photo downloads at under 100 KiB, and Google’s report no longer flags “LCP request discovery”.
- Effort
- Small: the product template’s image tags, using Shopify’s
image_urlwith a set of widths,preload: trueand high priority on the first photo, and thumbnail-sized files for the thumbnails.
-
2Photos further down download at full size, straight away
+5.2 s
Main photo on screen: 25.5 s for the unchanged page, 20.3 s with only this fixed.
- What it is
- Two photos below the first screen, of 1,067 KiB and 1,016 KiB, download the moment the page opens and compete with the main photo for the same connection. Neither has width and height set in the HTML (on the page today, its CSS holds their 3:2 frame).
- Evidence
- Google’s “Improve image delivery” lists both: a 2000 × 1333 photo shown at 364 × 243, with 1,053 KiB to save, and a 1600 × 1600 photo shown at 364 × 364, with 994 KiB. “Image elements do not have explicit width and height” lists them too. In my runs both started downloading at 0.6 s. The second is also the gallery’s second slide, so it still downloads in the copy with only this fixed; the time here comes from the first alone.
- The written check
- Every photo below the first screen has
loading="lazy"and its width and height set, and downloads at under 100 KiB. - Effort
- Small: the same image tags, lazy-loaded below the first screen. On a page this short, the browser may still fetch them early, because lazy loading starts some way before a photo scrolls into view, so most of the saving here is their size.
-
3jQuery and a carousel plugin hold up the first paint
+0.54 s
First text on screen: 2.14 s for the unchanged page, 1.60 s with only this fixed. On about half of loads, it also makes the page jump.
- What it is
- Four files from another server (jQuery, the Slick carousel and its two stylesheets) load in the page’s head, so the browser can’t show anything until all four have arrived. The gallery then stays hidden until the carousel starts.
- Evidence
- Google’s “Render-blocking requests” lists all four (40.7 KiB), with jQuery the slowest at 1,350 ms. In my runs jQuery took 1.12 s to arrive and the carousel script 0.83 s. On 11 of my 21 loads of the live page, the text and the Add to cart button showed first, and less than a tenth of a second later the gallery appeared above them and pushed them 364 px down; on the rest, the carousel started before the first paint and nothing moved. With the phone emulation on, Chrome marks that jump as following an input, so it isn’t counted in my figures; without the emulation it is, and that load’s layout-shift score is 0.31, which Google rates poor. Google’s report doesn’t show it.
- The written check
- Google’s report no longer flags “Render-blocking requests”, and the page makes no request for jQuery or the carousel.
- Effort
- Medium: the gallery rebuilt as a strip of photos that swipes with CSS alone (scroll snap), after checking what else in the theme relies on jQuery.
-
4An app’s script runs before the page can show
+0.38 s
First text on screen: 2.14 s for the unchanged page, 1.76 s with only this fixed.
- What it is
- A script in the page’s head, standing in for a typical review or upsell app, does 350 ms of start-up work before the browser may show anything.
- Evidence
- Every one of my runs shows a task of about 350 ms just before the first paint; in the copy with only this fixed, it’s gone. Google flags one long task, of 424 ms, and puts it down to the carousel’s script file, which loads just before the app’s. Google’s Total Blocking Time stays at 0 ms because it only counts what happens after the first paint.
- The written check
- Google’s report no longer flags “Avoid long main-thread tasks”, and the app’s code starts only after the page has loaded.
- Effort
- Small here: the script sits in the page itself, the way code pasted into a theme does, so it can be made to wait for the page. On a store where the delay comes from an app’s own embed that only its maker can change, the report lists it under “Yours to decide” instead. Whether to keep the app at all is yours to decide.
-
5Web fonts hold up the first paint
under 0.1 s
First text on screen: 2.14 s either way, because jQuery still holds the page up for longer.
- What it is
- The font stylesheet asks for two families in fifteen weights, far more than the page uses, with no early connection to the font server and no instruction to show text before the fonts arrive. It’s loaded in a way that stops the page showing until it’s back.
- Evidence
- Google’s “Render-blocking requests” lists the font stylesheet (1.9 KiB, 780 ms), and “Font display” lists both font files (10 ms each). In my runs the stylesheet took 0.68 s, less than jQuery’s 1.12 s, so fixing it alone moved the first paint by under 0.1 s. With jQuery gone, it’s the slowest thing left in the way, which is why it’s fixed with the rest. Fixed alone, it also lets text show first in a fallback font, which shifts the text very slightly when the web fonts arrive, well inside the 0.1 mark.
- The written check
- Google’s report lists no fonts.googleapis.com request under “Render-blocking requests”, and no Google font under “Font display”.
- Effort
- Small: one change in the theme’s layout file.
-
6A reviews widget pushes the Add to cart button down
0 s
Adds no load time. About 3.4 s in, it moves the Add to cart button 101 px down.
- What it is
- The reviews summary is added by a script 1.2 s after the page’s HTML is ready, above the Add to cart button, so the button moves just as a shopper may be reaching for it.
- Evidence
- Google’s “Layout shift culprits” names the Add to cart button (0.007), the whole of the page’s layout-shift score in its test. In my runs the button moved 101 px down at 3.4 s; with only this fixed, the score fell from 0.008 to 0.001.
- The written check
- The reviews summary is on the page from the first paint, and Google’s “Layout shift culprits” no longer names the Add to cart button.
- Effort
- Small: the reviews summary rendered with the page, or its space kept with CSS.
Yours to decide
Two things in the fixes change more than speed, so they’re your call rather than mine:
- The app. Making it wait removes its delay before the first paint, but its 350 ms of work still runs on every page view, a little later. If you don’t use what it does, removing it saves more, and costs nothing. I tell you what each app costs; you decide which stay.
- The carousel. Without the plugin, the gallery becomes a strip shoppers swipe through, with no arrow buttons. If you want arrows, a few lines of code add them back without the library.
What’s already fine
- The server. The page’s HTML arrives compressed (about 3 KiB) over HTTP/2, and Google’s “Network dependency tree” shows it back in 69 ms.
- Taps once it’s showing. Nothing keeps the phone busy after the first paint: Total Blocking Time is 0 ms in Google’s test and in every one of my runs.
- The product details are real text. The title, price, description and specifications are in the page itself, so they show with the first paint rather than waiting for a script or a photo.
- No redirects. The page answers at its own address the first time.
- Accessibility and best practices. Both score 100 in Google’s report.
Two things Google flags aren’t causes, so they’re left out of the ranking. “Use efficient cache lifetimes” is set by the demo’s host, GitHub Pages, not by the page; on a Shopify store, Shopify’s servers set it. And the SEO score of 54 is because the demo pages are kept out of search on purpose and have no description; that isn’t a speed matter.
About this sample
This is my own static demonstration, run on pages I built; it isn’t a client’s report or an offer. For any actual job, the scope, price, timing and acceptance criteria are agreed separately on the freelance platform we use.
What the after page achieved
The after page is the same page with all six causes fixed: same layout, same copy, same photos, and the same 350 ms of app work. Before report · After report.
| Measure | Before | After |
|---|---|---|
| Performance scoreGoogle | 68 | 96 |
| Main photo on screenGoogle | 24.3 s | 2.7 s |
| Main photo on screenMy lab | 25.6 s | 2.1 s |
| First text on screenGoogle | 2.8 s | 1.5 s |
| First text on screenMy lab | 2.1 s | 0.6 s |
| How much the page jumpsGoogle | 0.007 | 0.001 |
| How much the page jumpsMy lab, without the jump in cause 3 | 0.008 | 0.001 |
| Page weightMy lab · 13 requests before, 9 after | 4,484 KiB | 237 KiB |
Google: both reports run on 22 September 2026. My lab: 5 October 2026, median of three runs of each live page.
And the written checks, each read from the after report or the after page:
- Main photo: 53.2 KiB in Google’s after report (a 900 px AVIF), and “LCP request discovery” isn’t flagged.
- Photos further down: both have
loading="lazy"and their width and height set, at 45.6 KiB and 55.6 KiB. - jQuery and the carousel: “Render-blocking requests” isn’t flagged, and none of the page’s 9 requests is for jQuery or the carousel.
- The app’s script: “Avoid long main-thread tasks” isn’t flagged. The same 350 ms of work starts after the page has loaded, in 8 ms slices, and none of it shows up as a long task in my runs.
- Web fonts: no font request under “Render-blocking requests” or “Font display”.
- Reviews: on the page from the first paint, and the Add to cart button isn’t among Google’s layout shift culprits. It still moves 8 px as the web fonts arrive; Google scores the heading’s move at 0.001.
Two honest notes. Google’s figure for the main photo on the after page is 2.7 s, just over its 2.5 s mark for good, while my lab median is 2.1 s. And Google still suggests 81 KiB of image savings, because each photo is served one size up from its space: the next size down would be smaller than the space on that phone, and look soft. Neither is a cause left unfixed; it’s why each cause has a written check, not a score.