Paper Finder: A Free AI-Powered Research Tool and the 7-Second Freeze I Had to Debug A developer built Paper Finder, a free AI-powered research tool that searches arXiv, Semantic Scholar, and Crossref and re-ranks results using a browser-based AI model. The developer debugged a 7-second input freeze caused by synchronous WASM inference blocking the main thread, and resolved it by moving the model into a Web Worker. I built Paper Finder https://paperfinder.dev , a free tool that searches arXiv, Semantic Scholar, and Crossref at once, then re-ranks the results by semantic relevance. The ranking uses a small AI model that runs entirely in the browser, so there are no API costs or accounts. But it introduced a nasty bug: typing in the search box could freeze for up to seven seconds. Paper Finder initially sorted results by year. To improve relevance, I added Xenova/all-MiniLM-L6-v2 through transformers.js. It embeds the query and each result, then ranks them using cosine similarity. The results appeared before re-ranking began, so I assumed the AI work was safely happening "in the background." Then a user sent me a Chrome DevTools trace showing 1,350.9 ms of input delay. My first thought was, "That can't be the AI—it's async." I was wrong. I used Chrome's PerformanceObserver with the longtask entry type, which reports tasks that block the main thread for more than 50 ms: js const observer = new PerformanceObserver list = { for const entry of list.getEntries { console.log Blocked for ${entry.duration}ms ; } } ; observer.observe { entryTypes: "longtask" } ; After clearing the browser's model cache and running a fresh search, I saw this: { duration: 7079, start: 13735 } One task had blocked the main thread for 7,079 ms. During that time, the page couldn't process typing, clicks, or anything else. await didn't help The embedding call used await , but that doesn't move work off the main thread. It only yields while waiting for genuinely asynchronous work, such as network requests or timers. WASM inference is synchronous CPU work. Wrapping it in a Promise changes how you receive the result, not where the computation runs. The model initialization and inference were still competing with rendering and input on the main thread. I moved the model into a dedicated Web Worker: js // embedder.worker.ts self.onmessage = async event = { const extractor = await getExtractor ; const output = await extractor event.data.texts, { pooling: "mean", normalize: true, } ; self.postMessage { vectors: / ... / } ; }; The main thread now sends text to the worker and waits for the vectors to come back. The public interface didn't need to change—a Worker-backed Promise looks the same to callers. After repeating the cold-cache test: longtasks: Zero long tasks, and the search box remained responsive throughout. The main lesson: await does not mean "won't block the main thread." For client-side ML or any other CPU-heavy work, use a Web Worker from the start. The source is on GitHub https://github.com/emmanuel-adu/paper-finder , and the live app is at paperfinder.dev https://paperfinder.dev .