Case study · Work in progress

A website checked frame by frame

A veterinary practice's new website. The interface is in place and the copy is being finalised — this page describes where it stood in October 2026.

The situation
You run a veterinary practice and want a new website. You have already put the structure together in an outline, you would leave the design to someone else — and while the new one is built, the old site has to keep working.
What I have built so far
A custom theme on the outline's structure, with no plugins, in two languages. I measured loading, motion and the phone layout frame by frame, in two browser engines, and treated an old iPhone as a target device too.
Where it stands
The new site is built on a staging address, and by 4 October 2026, twenty-one versions had gone out there. The copy is being finalised, with avatars standing in for the team photos until then; the old site keeps running meanwhile.

The work in numbers

Key facts of the site in progress
Collaboration startedAugust 2026 — a quote request with a structural outline
Statusin progress — on a staging address, copy being finalised
Languages2 — Hungarian and English
Service subpages14
Versions released to staging21, by 4 October 2026
Test suite37 items (October 2026), the browser checks in two engines
Counter-tests98, each reverting one safeguard (October 2026)
Project
veterinary practice
Role
Design on the client's outline, custom theme, motion and loading, phone layout, releases
Engagement
Contracted build, in feedback rounds
What I take on
A custom website, with loading measured frame by frame.kristof@kristofkarner.com
Related
Building a website — price and process

The outline we started from

The request for a quote for the veterinary practice's new website arrived in August 2026, with a slide-deck outline of the structure: which pages there should be and what goes on them. The client left the design to me. Later, nine other veterinary sites arrived as references, some with a particular part highlighted.

From the nine I took three things: the team's personal introductions, a clear page structure, and a dropdown menu for the services. What became unique is the live opening status: the site shows whether the practice is open right now, which none of the nine did.

The copy came from the client's material — the outline, the old site and passages marked up and sent later — and the English version is a translation of it. Where something was still missing, a placeholder stands until the final version arrives: avatars in place of the team photos, for instance, at the client's request.

The old site kept running throughout

From day one the veterinary practice's new website was built on a staging address, kept apart from the old one; the old site kept running on the main address — the client asked for this at the start. After every round of feedback the new version went out there, so the practice looked at the real, clickable site in every round.

By 4 October 2026, twenty-one such versions had gone out. Before every release the staging site is backed up, and the release only continues with an intact backup. Afterwards an automated check runs: the pages and every file they reference, the security headers, and finally, file by file, whether the server holds exactly what the local machine does.

Refinement, measured

On the practice's new website my bar was that nothing should pop in or jump while loading, and that the site should behave the same in every browser. I did not check this by eye: a machine loads the page and records, frame by frame, what is visible of it.

An example: in one version the content sharpened out of a blur while loading, yet it still felt like a pop. The frames showed why: the text became 84 per cent visible within 80 milliseconds, and the blur only cleared after that. In the version of 4 October 2026 the page settles first — the header, the section backgrounds, the large image — and the content follows a beat later, softly.

Frame-by-frame measurement also found that during page changes the icons stood empty for 60–130 milliseconds, that on a first visit the menu items jumped 7–15 pixels when the fonts arrived, and that the opening image faded for a moment at the end of loading. To the eye each was only a brief jolt; what caused it came from the frames.

What this means if you are in a similar position

If you want a new website, you can put the structure together yourself, even as a slide deck, and leave the design and the build to me. Your old site keeps working meanwhile, and you can review every round on the real site, on your own phone too.

As for refinement, I look at what the visitor sees: what appears when the page loads and when — measured frame by frame — and whether it works the same way on an older phone too.

If you want a new website and already have an idea of its structure, write a few lines about what you have now and what you would like. One exchange of emails usually shows how much work it is.

kristof@kristofkarner.com
Technical detail

From here on, the details: how I measure page loading frame by frame, how I check the tests themselves, what an old iPhone means as a target device, and a few bugs only the measurement revealed.

The size of the build

The size of the build
Custom theme53 PHP files, 9,348 lines
Hand-written CSS and JavaScript3,065 lines of CSS, 1,822 lines of JavaScript
Copykept apart from the code, in two JSON sets, Hungarian and English
Pluginsnone
Test suite37 items — 36 files, 7,975 lines (October 2026)
Counter-tests98 rows with 118 replacements (October 2026)
Verified on the local copy — the theme, the test suite and the counter-tests
$ find . -name "*.php" | wc -l             # in the theme folder
      53
