Minid.net

August 31, 2026

Congratulations on Your Ten Milliseconds

Two very different things get called performance: the one you can measure and the one the user can actually feel. Almost three decades of chasing both taught me which one gets noticed, and it is not the one that looks good in a report.

I am sorry Dad, now your computer may rest in peace.

I have been obsessed with the performance of systems for as long as I have been able to touch a computer, and the first computer I could touch was not mine. It was my father’s work machine, and I destroyed it more times than I can honestly count, because I spent my afternoons rewriting his MS-DOS setup according to whatever I had read in computer magazines that month or seen on a friend’s machine. Sometimes I could actually feel the machine get faster, which was all the encouragement I needed. I went from something like forty reinstalls a week down to a few every couple of months, and by then my father had accepted the arrangement: the computer was technically his, but in practice it answered to his curious son. The detail I am still a little proud of is that I always had his work backed up before I started, because I knew perfectly well that the afternoon might end with a reformatted drive. If it went wrong I restored everything and never had to say the sentence Dad, I broke the computer again.

That curiosity turned into a working method. By the time I was old enough to shave I was an engineer whose first instinct on any problem was to look at what the system was wasting, and that instinct came from geography as much as from temperament. Internet connections in Argentina were miserably slow, and not only because of the last mile. The number of hops a packet had to make before it reached anything useful was enormous; I remember traceroutes running past twenty-seven hops as an ordinary Tuesday. Building web systems that did not take forever to load and respond was not an aesthetic preference where I lived, it was the difference between a site working and a site not existing. That is also, incidentally, how this blog started.

We were all on 28.8K, 33.6K, and if you were doing well, 56K modems. You can imagine what visiting a web page was like. The pages of that era are a joke by today’s standards in raw size, and they still took not one or two seconds but thirty or more. Almost nobody optimized anything between 1997 and 2000, and I mean that literally: the web was taking off and developers were competing to see who could build the heaviest page. When cable arrived the pipe got wider and the pages got slower, because the incentive had not changed. Everyone wanted to show off the new capabilities, so we got animated GIFs, a plugin for every kind of media, and roughly twelve hundred million nested tables holding up visual effects that a single CSS rule handles today.

Through all that festival of waste I kept writing HTML by hand, using CSS 1.0 the way you use a hand tool, and treating images as something to be fought with rather than dropped in. Every character I saved from the source felt like a win, and over a few years those wins became visible to other developers and, more importantly, to the people using the sites. Around 2002, with Flash at its peak, web standards and UX finally became things people argued about seriously, and efficiency entered the vocabulary. That is when I had the first of two realizations that have shaped everything I have built since: real, measurable performance and perceived performance are two different products, and when they compete, perception wins. An optimization the user can feel beats an optimization the user cannot feel, even when the one they can feel is not really an optimization at all.

The second realization cost me a lot more sleep. Years later, in the middle of the iPhone boom, I was building Notegraphy, which we described to anyone who asked as the Instagram of words. It shipped as a web app and as native apps for iOS and Android. On Android, speed and efficiency were not remotely comparable to what we could get out of iOS 6 and 7, and Apple’s platform gave me as many headaches as it gave our users, because Objective-C in those days demanded creativity from anyone who wanted to be efficient. The iPhone app still got better every week, and then it got popular, largely because I had unknowingly designed it in the visual language of iOS 7 right as iOS 7 landed. We took more than a million users in two weeks. I had never experienced anything like it, and I barely slept, because the reports came in faster than we could read them. The most common one, by an enormous margin, was the app is slow. No screen, no action, no context. Just slow. So we went digging.

The first thing we found was bad enough to be embarrassing. When someone picked a profile photo on their phone and uploaded it, we happily stored whatever came out of the camera, which was around four megabytes. That image was never cached on the device, so every single time you opened a view that needed it, the app downloaded it again. Multiply that by every other user whose avatar appeared in your feed and the phone eventually gave up and died. Our first fix was the one that looks correct on a whiteboard: check the file size before uploading and refuse anything too large, with a polite message asking for a smaller image. Users hated it, and they were right to. They did not know why the app was scolding them, and making a smaller image on a phone in 2013 was not something you did casually, so the ones who understood the request were furious about being asked to do our work for us. We pulled the check and did the honest thing instead: upload whatever you like, we will resize it on the device before sending it. That was necessary but not sufficient, because the resizing froze the app for a few seconds and then released it, which is a terrible thing to do to someone who just wanted to change their picture. The version that actually worked was the simplest one to describe and the most work to build: send the original, and let the backend process it however we want.

Once processing lived on the server the benefits compounded. We stopped producing one avatar and started producing a family of them for different contexts, tuned per surface, mostly as JPEGs. Then retina screens arrived, and the same picture had to exist at multiple densities, which meant that satisfying people’s eyes now cost them a meaningful amount of data on connections that were not ready for it. We needed strategies that were not simply send fewer bytes, and that is where I started playing seriously with perception.

We had a ritual that made this possible. Once a week a group of users came to the office to talk to us. We used those sessions to show the loudest, most invested people on the platform what was coming next, and, with no shame whatsoever, to use them as test subjects for usability. A surprising number of the tricks I still use came out of that room.

