The site scored 91 on mobile. Then 88. Then 75. Then 91 again. Then 100 on the final pass. Somewhere in that arc, I broke the live booking system twice in ways that took customer-facing actions out of service for short windows. This is the postmortem.

The site is liceclinicsoftexas.com. Five locations. The booking flow is the conversion event that justifies the entire build. A broken booking link is not a small bug. It is the bug.

The third-party widget problem

The booking system is Mangomint, a salon and clinic SaaS that handles scheduling, intake forms, and payments. Their integration is a JavaScript widget. You drop a script tag in your page, the script loads on page parse, and clicking any link with a href containing booking.mangomint.com triggers the widget to open a booking modal.

The Mangomint widget is well built. It is also expensive. Loading their app.js on every page costs roughly 200 KB of JavaScript and adds a measurable amount to Total Blocking Time. On a site optimizing for a 100 mobile score, 200 KB of third-party JavaScript at parse time is a significant fraction of your performance budget gone to a single feature.

The default integration is correct for most sites. For a site chasing a 100, it is not.

The first bug: deferring everything

The standard optimization for a heavy third-party widget is to defer it. Do not load the script on page parse. Wait until the user does something that suggests they need it (a click, a hover, a viewport intersection), then load it on demand. This pattern is well documented and works for analytics, chat widgets, video embeds, and most marketing scripts.

I wrote a small defer script that intercepted clicks on any link containing booking.mangomint.com, prevented the default action, loaded the Mangomint app.js dynamically, then re-triggered the click after a 1500 ms delay to give the widget time to initialize. The score moved from 88 to 91. I shipped it.

The bug surfaced two days later. The site has a feature called Larry, a small floating chatbot trigger styled as a cartoon louse. Larry sits in the bottom right corner of every page. Clicking Larry is supposed to open the Mangomint chat widget. The chat widget is part of the same Mangomint script as the booking widget, and the chat trigger function lives in the same window.Mangomint object.

What I did not realize: the click handler I wrote for deferred booking was matching every link with a Mangomint URL, including the one inside Larry’s component. Worse, Larry’s own click handler was trying to call window.Mangomint.openWebChat, but Mangomint had not loaded yet because we deferred it. So Larry’s handler fell through to the booking drawer fallback, which opened the booking iframe instead of the chat. Customers clicking Larry expecting to chat with a human were instead booking lice treatments.

The fix took twenty minutes once I diagnosed it. The diagnosis took two hours. The lesson is that “click intercept on every link matching pattern X” is a pattern that quietly assumes the only links matching pattern X are the ones you intended. In a real codebase with a third-party widget you do not control, that assumption breaks.

The corrected click handler that broke something else