$ grep -c '^\s*fut "' teszt/mind.sh        # in the project root
37
$ python3 teszt/ellenproba.py --horgonyok
Ellenpróba-horgonyok: mind a 118 egyértelmű.

One page load, frame by frame

On this project the page load is examined by a measuring script of my own. A machine loads the page on a throttled network with an empty cache, in the engines of Chrome and Safari (Chromium and WebKit), at desktop and phone sizes. For the first 3.5 seconds it looks at the elements on screen on every frame — images, icons, headings, paragraphs, buttons — and records how visible they are and where they stand.

It counts as a failure if anything appears within a single frame with no fade, if a visible element jumps more than three pixels, or if an element of the page turns up ready-made after the first paint. The measurement runs separately for the first visit and for page changes, because the browser's cache holds different things in the two cases.

The measuring script checks itself too: when it deliberately switches off the header's hiding on the first visit, it has to report the jump of the font swap. If it does not, the measurement is blind, and the run fails.

One page load, measured frame by frame A machine loads the page on a throttled network with an empty cache, in two browser engines, at desktop and phone sizes. On every frame it looks at the elements on screen: how visible they are, where they stand, whether the image has loaded, whether the icon's drawing is there. It is a failure if anything appears within a single frame with no fade, or if a visible element jumps more than three pixels. At the end comes a self-check: with a safeguard deliberately switched off, the measurement has to report. The first visit and page changes are measured separately. Slow network, empty cache two engines, desktop and phone Every frame, every element opacity, position, image, icon Fail: if it pops in within one frame, with no fade Fail: if it jumps over 3 pixels, while visible Self-check with a guard removed, it must report first visit and page change apart · five pages · two engines
The first visit and page changes are measured separately: the cache holds different things in the two cases, and different things can pop in.
  1. 01 Slow network, empty cache two engines, desktop and phone
  2. 02 Every frame, every element opacity, position, image, icon
  3. 03 Fail: if it pops in within one frame, with no fade
  4. 04 Fail: if it jumps over 3 pixels, while visible
  5. 05 Self-check with a guard removed, it must report

first visit and page change apart · five pages · two engines

The test I break on purpose

On this project the counter-test script removes each safeguard from the source, one at a time and temporarily, runs the test that belongs to it, and expects it to fail. If the test stays green even then, it is not measuring what I meant it to. After the run the source is restored byte for byte, and the script checks that too.

By October 2026 there were 98 such rows, with 118 replacements. On 3 October 2026 it happened three times in a single day that a row went stale without a sound: the code changed underneath, and the place of the replacement was gone. The full run only stopped at the broken row, and the test suite did not see it at all. Since then, the first step of the suite checks the place of all 118 replacements, in seconds.

For the inline script in the page head this takes more steps: the browser only runs it if its fingerprint (hash) is listed in the security policy, so reverting it means swapping the fingerprint too. On 2 October 2026 the head changed five times in a single day, and with it, each time, five fingerprints computed by hand. Since then the counter-test computes the fingerprint at run time.

An iOS 15 iPhone as a target device

On this website one of the target devices was an iPhone 6s on iOS 15. That older Safari does not know several parts of current CSS — subgrid or the lh unit, for example — so wherever I used them, a fallback stands before them, and the layout stays the same.

This is also why the transition between pages is a script of my own. The browsers' built-in solution (View Transitions) is unknown to iOS 15 and Firefox, while the bar was that it should work the same in every browser. The old page's content fades within 140 milliseconds, the header stays put, and the new page's content sharpens.

The security policy, measured live

The veterinary website's pages run under a full Content Security Policy: no foreign script can run, and the site's own inline scripts only with their fingerprints. Since 24 September 2026, after every release a checker walks through the staging site in both browser engines, and verifies with a deliberately injected foreign script that the block really holds.

What is not obvious

The blur that did not prevent a pop-in

In one version of the load-time sharpening the text came out of a blur, yet it felt like a pop. The cause was the curve: opacity reached 45 per cent within 30 milliseconds and 84 within 80, and the blur only cleared after that. In the corrected version the opacity starts gently — at 100 milliseconds it is at about 40 per cent — and the blur clears along with it.

The menu the animation had no effect on