Images were the obvious place to start, because Notegraphy was a graphically loud product: notes in every size, color, and style, each one with a drop cap rendered as an SVG or PNG drawn by designers I had no business being able to hire, people like Alex Trochut and Joshua Davis, who had been my internet hero for a decade. What I found was that if I encoded the 2x image at a JPEG quality close to zero, it came down to two or three kilobytes against the hundred or so of the real thing. A hundred kilobytes is nothing on WiFi and lands in well under a second, but on the 3G of the day the penalty was obvious. So we sent the ruined version first and swapped in the real one once it arrived. The perceived improvement was dramatic, and the interesting part is that nobody read the degraded image as a defect. They read it as the image rendering. Remember what the alternatives looked like: either the picture painted itself line by line until it finished, or you stared at nothing until it was fully downloaded and it appeared all at once. For PNG and GIF that was genuinely awful. Progressive JPEG was a consolation prize, not a solution.

Then it got better. I realized we did not need to ship a 1x and a 2x at all. A profile image stored at 640 by 640 was being displayed at 320, so instead of sending two assets I sent one deliberately enormous one, encoded at a fraction of its original quality, and let the view scale it down. Downscaling by that much hides compression artifacts almost perfectly, so the result looked razor sharp on retina while the file collapsed to a size that had no business looking that good. The servers stopped generating variants, the phone stopped making two or three round trips for the same picture, and the users told us the images loaded instantly and looked perfect. Both halves of that sentence were true, which does not happen often.

The best discovery of the whole period was an accident. Even after we had made asset loading and API responses genuinely efficient, plenty of flows still felt slow, and I could see the frustration in the room even though I had the numbers in front of me saying the data was arriving in milliseconds. Then someone said, almost as an aside, that they saw loading spinners everywhere. That was the click. The API was fast, and the app was rendering a spinner regardless, because that is how we had built every list. Even when the spinner lasted under a second, the user paid a visual tax that told them, explicitly, that they were waiting. We changed two things. Spinners disappeared from every place they were not earning their keep, and where they stayed, they only appeared if the load had not finished after two seconds. In their place we drew placeholders for images, text, and the rest of the furniture, so a screen arriving looked like a screen arriving rather than a hole with a wheel in it. We shipped that without touching the API at all, and the reaction was immediate and euphoric. A lot of people were convinced we had finally rewritten the app. It was a few lines of Objective-C.

I know that period produced an enormous pile of UX writing insisting that failing to show a spinner was close to sacrilege, and that always struck me as advice written by people who had never watched a real user’s face. I removed every one I could and felt no guilt whatsoever. If anything it felt like an act of devotion.

Those wins were on native platforms, where the constraints were impossible to ignore. On the web nobody cared, because in those years the web was a desktop on WiFi and shaving a second or two off a render was a hobby, not a requirement. It was my hobby. I went looking for optimizations at every layer and found that the backend offered the largest gains and demanded the most work to get them, while the frontend offered gains that were entirely achievable and almost nobody knew how to collect. So I went after CSS, because CSS was where the waste was piling up fastest. A newspaper that adopted CSS around 2000 shipped maybe five kilobytes of it. By 2015 the same newspaper shipped seven hundred, and I have seen sites carrying four megabytes of stylesheet to satisfy a design system and a browser support matrix. People talk about that as a download cost, which is the least interesting part, because after the download the browser still has to decompress it, compute it, and render with it.

Nobody was watching the size, and the tooling made not watching easy. LESS and Sass let us write stylesheets that were pleasant and maintainable for the author while the output multiplied nearly identical rules without limit. I have written about this at length in The Utilitarian CSS Methodology and In Defense of Functional CSS, so the short version is enough here: I kept building atomized utility classes and then burying them under object-oriented CSS, and it took me years to admit that inheritance was the problem and not my discipline. Object orientation in any language is a well-aimed shot at your own foot, and composition is simply the better pattern. So I built a CSS framework on one premise, utilities plus composition and nothing else, rewrote bloated sites with it, and watched stylesheet sizes fall by amounts that made people ask what I had deleted.

Understanding why that mattered means looking at what the browser actually does, which happens in three stages. Loading is the browser pulling the stylesheet into memory and building the CSSOM, the tree that represents your rules. Rendering is the browser deciding which of those rules apply to which nodes, and this is where selectors bill you: * { … } or div h1.title genuinely costs more to resolve than h1.title, and every change to the viewport triggers a recalculation where the browser has to fight through your cascade again to decide which declaration wins. Individually these are milliseconds. Collectively, on a four megabyte stylesheet, they are seconds. Painting comes last, where the browser fills in each piece of the page in its final position. None of this is theoretical; open Developer Tools and watch it happen on your own site.

Optimizing at that level did something I did not fully expect. The pages stopped flashing, stopped reflowing under you as they settled, and started arriving as finished objects. Measured load times improved, but the thing clients commented on was that the site felt fast and smooth, which is not a metric I could have put in a report.

This is the part I want engineers to sit with. We optimize what we can measure because measuring is how we know we did anything, and then we present the measurement as though it were the product. It is not. Ten milliseconds off an API call is invisible to every human being who will ever use the thing you built, and a screen that stops announcing its own latency is a gift they will notice within a second.