[{"data":1,"prerenderedAt":64},["ShallowReactive",2],{"blog-en":3},[4,17,29,41,54],{"slug":5,"title":6,"description":7,"category":8,"date":9,"readingMinutes":10,"cover":11,"coverAlt":12,"tags":13},"vue-3-proxy-deep-comparison","Vue 3 didn't make our autocomplete slow. 244,650 comparisons did","After a Vue 3 migration, an autocomplete with 700 domain zones took about 12 seconds to open. The cause was an old quadratic deduplication that became expensive once it read every object through a reactive Proxy.","Performance","2026-10-21",7,"\u002Fimages\u002Fblog\u002Fvue-3-proxy-deep-comparison.svg","Diagram of 700 items producing 244,650 deep comparisons through Vue Proxies, and the two fixes that removed the delay",[14,8,15,16],"Vue 3","Reactivity","Migration",{"slug":18,"title":19,"description":20,"category":21,"date":9,"readingMinutes":22,"cover":23,"coverAlt":24,"tags":25},"redesign-recovery-mode","The redesign removed the sidebar, and with it the only way to fix a broken page","A cleaner visual editor moved widget controls into the storefront preview. When custom code breaks that preview, the controls disappear with it. This is how I found that gap and the recovery mode I built for it.","Product UX",6,"\u002Fimages\u002Fblog\u002Fredesign-recovery-mode.svg","Three diagrams showing the old sidebar, the redesign where a broken preview hides all widget controls, and a recovery mode that lists widgets without the preview",[21,26,27,28],"Redesign","iframe","Error recovery",{"slug":30,"title":31,"description":32,"category":33,"date":9,"readingMinutes":10,"cover":34,"coverAlt":35,"tags":36},"one-token-two-forms","One CAPTCHA token, two waiting forms: what invisible verification broke across a platform","Invisible CAPTCHA looks like a script tag. Across several generations of store forms it turned into async-state bugs and a production rollback caused by a settings record that didn't exist.","Architecture","\u002Fimages\u002Fblog\u002Fone-token-two-forms.svg","Diagram comparing a shared verification event that resumes the wrong waiting form with a callback bound to the form that started the check",[37,38,39,40],"CAPTCHA","Async state","Forms","Rollout",{"slug":42,"title":43,"description":44,"category":45,"date":9,"readingMinutes":10,"cover":46,"coverAlt":47,"tags":48},"monaco-liquid-tokenizer","Teaching Monaco to read Liquid templates: the hard part is knowing the language under the cursor","How a custom tokenizer for HTML with Liquid, JavaScript and CSS islands made suggestions follow the cursor, and why the editor shipped as a versioned package with a three-function browser API.","Browser editors","\u002Fimages\u002Fblog\u002Fmonaco-liquid-tokenizer.svg","Diagram of one storefront template split into HTML, Liquid, JSON and CSS regions, with the editor asking the model which language is at the cursor",[49,50,51,52,53],"Monaco","Liquid","Tokenizer","Web workers","Packaging",{"slug":55,"title":56,"description":57,"category":8,"date":9,"readingMinutes":10,"cover":58,"coverAlt":59,"tags":60},"cache-by-change-not-timer","Replacing a cache timer with \"has this product changed?\"","A storefront library cached product data for a fixed time, so pages either showed stale data or re-requested products that hadn't changed. Comparing each product's update timestamp cut product-data requests by about 70%.","\u002Fimages\u002Fblog\u002Fcache-by-change-not-timer.svg","Decision table comparing a time-based cache expiry with a check of each product's update timestamp",[61,62,63,50,8],"Caching","IndexedDB","Storefront",1793195986409]