In the desktop view the menu's container is set to display: contents, so it has no box of its own. The entrance animation placed on it therefore did nothing: the menu jumped in at once while the logo and the buttons sharpened. Since the fix the container's children get the animation, and the measuring script does not count the animation of such an element as a fade.

The hidden icon set that took up 150 pixels

The icons come from a single hidden SVG set. The hidden attribute hid nothing on this element: the browser's rule for it applies to HTML elements, not to SVG. So the set left a 150-pixel empty strip at the bottom of every page. Since then display: none hides it, and the measurement checks the bottom of the page too.

Safari computed it but did not repaint

The button fills were first built on the transition of a registered CSS variable (@property), with the background or the mask drawn from it. Safari's engine ran the variable's transition but did not refresh the painting built on it, so the buttons, the team cards and the fading of the reviews jumped to their end within a single frame. Meanwhile the computed value showed that everything was fine. Since then the fills run on the browser's own animatable properties — size, clipping, mask size — and I look at motion on WebKit frames too, not at the computed value.

The image that faded after loading

On a slow network the home page's opening image faded in after loading, then faded for a moment from 0.99 to 0.11 opacity, and reappeared: the image rotator's own opacity transition restarted on load. Since the fix the image is invisible rather than transparent until it loads (visibility), so the rotator's transition never starts, and the image fades in once, evenly.

Questions about this project

How do you check that a website works the same on every phone and browser?

By measuring it. On this project a machine loads every important view in two browser engines — those of Chrome and Safari — at desktop and phone sizes, zoomed in and on a slow network too. By October 2026 the test suite had 37 items, and a full run takes about three quarters of an hour. I also look at it on an iOS 15 iPhone, because that older Safari does not know some newer CSS features, and there they need a fallback.

Why do images or icons pop in while a page loads, and what can be done about it?

Most often because they arrive later than the text, and at the moment they load they appear within a single frame. On this site an image stays invisible until it has loaded, then fades in over 400 milliseconds; anything already in the cache appears at once. The icons had a different cause: their drawings stood at the very end of the page, and during page changes they showed empty for 60–130 milliseconds, so the drawings moved to the start of the page. Frame-by-frame measurement revealed both, and since then the test suite watches for them on every page.

What does it mean that you test the tests?

It means I revert each safeguard in the source code, one at a time and temporarily, run the test that belongs to it, and expect it to fail. If it stays green, the test is not measuring what I meant it to. On this project there were 98 such counter-tests by October 2026; after a run the source is restored byte for byte, and the script checks that too. Because a counter-test can go stale without a sound when the code changes underneath it, the first step of the full suite checks the place of all 118 replacements automatically.

Can a new website work on an old iPhone?

Yes, if the old browser is a target device from the design stage on. On this project an iPhone 6s on iOS 15 was a target device. That older Safari does not know subgrid or the lh unit, for example, so wherever I used them, a fallback stands before them, and the layout stays the same. This is also why I wrote the page transition as a script of my own: the browsers' built-in solution is unknown to iOS 15 and Firefox.

Does the old website have to go offline while the new one is built?

No. On this project the new site is built on a staging address, kept apart from the old one, while the old one keeps running on the main address. After every round of feedback the new version goes out to the staging address — twenty-one by 4 October 2026 — so the client looks at the real site in every round. The switch comes when the copy is final.

Summary

A veterinary practice's new website has been in the works since August 2026. The client asked for a quote with a slide-deck outline of the structure and nine reference sites, left the design to me, and asked that the old site keep running meanwhile; so the new one is built on a staging address, where twenty-one versions had gone out by 4 October 2026. What is being built is a custom theme with no plugins: 53 PHP files, 9,348 lines, with the copy kept apart from the code, in two languages. The bar was that nothing should pop in or jump while loading, the same in every browser; frame-by-frame measurement guards this, separately for the first visit and for page changes, in the Chromium and WebKit engines, and an iOS 15 iPhone is a target device too. By October 2026 the test suite had 37 items, and 98 counter-tests check that the tests really measure: each reverts one safeguard and expects the test to fail. The measurement found the sharpening that rose too fast, the icons that stood empty during page changes, the menu that jumped in at once because of display: contents, and the opening image that faded after loading.

Kristóf Karner

Independent developer in Budapest. This work is in progress — questions are welcome at the address in the contact section.

← All case studies