The fix was to make the click handler exclude Larry. The CSS selector became something like a[href*="booking.mangomint.com"]:not(#larry-float a):not(.larry-chat-label). This stopped the booking defer script from hijacking Larry’s click. Larry’s own handler was rewritten to load Mangomint’s app.js on demand specifically for chat, then call openWebChat once it loaded.

The score had moved to 75.

This was the second bug, and it took longer to track down because everything appeared to work correctly. Booking links opened the booking drawer. Larry opened the chat. Both features functional. The score was just garbage.

The cause was that I had two competing click handlers for booking.mangomint.com links. The original booking drawer code in booking.js had its own click intercept that opened the custom drawer with an iframe to the Mangomint booking page. The defer script I added had a separate click intercept that loaded the Mangomint widget and triggered its modal. Both were running. Both were attached to the document. Both fired on the same click event. The second handler was loading 200 KB of unnecessary JavaScript every time anyone clicked any booking link, because the first handler had already opened the drawer and the user did not need the Mangomint widget at all.

The fix was to delete the defer script entirely. The booking drawer in booking.js handles all booking link clicks via its own iframe-based panel. The Mangomint widget was never needed for booking on this site. The defer script was solving a problem that did not exist, and creating two new problems in the process. Once I removed it, the score recovered to 91.

The Codex 5.5 final optimization pass moved the score from 91 to 100 a few weeks later, which is a story for another article.

Three things that should have caught these earlier

The honest postmortem question is not “what was the bug” but “what process should have caught the bug before the customer did.” In both cases I had the same answer: I did not have the right process.

The first thing that would have caught the Larry bug is automated end-to-end click testing. A single Playwright test that navigates to the homepage, clicks Larry, and asserts that the chat widget opens (rather than the booking drawer) would have failed immediately on the deferred build. I did not have such a test. I have one now.

The second thing that would have caught the duplicate-handler bug is opening the Network tab in DevTools and watching what happens on a click. The Mangomint app.js loading every time anyone clicked any booking link would have been visible immediately. I had been running PageSpeed Insights without watching the Network tab. PageSpeed gave me a score, not a behavior.

The third thing, and this is the meta-lesson, is keeping a written changelog of optimization changes with the score before and the score after. When the score went from 91 to 75 between two pushes, my response should have been “let me revert and bisect.” Instead I tried to optimize forward, which compounded the problem. A changelog turns optimization from a vibes-based activity into a systematic one.

The grey-hat note about which bugs are fine to ship

A confession that requires a small caveat first. The booking system was never broken in a way that lost the customer the booking. Larry opened the wrong widget but the wrong widget was a working booking flow. Customers who clicked Larry got a booking experience instead of a chat experience. They could still book. The chat affordance was misrouted, not absent. The bug was a user experience bug, not a revenue bug.

This matters because the practical question on a tight launch deadline is not “is this perfect” but “is the failure mode acceptable for the time I need to ship the fix.” A bug that loses bookings is not acceptable for ten minutes. A bug that misroutes a chat affordance to a still-functional booking flow is acceptable for a day if you have already diagnosed it and the fix is queued up.

Most agencies will tell you every bug is critical and every fix must ship immediately. That is the safe thing to say. It is also wrong. Production engineering is the practice of triaging bugs by failure mode and shipping fixes in the right order. The bug that costs you a customer ships first. The bug that wastes a customer’s time ships second. The bug that violates an aesthetic preference ships when you have a window.

This is the kind of thing experienced engineers know and never write down. Most clients never need to hear it. The few who ask the right question on a discovery call appreciate hearing it. It signals you have actually shipped things to production rather than just talked about shipping things to production.

What I do differently now

The patterns I changed after the booking system bugs:

I no longer use document-level click delegation for any handler that triggers a third-party widget. Click handlers are scoped to specific elements via direct addEventListener calls. If Larry needs a click handler, Larry gets one attached to its own element. If booking links need a click handler, booking links get one attached via a precise selector that I have audited against the actual DOM.

I write a Playwright test for every interactive element on a site before I ship the site. The tests are short and ugly. They click each thing, assert that the expected modal/drawer/page opens. This catches click-handler hijacking immediately on every push.

I keep a changelog for every optimization push. The changelog has three columns: what changed, score before, score after. When a push regresses the score, I revert immediately, then bisect what specifically caused the regression before re-applying anything.

I run the Network tab during PageSpeed runs, not just the PageSpeed score. The score is a summary number. The Network tab is the truth.

The reason I am writing this

Most agency content about web performance is sanitized. Everything works. Every score lands the first try. Every optimization is a pure win. This is rarely how production engineering actually goes.

The reason I am writing the postmortem is that prospects considering hiring a developer should know what they are buying. A developer who has shipped enough production code to break a booking system, diagnose the break, and write the lessons down is a different category of hire than a developer who has only ever shipped greenfield projects on personal sites. The first kind costs more and is worth more. The second kind costs less and produces sites that score well on Lighthouse and break in production.

I am the first kind. The way you can tell is that I am willing to write down the bugs I shipped. Most developers are not. That difference is most of what you are paying for when you hire a senior over a junior.

View the case study for the project this article describes, including the live site that now scores 100 on mobile despite the breaks chronicled above.