Blank on purpose

A month of blocking one domain at a time showed that the most expensive script on the site was the one somebody had paid for.

#performance #tooling

A client’s homepage sat blank for about a second and a half on a phone before it painted anything. It was doing that on purpose.

It wasn’t slow, it was blank. The HTML had arrived and so had the stylesheet. The browser knew what the page looked like and was ready to draw it.

But something up in the head was holding the whole body at opacity: 0 until a script from another company answered. Somebody had pasted that snippet in by hand, straight out of the documentation for the tool they were paying to make the site convert better.

I only found it because I’d spent most of November going through every third party on that site one at a time. I only started doing that because somebody on their marketing team asked me a direct question and got a bad answer.

They wanted to know whether the review widget was slowing the site down. I said probably, a bit. That’s not an answer, that’s a vibe. They’d have been within their rights to ignore it.

Have you tried turning it off?

The technique that ends the argument is free and already in the browser you have open. It costs a few minutes per script.

DevTools can block requests. Right-click any request in the network panel, block its domain, reload and read the difference. You’re not estimating from a vendor’s marketing page or guessing from a bundle analyzer. You’re measuring the counterfactual on the actual site, the page with the thing on it against the page without.

a few minutes per script
1. Network panel, throttle to Fast 3G, CPU 4x slowdown
2. Reload, write down: total transfer, blocking time, first paint
3. Right-click the request, Block request domain
4. Reload, write down the same three numbers
5. Unblock, next one
Text

Two things make or break it. Throttle the processor as well as the network because the processor half is where this stuff does its real damage. An unthrottled laptop hides it completely. And run every measurement three times because the numbers are noisy and the first run will talk you into something wrong.

You end up with a table that has a row per vendor and a cost in seconds. I’ve gotten more out of that table in one meeting than out of every principled argument about page weight I’ve ever made.

Size isn’t everything

Before any of the results, I’d want you to know that file size understates all of this in about four ways.

For one, third parties load more things. A tag container is maybe 30 KB and then fetches whatever somebody configured it to fetch, at runtime, from wherever. Your build tooling has never seen those files and your bundle analyzer can’t know they exist. The 30 KB is the menu and not the meal.

JavaScript also costs more than its weight. 100 KB of image is decode work on a track of its own, where 100 KB of JavaScript has to be downloaded, parsed, compiled and run on the main thread. On a mid-range Android that math is brutal in a way it just isn’t on the machine you’re testing on.

Every new origin costs a connection of its own. There’s a lookup, then a connection, then a TLS handshake, all before a single byte of the actual script arrives. On a high-latency mobile connection that’s a few hundred milliseconds per domain. That site was talking to 11 other companies.

And a fair amount of it blocks. Anything injected synchronously into the head stops the parser where it stands. Historically that’s meant most analytics snippets and all of the category I’m about to be rude about.

Now you see it

Honestly, most of the results were dull. Most third parties on most sites cost a bit. If you took them off, nobody outside the meeting would notice either way. One category is different, and it’s the finding I’d most like people to walk away with.

Client-side A/B testing and personalization tools are, by design, catastrophic.

Those tools have a flicker problem. If the tool loads asynchronously, the visitor gets the original page and then watches it turn into the variant a beat later. That looks broken, which is worse for the client’s numbers than not testing at all.

So essentially all of them offer the same fix: a small synchronous snippet at the top of the head that hides the page until the tool has loaded and applied its variants. They put a timeout on it because the tool might never load. The defaults I’ve seen are measured in whole seconds.

That means the tool bought to increase conversion shows every visitor a blank white page while a script comes down over their connection. If that request is slow, everybody waits. If it fails, everybody waits out the entire timeout and then gets the original page anyway.

On the site I was measuring it was the single largest contributor to first paint by a mile. That held on every page, including all the ones with no test running on them.

I don’t think it’s a scandal. The vendor made an honest trade and documented it in their own help pages. Given what they’re being asked to do inside a browser, I’m not sure a better answer exists.

But it does mean the tool spends a fixed cost on 100% of visitors to maybe gain a couple percent on a fraction of them. I’ve never once seen the person who bought it do that math.

Somebody else’s bad afternoon

The other cost never makes it into the table because it isn’t there on the day you measure.

Every one of these scripts is a dependency on another company’s uptime, sitting in your critical path with nothing behind it. No contract, no status page anybody here is watching, and in most cases no relationship at all.

A synchronous script from an origin that hangs will hang your page. It won’t fail quickly. It’ll just hang until the browser gives up, which is a long way past the point where the visitor has gone somewhere else.

I once watched a site sit unusable for about 20 minutes because a font service was having a bad afternoon. There was nothing on our side to fix and nothing to do but reload and wait. The client asked a completely fair question about why their site was down when nothing about it had changed. I didn’t have a good answer for them.

async and defer fix most of that and cost nothing. They should be the default for anything that isn’t genuinely needed before the page renders, which is nearly everything on nearly every site. The testing tools from the last section are the one category they can’t fix, because there the blocking is the entire function.

So whose is this one?

I handed over a spreadsheet, which is kind of an anticlimax after a month of work.

It had a row for every third party on the site, with what it was, who asked for it and what it cost in seconds. The last column was a person, not a team. “Marketing owns that one” is how a row survives review cycle after review cycle without anybody looking at it. A name in a cell is somebody you can ask whether they still want the thing now that there’s a number sitting next to it.

Nobody could account for half the rows at all, and not in a sinister way. The person who added the thing had left, or the campaign it was bought for ended in 2017 and the tag stayed behind, or it came bundled with a theme.

There were two analytics scripts collecting more or less the same events, added about two years apart. Nobody still working there could tell me why either one was on the page. I think that’s just the normal condition of a site more than a couple of years old.

The spreadsheet didn’t actually move anybody. I recorded the client’s own homepage loading twice on a throttled phone, once with that one domain blocked. Then I played the two recordings side by side on a call. Nobody argues with that.

It works for the same reason a keyboard walkthrough done live lands where a written finding gets noted politely. It’s their own site and it isn’t a gotcha. Everyone in the room can see the thing for themselves.

They canceled the subscription that week. I’d love to say this works every time, but it doesn’t. The same demo on a different project in the spring changed nothing at all. That time the person who’d bought the tool was sitting in the room and I was playing their own decision back to them.

What stayed

Most of it stayed. The analytics is small and async and somebody genuinely opens it. I’d rather spend the seconds there than pretend I’ve achieved some kind of purity. The consent platform stayed too, which is its own circular little joke and a fight for a different month.

The week before I started any of this, I sat through a 40-minute argument about whether a 28 KB framework was too heavy for a project. It was a good argument. Everybody was reasonable and technical and enjoying themselves. There were real numbers on both sides.

It was also 28 KB against the 470 KB of other people’s JavaScript I’d measured on that same site the day before. So the whole thing was a fight over about 6% of the page’s script weight. The other 94% never came up once.

I don’t think that’s stupidity. The framework is ours. It feels like our decision and our responsibility, so that’s where rigor feels appropriate.

The tag somebody in marketing added in March is weather. You don’t audit the weather, you complain about it and move on with your day. That’s the same reflex that had me spending a day on a stylesheet while a 2 MB image sat at the top of the page.

Everything in this post is really just a way of making other people’s code feel like it’s on your page too. It is on your page, and the person on their phone waiting out a timeout already knows that.

Read similar posts

View posts by tag

#accessibility #agency #ai #careers #cross-browser-compatibility #css #culture #debugging #documentation #ecommerce #forms #git #html #images #javascript #performance #php #privacy #scss #security #shopify #small-print #tooling #typography #wordpress