Concept teardown · Behavioral health
Tuckerman Behavioral Health
A telehealth-first therapy practice, built to show what changes when the person building the site also handles the security. Analytics stop at the intake form.
Tuckerman Behavioral Health is a fictional business. The build is real and every number below is measured against it; the client is not. Visit the concept site.
How to read this
Tuckerman Behavioral Health is not a real practice. The site is a real build, served from the same infrastructure as everything else this studio ships, and every number on this page was measured from the served site rather than asserted from the source code. Where a measurement could be faked by looking at the wrong thing, we say which instrument was used and what the control was. The method is at the bottom, and it is worth reading.
The build measured here is 5122cfa, verified stable across six consecutive measurement rounds.
Four defects are reported below, one of them trivial. That is deliberate. A technical report with no findings is not evidence that a site is clean. It is usually evidence that nobody looked.
Performance
Measured on a throttled mobile profile, because that is the honest case: a 390 by 844 screen, a processor slowed fourfold, and a connection at 1.6 Mbps with 150 milliseconds of latency. Roughly, a mid-range phone on ordinary cellular service.
Staging, mobile profile: 390x844, 4x CPU throttle, 1.6 Mbps down, 150ms latency. Cold cache. Laboratory measurement, not field data.
| Page | First paint | Largest paint | Layout shift | Blocking time | Largest element |
|---|---|---|---|---|---|
| Home | 696ms | 696ms | 0.0061 | 35ms | Opening paragraph |
| Contact | 668ms | 668ms | 0.0000 | 3ms | Paragraph |
| First visit | 692ms | 948ms | 0.0239 | 36ms | Photograph |
| Fees | 716ms | 1,408ms | 0.0038 | 30ms | Photograph |
| Services | 704ms | 1,604ms | 0.0225 | 35ms | Photograph |
| Team | 688ms | 1,912ms | 0.0355 | 30ms | Portrait |
Google’s threshold for a good Largest Contentful Paint is 2.5 seconds and for cumulative layout shift is 0.1. The slowest page on this site reaches its largest paint in 1.9 seconds, and the worst layout shift on the site is 0.0355, about a third of the allowance. On desktop every page paints between 144 and 348 milliseconds with zero measurable layout shift at all.
The claim worth making, stated exactly as far as the evidence goes. On Services, Fees and First Visit the largest element on the page is the photograph itself, and it arrives between 0.95 and 1.6 seconds. The slowest single element anywhere on the site is the Team page’s first portrait at 1.9 seconds. A photograph-led site is usually a slow site. This one is not, and the photographs are the thing being measured.
The claim we are not making. On the home page the largest element is not the hero photograph, it is the opening paragraph, at 696 milliseconds. We do not currently have a verified explanation for why the browser never nominated the hero image as a candidate on that page, and rather than offer a plausible one we are telling you the measurement is unexplained. It will either be explained or corrected here.
Weight, as served
Staging, mobile profile, cold cache. Transfer as served and compressed, not bytes on disk.
| Page | HTML | CSS | JS | Fonts | Images | Other | Total |
|---|---|---|---|---|---|---|---|
| Contact | 5.3 | 5.9 | 2.5 | 86.2 | 6.0 | 9.3 | 115.3 KB |
| First visit | 4.5 | 6.0 | 164.4 | 86.2 | 28.1 | 9.3 | 298.5 KB |
| Home | 4.2 | 5.9 | 164.3 | 86.2 | 43.0 | 19.1 | 322.8 KB |
| Fees | 4.5 | 5.9 | 164.4 | 86.2 | 56.0 | 9.3 | 326.3 KB |
| Services | 4.3 | 6.0 | 164.4 | 86.2 | 83.5 | 9.3 | 353.7 KB |
| Team | 3.4 | 5.9 | 164.4 | 86.2 | 106.0 | 9.3 | 375.3 KB |
Look at the JavaScript column, then look at Contact.
The 164 KB carried by every other page is not the site’s own code. It is analytics: a tag manager and a performance beacon. The site’s own JavaScript is 2.5 KB. Contact runs that 2.5 KB and nothing else.
Typefaces are self-hosted and served from this site’s own origin: two variable font files, 86.2 KB
together, Latin subsets only, with font-display: swap so text appears immediately in a fallback and
swaps when the real face arrives. There are zero requests to Google Fonts or any font CDN, on every
page. Most sites hand a third party a record of every visitor in exchange for a typeface.
🔎 Finding 1: 128.9 KB of waste on every page, invisible to the eye
Measuring weight by category rather than in total is what caught this, and it is the argument for doing it that way.
The site preloaded one Newsreader font file while its stylesheet asked for a different one. The two files carry different, non-overlapping axes: one can vary weight, the other can vary optical sizing, and neither can do both. So every page downloaded both files and used one. 128.9 KB of pure waste, on every page, and it was the single largest asset on the site.
It also meant that ten separate optical-sizing declarations in the stylesheet had been doing nothing at all since the day they were written.
Fixed. Fonts went from 215.9 KB across three files to 86.2 KB across two. Nothing changed visually, because the effect those declarations described had never once been applied.
Nobody would have found this by looking at the site. It was not slow enough to feel and not wrong enough to see. It was found by counting bytes by category and noticing that one number was implausible.
For scale: three real practices, same instrument
Three real behavioral health practice websites in New Hampshire, measured on the same throttled mobile profile, cold. They are not named and not linked, because the point is the range rather than any particular business.
Their production, our staging, same mobile profile, cold cache, network round trips. Single runs, not medians.
| Site | Requests | Third-party origins | Weight | Largest paint |
|---|---|---|---|---|
| Practice A | 50 | 5 | 1,029 KB | 3,544ms |
| Practice B | 204 | 15 | 7,306 KB | 6,140ms |
| Practice C | 60 | 14 | 1,867 KB | 3,312ms |
| Tuckerman, home | 21 | 4 | 323 KB | 696ms |
| Tuckerman, intake | 11 | 0 | 115 KB | 668ms |
One of them is 7.3 megabytes and makes 204 requests. All three load third-party trackers on the page a prospective client lands on.
Two honesty notes. These are the practices’ home pages, not their intake forms, so we are not claiming anything about what happens when someone types into them. And these are single cold runs rather than medians, so treat them as the order of magnitude, which is where the difference lives anyway.
Why you should care. Someone looking for a therapist is often doing it on a phone, in a parking lot, in a spare five minutes, and frequently at the worst moment of their month. If the page has not painted before their patience runs out, they leave, and they do not tell you they left. A slow site does not generate an angry email. It generates nothing at all, which is why it can go unnoticed for years.
Security posture
Seven response headers, served on every route:
| Header | Value |
|---|---|
strict-transport-security |
max-age=31536000; includeSubDomains; preload |
content-security-policy |
frame-ancestors 'none' |
content-signal |
search=yes, ai-input=yes, ai-train=no |
permissions-policy |
camera=(), microphone=(), geolocation=(), payment=(), usb=(), browsing-topics=() |
referrer-policy |
strict-origin-when-cross-origin |
x-content-type-options |
nosniff |
x-frame-options |
DENY |
The permissions policy is the one worth reading twice. It tells the browser that this site may not request the camera, the microphone, location, payment credentials, USB devices, or take part in interest-based ad topics. Not that it does not. That it may not, enforced by the browser rather than by our good intentions.
These headers were not added for this build. They are inherited unchanged from the studio infrastructure the site runs on. That is the entire point. A site built here passes a security header audit on the day it launches because of who built it, not because somebody opened a ticket six months later.
Why you should care. This is the list your IT person asks for, and increasingly the list your insurer asks for. Most agencies cannot produce it, and the reason is rarely negligence. It is that the person who designed your site and the person who understands transport security are usually not the same person, and nobody owns the gap between them.
The intake form, and the number this page exists to show you
The page where someone types why they need therapy makes 11 network requests. All 11 go to this site’s own origin. Zero go to anyone else.
No analytics. No tag manager. No session recorder, no heat map, no advertising pixel, no chat widget, no font CDN. Nothing loads on that page capable of reporting what was typed, how long someone hesitated over it, or that they were ever there.
Rather than describe what the page does not load, here is every request it makes.
Staging, mobile profile, cold cache, counted as network round trips, so redirects appear as their own rows.
| # | Status | Bytes | Resource | |
|---|---|---|---|---|
| 1 | request | 200 | 5.3 KB | the page itself |
| 2 | request | 200 | 57.4 KB | Newsreader, variable weight |
| 3 | request | 200 | 28.8 KB | Source Sans 3, variable weight |
| 4 | request | 200 | 5.9 KB | stylesheet |
| 5 | request | 200 | 0.7 KB | page script |
| 6 | request | 200 | 1.8 KB | prefetch script |
| 7 | request | 200 | 6.0 KB | favicon |
| 8 | request | 200 | 5.2 KB | prefetch of the teardown page |
| 9 | request | 200 | 4.1 KB | prefetch of the concept home page |
| 10 | redirect | 307 | none | trailing-slash normalisation |
| 11 | redirect | 307 | none | trailing-slash normalisation |
Eleven round trips, nine distinct resources served, zero of them belonging to anybody else. The two prefetches are this site warming pages this one links to. The page is not empty of requests. It is empty of other people’s requests.
It is also, by a wide margin, the fastest and lightest page on the site. At 115.3 KB it is 183 KB lighter than the next lightest page and 260 KB lighter than the heaviest, and 88% of that difference is the tracking it does not load. The privacy decision and the performance decision turned out to be the same decision. That is worth sitting with, because it is usually presented as a trade.
And here is what makes it a measurement rather than a claim. An absent tag and a broken tag look identical from outside. So the same instrument was run against the Fees page on the same build, and Fees reaches both the tag manager and the performance beacon exactly as it should, carrying 164.4 KB of JavaScript to the intake page’s 2.5 KB. The zero on the intake page is deliberate suppression, proved by a control showing the machinery works everywhere else. It was measured by watching what the page actually requested in a clean browser session, not by searching the source code for the absence of a string.
🔎 Finding 2: our own redirect tax, and why it is printed rather than dropped
Rows 10 and 11 are redirects, and they are ours. The internal links are written without a trailing slash while the server normalises to one, so each prefetch spends a round trip being told where it already was going.
It is small. Two round trips, no bytes, no measurable effect on any page’s rendering. It is not specific to this concept either: every internal link on the studio’s own site pays it.
We are printing it anyway, because the alternative is a request table that quietly omits the two rows showing us paying a tax, on a page whose argument is that we count things honestly. A reader with developer tools open finds those rows in about ten seconds, and finding them there after we left them out would cost more than the tax does.
It is logged to be fixed as a site-wide change rather than patched here, because changing how internal links are written touches how the sitemap is built, and a teardown is not the place to quietly re-scope that.
Why you should care. In behavioral health this sits close to a compliance posture, and it is expensive to retrofit and nearly free to get right at the start. In any other industry it is still the difference between a contact form and a data-collection event, and most sites cannot tell you which one they are running, because nobody ever put the question in those terms.
Findability
The concept sites in this gallery carry noindex, follow, which keeps invented businesses out of
search results while still letting links through to pages like this one. This teardown carries no
robots restriction and sits in the sitemap. The distinction is deliberate: the demo is not the
product, the report is.
🔎 Finding 3: 20 of our own 21 pages had never been crawled
A findability section written by somebody with nothing to admit is not worth reading, so here is ours.
On 30 July 2026 we checked this studio’s own site in Google Search Console and found that 20 of its
21 pages had never been crawled. Not ranked badly. Never fetched. The cause took minutes to find:
no sitemap had ever been submitted, the property was new, and Google had never retrieved
robots.txt, so it had never seen the line pointing at the sitemap that did exist. Fixed the same
morning.
We audit sites for a living and we still found that on ours. Every studio you talk to has something like it. The difference worth paying for is not an absence of mistakes. It is whether anybody is running the check that finds them, and whether you get told when it does.
Why you should care. A site invisible to search is not underperforming. It is absent, and from the outside absent looks exactly like “SEO takes time” for as long as nobody looks.
AI legibility
The content-signal header states this site’s terms to automated readers in a machine-readable way:
searchable yes, usable to compose an answer yes, usable as training data no. The pages are
semantic HTML, and the HTML is small, between 3.4 and 5.3 KB per page, because a page whose meaning
lives in its markup rather than in its scripts is legible to anything that reads it.
Why you should care. A growing share of the people looking for a service never see a list of links. They see an answer, assembled from sites a machine could read. Whether your business appears in that answer is now partly a build question, and it is one you can only take a position on if the header is actually being served. Stating terms you cannot enforce is not a position, it is a wish.
Accessibility
Contrast was sampled from rendered pixels with the text hidden, taking the lightest pixel behind each block of type rather than the average. That distinction is the entire credential of this section, and it is worth being plain about why.
Contrast calculated from the values in a stylesheet is a statement of intent. It tells you what the designer meant to happen. On a page where pale text sits over a photograph, intent is not enough, because the photograph has a bright corner somewhere and the average is never where the text fails. These numbers come from the built page as a browser actually drew it, at the worst pixel, behind live type.
Rendered pixels at 5122cfa, text hidden, worst (lightest) pixel behind each block.
| Where | Headline | Body |
|---|---|---|
| Home hero | 4.76:1 | 14.28:1 |
| Home hero, supporting line | 8.07:1 | |
| Stat strip | 16.57:1 | 7.70:1 |
| Home panel, first | 16.06:1 | 8.96:1 |
| Home panel, second | 15.21:1 | 8.30:1 |
| Home panel chips | 17.48:1 and 17.36:1 | |
| Services page hero | 11.59:1 | 7.76:1 |
| Fees page hero | 6.99:1 | 7.03:1 |
| First visit page hero | 10.34:1 | 8.96:1 |
| Interior panels | 16.73:1 | 8.90:1 and 8.85:1 |
The palette itself: primary ink 17.91:1, muted ink 7.13:1, accent ink 5.69:1, text on dark 16.63:1.
Every text block on the site clears its WCAG threshold, measured. We are deliberately not claiming AAA everywhere, because the Fees hero sits close enough to that line that the claim would be doing more work than the measurement supports.
The lowest number on the site is 4.76:1, the home page headline over the photograph, against a threshold of 3:1 for type at that size. The tightest pair is the Fees hero at 6.99:1 and 7.03:1. Both pass with room. We name them rather than burying them because these are the places where swapping a photograph could push a compliant page out of compliance with nobody noticing. They are where the next check should look first.
The accent color, #C9603F, measures 3.85:1 against paper. It is therefore used for fills and
never for text on paper. A color that fails as type is not a reason to change the palette. It is a
reason to write down where the color may be used, and then hold to it.
Structure, across all six pages: exactly one h1 per page, landmarks present, a skip link that
actually operates, the document language set, and no heading level skipped. Every focusable element
carries a visible focus ring. There are 13, 11, 11, 12, 11 and 20 focusable elements across the six
pages, and not one of them is missing a ring.
🔎 Finding 4: a skipped heading level on the Team page
The Team page shipped with its headings running h1, h3, h3, h2, skipping a level, because the clinician names were marked as third-level headings with no second-level heading above them. Someone navigating by heading structure would have hit a hole.
It was promoted to h2 and kept at the smaller type size, since how important a heading is and how
large it looks are two different questions.
Why you should care. There is a legal exposure argument here and it is real. There is also a simpler one: a person navigating by keyboard who cannot tell where they are on the page is a person who cannot book an appointment.
The images, and what is real about them
The photographs of rooms on this site are AI-generated. So are the portraits on the Team page, which are illustrations of clinicians who do not exist, and the site says so on the page.
For a real practice these would be photographs of real clinicians, and illustration is used here only because the practice is fictional. We state that plainly because the alternative is that this page quietly advertises a service we do not offer. We do not generate your team’s photographs. If you are a real business with real people, those people get photographed.
The rule the whole site was built on is a single question: does this image assert that a specific person exists? If it does, it is illustrated, always, and disclosed. If it is a room, it can be photoreal. Composites, an illustrated person standing in a photographed room, are not permitted at all, because that is the combination that stops reading as a drawing and starts reading as evidence.
Tuckerman carries a stricter rule than the gallery requires: no people at all, not even anonymous ones. A real behavioral health practice would not put faces on its website, because the faces most readily to hand are its clients. That is a confidentiality position rather than an aesthetic one, and it is the sort of constraint that only surfaces when the person building the site understands the industry it serves.
What we chose not to do
The Contact page requests exactly one image: the site’s 6 KB favicon. There is no photograph, no illustration and no logo file on it. It is the quietest page on the site by a distance, and the lightest by 183 KB.
Nothing forced that. Images are served from this site’s own origin, so a photograph on the Contact page would not have broken the zero-third-party rule. This was a judgment, and it is worth saying so, because claiming a technical constraint we did not have would be a small lie on the page whose entire product is honesty.
Two reasons. A mood photograph competes with the form at the precise moment a person is deciding whether to type why they need help, and there is no image worth that. And the argument of that page is that nothing on it is watching. Visual restraint is that argument made in a second language, sitting directly beside the paragraph that makes it in words.
Method
- Build measured:
5122cfa, verified stable across six consecutive rounds. - Performance: laboratory measurements, not field data from real visitors. Mobile figures use a 390 by 844 viewport, 4x processor throttling and a 1.6 Mbps connection at 150ms latency. Desktop uses 1440 by 900 with no throttling. Each route measured in a fresh browser context, cold cache, over the network from the live staging deployment.
- Blocking time is reported in place of interaction latency, which cannot be measured without a real person interacting with the page. Anything else would be invented.
- Weight is reported from the mobile profile only, as served and compressed, not as stored on disk. Desktop figures were taken but are not published: an intermittent third-party script failure on the measuring machine made two desktop rows unreliable, and a number we cannot stand behind does not belong on this page.
- Requests and third parties: counted over the network from the live deployment, per route, in a clean browser session, with the Fees page used as a positive control to prove the tags do load where they are meant to. The same counting method was used for this site and for the three comparison sites, which matters more than the method itself: an earlier draft of this page compared a locally measured figure against externally measured ones, and the numbers were not comparable. That draft was never published.
- Contrast: sampled from rendered pixels with text hidden, worst case behind each block rather
than the mean, and re-measured on every new photograph rather than inherited from the previous one.
Every figure above was re-measured at
5122cfarather than carried forward from the earlier build, because a section whose credential is “measured, not assumed” cannot inherit its own numbers. - Headings, landmarks and focus rings: asserted programmatically against the built HTML across all six pages, not spot-checked.
- Headers: read from live responses on every route, not from configuration files.
- The three comparison sites: same mobile profile, cold, single runs rather than medians, home pages only.
One rule governs every number above, and it was learned the hard way while producing this page. A measured number never travels alone. It carries the instrument (what was counted), the target (local, staging or production) and the profile (viewport, throttling, cache state). Two numbers may be compared only when all three match, and where they do not match the answer is to measure again rather than to reconcile the difference in prose.
Three times while preparing this report, two numbers that were each individually correct were compared under different instruments: a request count taken locally against one taken over the network, an image total counting what the page asks for against one counting what the browser asks for, and a count of responses against a count of round trips. None of them looked wrong, which is exactly the problem. That is why every table on this page carries a caption naming its instrument. If you reproduce a number and get a different one, the caption tells you which knob to check.
Measured against build 5122cfa.