51
Performance score
Lab — concept build — anchor: muji-kenya-hara
Semantic HTML and near-zero JavaScript on an image-heavy portfolio, where the photograph is the payload and everything else is overhead.
Not client work. jameskushphotography.com is our own site, and this is a concept rebuild of it — measured, argued, and deliberately unfinished. Both sets of numbers below were taken with a named tool on a named date. The rebuild's were taken against the published page, on the same CDN the baseline crossed, so the weight and score comparisons are like for like.

jameskushphotography.com — Squarespace — as measured, 8 August 2026
Lighthouse, three consecutive runs — 8 August 2026
51
Performance score
18.2s / 18.2s / 17.6s
Largest contentful paint
3,110 KiB
Page weight
696 KB
Largest single file
~450 KB
Squarespace JavaScript
60ms
Server response
Lighthouse, three consecutive runs, deployed to Cloudflare Pages — 9 August 2026
Measured against the deployed page.
99 / 99 / 100
Performance score
1.3s / 1.2s / 1.2s
Largest contentful paint
206 KiB
Page weight
64 KB
Largest single file
Zero bytes
JavaScript
0
Cumulative layout shift
Both columns now cross the internet: these runs were taken against the published page on Cloudflare's CDN, the same conditions the baseline was measured under, so the weight and score comparisons are like for like. The LCP comparison is not, and the reason belongs to the baseline rather than this page — the live site shuffles six photographs into its hero on every load, so which one becomes the largest contentful element is chance. Four re-runs returned 5.5s, 17.6s, 18.0s and 17.6s. Weight and server response reproduced exactly; LCP did not, and the 18.2s figure should never be quoted on its own.
The baseline, over the internet
Run 01
18.2s
Run 02
18.2s
Run 03
17.6s
The rebuild, on the same axis
Run 01
1.6s
Run 02
1.6s
Run 03
1.6s
The vertical rule sits at 2.5s — the Core Web Vitals threshold for a good largest contentful paint. It marks the standard, not a result.
The site works. Twenty-one pages of galleries, every link on it resolving, a server that answers in 60ms. What it does not do is arrive. Three consecutive Lighthouse runs put the largest contentful paint — on a photography site, that is the photograph — at eighteen seconds.
Sixty milliseconds of that is the server. The remaining seventeen-and-a-half seconds were shipped.
Two files account for more than a third of the page. One is a photograph served at its original size to every device that asks for it, phone included. The other is the platform’s own JavaScript, none of which a portfolio needs to show a picture.
A note on units. Lighthouse reports the page total in KiB and individual resources in KB. The two differ by 2.4%, which is narrower than the spread between the three runs above, so the bar is drawn on the KiB total with the resource figures unconverted. Said out loud because a chart that quietly mixes units is wrong by an amount nobody can see.
Four moves, in the order they matter.
Serve the photograph, not the original. One <picture> element, AVIF first,
WebP behind it, JPEG last, and a srcset that hands a 400px-wide phone a
400px-wide file. The photograph stays the payload; it stops being the whole
payload.
Ship no framework. Semantic HTML, one stylesheet, and JavaScript only where something genuinely cannot be done without it — which on a gallery is the lightbox and nothing else. This page you are reading is built that way; so is every other page on this site.
Reserve the box before the image lands. Intrinsic width and height on
every image, so the layout is final at first paint and the type does not jump
when a photograph decodes. Cheap, structural, and invisible when it works.
Put nothing above the photograph. No announcement bar, no cookie banner racing the hero, no font that blocks text for 300ms. On a portfolio the first paint and the point of the page are the same object, and everything queued in front of it is a delay charged to the work.
It exists now, at /lab/photography-rebuild/demo. Same copy, same six photographs, same layout, same typefaces, same six-second hero rotation. It is a reconstruction, not a redesign — a free reimagining is a separate piece, and improving the design here would have quietly turned a measurement into a sales pitch.
Two hundred and six kibibytes, all in. Six photographs, four typefaces, the
markup and the stylesheet. Zero bytes of JavaScript, which is not a boast but a
build condition: scripts/check-budgets.mjs fails the build if that route ships
any.
The photograph that paints first is 23 KiB of AVIF. It is smaller than the four subset typefaces behind it, which is the sort of thing you only find out by measuring, and it is why the typefaces were cut to the glyphs the page actually sets rather than shipped whole.
Reading the live page’s own source answered a question the baseline numbers
could only pose. The six hero stills are not in the markup at all. A script runs
after DOMContentLoaded, finds every photograph elsewhere on the page, hides the
sections it took them from, shuffles them, and sets the first six as CSS
background images.
The browser’s preload scanner cannot see a URL that does not exist yet. So the photograph — the entire point of the page — cannot begin downloading until the platform’s JavaScript has arrived, parsed and run. The sixty-millisecond server was never the problem, and neither, really, was the 696 KB file. The problem is that nothing could start.
One consequence is worth stating because it makes the baseline harder to quote than it looks: since the six are shuffled, which photograph becomes the largest contentful paint is decided by a coin toss on every visit. Re-running the same tool against the same URL on 2026-08-09 produced 5.5s, 17.6s, 18.0s and 17.6s — three runs that reproduce the original measurement almost exactly, and one that happened to draw a light photograph first. A page whose headline metric varies by a factor of three depending on chance is not a page with a slow image. It is a page with no priority order at all.
Both columns were measured the same way: Lighthouse, three consecutive runs, against a page served over Cloudflare’s CDN. The weight and score columns are like for like. 3,110 KiB became 211 KiB, and 51 became 99.
The largest-contentful-paint column is not, and the reason belongs to the baseline rather than to this page. The live site shuffles six photographs into its hero on every load, so which image becomes the largest contentful element is decided by chance. Re-running it four times returned 5.5s, 17.6s, 18.0s and 17.6s — the same page, the same connection, a spread of twelve and a half seconds. Page weight and server response reproduced exactly across all four; LCP did not.
So the honest statement is this: the baseline’s LCP is somewhere between five and eighteen seconds depending on which photograph it happens to load, and the rebuild’s is 1.2s regardless of which one it loads, because every image on it is served at a size that suits the slot it fills. The 18.2s figure should never be quoted on its own, and it is not quoted on its own here.
The demo is published. Anyone can open it and run the same tool.